Browse Source

Merge pull request #9879 from nocodb/nc-feat/snapshot

feat: base snapshots
fix/oos
Anbarasu 21 hours ago committed by GitHub
parent
commit
8bb295277e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      packages/nc-gui/assets/nc-icons/database.svg
  2. 14
      packages/nc-gui/assets/nc-icons/settings.svg
  3. 10
      packages/nc-gui/assets/nc-icons/users.svg
  4. 8
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  5. 82
      packages/nc-gui/components/dashboard/settings/Misc.vue
  6. 7
      packages/nc-gui/components/dashboard/settings/base/Snapshots.vue
  7. 102
      packages/nc-gui/components/dashboard/settings/base/Visibility.vue
  8. 84
      packages/nc-gui/components/dashboard/settings/base/index.vue
  9. 22
      packages/nc-gui/components/general/AddBaseButton.vue
  10. 17
      packages/nc-gui/components/project/View.vue
  11. 7
      packages/nc-gui/composables/useBetaFeatureToggle.ts
  12. 1
      packages/nc-gui/context/index.ts
  13. 15
      packages/nc-gui/lang/en.json
  14. 1
      packages/nc-gui/lib/acl.ts
  15. 23
      packages/nc-gui/pages/index.vue
  16. 17
      packages/nc-gui/pages/index/[typeOrId]/[baseId].vue
  17. 2
      packages/nc-gui/store/config.ts
  18. 25
      packages/nocodb/src/interface/Jobs.ts
  19. 1
      packages/nocodb/src/meta/meta.service.ts
  20. 31
      packages/nocodb/src/models/Comment.ts
  21. 140
      packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
  22. 44
      packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
  23. 36
      packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
  24. 41
      packages/nocodb/src/schema/swagger.json
  25. 1
      packages/nocodb/src/services/bases.service.ts
  26. 2
      packages/nocodb/src/utils/globals.ts
  27. 57
      tests/playwright/pages/Dashboard/ProjectView/Settings.ts
  28. 3
      tests/playwright/pages/Dashboard/ProjectView/index.ts
  29. 6
      tests/playwright/pages/Dashboard/Settings/Miscellaneous.ts
  30. 8
      tests/playwright/pages/Dashboard/Settings/index.ts
  31. 1
      tests/playwright/tests/db/features/erd.spec.ts
  32. 11
      tests/playwright/tests/db/features/filters.spec.ts

8
packages/nc-gui/assets/nc-icons/database.svg

@ -1,3 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 5.14286C4.25029 5.14286 2.81286 4.90171 1.68771 4.41943C0.562571 3.93714 0 3.32114 0 2.57143C0 1.86914 0.589143 1.26486 1.76743 0.758571C2.94629 0.252857 4.35714 0 6 0C7.64286 0 9.05371 0.252857 10.2326 0.758571C11.4109 1.26486 12 1.86914 12 2.57143C12 3.32114 11.4374 3.93714 10.3123 4.41943C9.18714 4.90171 7.74971 5.14286 6 5.14286ZM6 8.57143C4.29771 8.57143 2.872 8.32429 1.72286 7.83C0.574286 7.33629 0 6.72629 0 6V4.32171C0 4.66686 0.160571 4.99114 0.481714 5.29457C0.803429 5.598 1.238 5.866 1.78543 6.09857C2.33343 6.33057 2.97029 6.51486 3.696 6.65143C4.42229 6.78857 5.19029 6.85714 6 6.85714C6.80971 6.85714 7.57771 6.78857 8.304 6.65143C9.02971 6.51486 9.66657 6.33057 10.2146 6.09857C10.762 5.866 11.1966 5.598 11.5183 5.29457C11.8394 4.99114 12 4.66686 12 4.32171V6C12 6.72629 11.4257 7.33629 10.2771 7.83C9.128 8.32429 7.70229 8.57143 6 8.57143ZM6 12C4.33314 12 2.91657 11.7411 1.75029 11.2234C0.583428 10.7057 0 10.0777 0 9.33943V7.66114C0 8.00629 0.163714 8.33657 0.491143 8.652C0.818571 8.96743 1.25914 9.24714 1.81286 9.49114C2.366 9.73514 3.00571 9.92857 3.732 10.0714C4.45829 10.2143 5.21429 10.2857 6 10.2857C6.78571 10.2857 7.54171 10.2143 8.268 10.0714C8.99429 9.92857 9.634 9.73514 10.1871 9.49114C10.7409 9.24714 11.1814 8.96743 11.5089 8.652C11.8363 8.33657 12 8.00629 12 7.66114V9.33943C12 10.0777 11.4166 10.7057 10.2497 11.2234C9.08343 11.7411 7.66686 12 6 12Z" fill="#3366FF"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M14 8C14 9.10667 11.3333 10 8 10C4.66667 10 2 9.10667 2 8" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 5.33331C11.3137 5.33331 14 4.43788 14 3.33331C14 2.22874 11.3137 1.33331 8 1.33331C4.68629 1.33331 2 2.22874 2 3.33331C2 4.43788 4.68629 5.33331 8 5.33331Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 3.33331V12.6666C2 13.7733 4.66667 14.6666 8 14.6666C11.3333 14.6666 14 13.7733 14 12.6666V3.33331" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 744 B

14
packages/nc-gui/assets/nc-icons/settings.svg

@ -1,15 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<g clip-path="url(#clip0_3711_1275)">
<path
d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M12.9333 9.99996C12.8445 10.201 12.8181 10.4241 12.8573 10.6404C12.8965 10.8566 12.9996 11.0562 13.1533 11.2133L13.1933 11.2533C13.3173 11.3771 13.4156 11.5242 13.4827 11.686C13.5498 11.8479 13.5843 12.0214 13.5843 12.1966C13.5843 12.3718 13.5498 12.5453 13.4827 12.7072C13.4156 12.8691 13.3173 13.0161 13.1933 13.14C13.0695 13.2639 12.9224 13.3623 12.7605 13.4294C12.5987 13.4965 12.4252 13.531 12.25 13.531C12.0747 13.531 11.9012 13.4965 11.7394 13.4294C11.5775 13.3623 11.4305 13.2639 11.3066 13.14L11.2666 13.1C11.1095 12.9463 10.9099 12.8432 10.6937 12.804C10.4774 12.7647 10.2544 12.7912 10.0533 12.88C9.85611 12.9645 9.68795 13.1048 9.5695 13.2836C9.45105 13.4625 9.38748 13.6721 9.38663 13.8866V14C9.38663 14.3536 9.24615 14.6927 8.9961 14.9428C8.74605 15.1928 8.40691 15.3333 8.05329 15.3333C7.69967 15.3333 7.36053 15.1928 7.11048 14.9428C6.86044 14.6927 6.71996 14.3536 6.71996 14V13.94C6.7148 13.7193 6.64337 13.5053 6.51497 13.3258C6.38656 13.1462 6.20712 13.0095 5.99996 12.9333C5.79888 12.8445 5.57583 12.8181 5.35957 12.8573C5.1433 12.8965 4.94375 12.9996 4.78663 13.1533L4.74663 13.1933C4.6228 13.3173 4.47574 13.4156 4.31388 13.4827C4.15202 13.5498 3.97851 13.5843 3.80329 13.5843C3.62807 13.5843 3.45457 13.5498 3.29271 13.4827C3.13084 13.4156 2.98379 13.3173 2.85996 13.1933C2.73599 13.0695 2.63765 12.9224 2.57055 12.7605C2.50345 12.5987 2.46891 12.4252 2.46891 12.25C2.46891 12.0747 2.50345 11.9012 2.57055 11.7394C2.63765 11.5775 2.73599 11.4305 2.85996 11.3066L2.89996 11.2666C3.05365 11.1095 3.15675 10.9099 3.19596 10.6937C3.23517 10.4774 3.2087 10.2544 3.11996 10.0533C3.03545 9.85611 2.89513 9.68795 2.71627 9.5695C2.53741 9.45105 2.32782 9.38748 2.11329 9.38663H1.99996C1.64634 9.38663 1.3072 9.24615 1.05715 8.9961C0.807102 8.74605 0.666626 8.40691 0.666626 8.05329C0.666626 7.69967 0.807102 7.36053 1.05715 7.11048C1.3072 6.86044 1.64634 6.71996 1.99996 6.71996H2.05996C2.28062 6.7148 2.49463 6.64337 2.67416 6.51497C2.85369 6.38656 2.99044 6.20712 3.06663 5.99996C3.15537 5.79888 3.18184 5.57583 3.14263 5.35957C3.10342 5.1433 3.00032 4.94375 2.84663 4.78663L2.80663 4.74663C2.68266 4.6228 2.58431 4.47574 2.51721 4.31388C2.45011 4.15202 2.41558 3.97851 2.41558 3.80329C2.41558 3.62807 2.45011 3.45457 2.51721 3.29271C2.58431 3.13084 2.68266 2.98379 2.80663 2.85996C2.93046 2.73599 3.07751 2.63765 3.23937 2.57055C3.40124 2.50345 3.57474 2.46891 3.74996 2.46891C3.92518 2.46891 4.09868 2.50345 4.26055 2.57055C4.42241 2.63765 4.56946 2.73599 4.69329 2.85996L4.73329 2.89996C4.89041 3.05365 5.08997 3.15675 5.30623 3.19596C5.5225 3.23517 5.74555 3.2087 5.94663 3.11996H5.99996C6.19714 3.03545 6.3653 2.89513 6.48375 2.71627C6.60221 2.53741 6.66577 2.32782 6.66663 2.11329V1.99996C6.66663 1.64634 6.8071 1.3072 7.05715 1.05715C7.3072 0.807102 7.64634 0.666626 7.99996 0.666626C8.35358 0.666626 8.69272 0.807102 8.94277 1.05715C9.19282 1.3072 9.33329 1.64634 9.33329 1.99996V2.05996C9.33415 2.27448 9.39771 2.48408 9.51616 2.66294C9.63461 2.8418 9.80278 2.98212 9.99996 3.06663C10.201 3.15537 10.4241 3.18184 10.6404 3.14263C10.8566 3.10342 11.0562 3.00032 11.2133 2.84663L11.2533 2.80663C11.3771 2.68266 11.5242 2.58431 11.686 2.51721C11.8479 2.45011 12.0214 2.41558 12.1966 2.41558C12.3718 2.41558 12.5453 2.45011 12.7072 2.51721C12.8691 2.58431 13.0161 2.68266 13.14 2.80663C13.2639 2.93046 13.3623 3.07751 13.4294 3.23937C13.4965 3.40124 13.531 3.57474 13.531 3.74996C13.531 3.92518 13.4965 4.09868 13.4294 4.26055C13.3623 4.42241 13.2639 4.56946 13.14 4.69329L13.1 4.73329C12.9463 4.89041 12.8432 5.08997 12.804 5.30623C12.7647 5.5225 12.7912 5.74555 12.88 5.94663V5.99996C12.9645 6.19714 13.1048 6.3653 13.2836 6.48375C13.4625 6.60221 13.6721 6.66577 13.8866 6.66663H14C14.3536 6.66663 14.6927 6.8071 14.9428 7.05715C15.1928 7.3072 15.3333 7.64634 15.3333 7.99996C15.3333 8.35358 15.1928 8.69272 14.9428 8.94277C14.6927 9.19282 14.3536 9.33329 14 9.33329H13.94C13.7254 9.33415 13.5158 9.39771 13.337 9.51616C13.1581 9.63461 13.0178 9.80278 12.9333 9.99996V9.99996Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<g clip-path="url(#clip0_1195_12945)">
<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.7091 9.90909C12.6244 10.101 12.5991 10.3139 12.6365 10.5204C12.674 10.7268 12.7724 10.9173 12.9191 11.0673L12.9573 11.1055C13.0756 11.2237 13.1695 11.364 13.2335 11.5185C13.2976 11.673 13.3305 11.8387 13.3305 12.0059C13.3305 12.1732 13.2976 12.3388 13.2335 12.4933C13.1695 12.6478 13.0756 12.7882 12.9573 12.9064C12.8391 13.0247 12.6987 13.1186 12.5442 13.1826C12.3897 13.2467 12.2241 13.2796 12.0568 13.2796C11.8896 13.2796 11.7239 13.2467 11.5694 13.1826C11.4149 13.1186 11.2746 13.0247 11.1564 12.9064L11.1182 12.8682C10.9682 12.7215 10.7777 12.6231 10.5713 12.5856C10.3649 12.5482 10.1519 12.5735 9.96 12.6582C9.77178 12.7388 9.61126 12.8728 9.4982 13.0435C9.38513 13.2143 9.32445 13.4143 9.32364 13.6191V13.7273C9.32364 14.0648 9.18955 14.3885 8.95086 14.6272C8.71218 14.8659 8.38846 15 8.05091 15C7.71336 15 7.38964 14.8659 7.15096 14.6272C6.91227 14.3885 6.77818 14.0648 6.77818 13.7273V13.67C6.77325 13.4594 6.70508 13.2551 6.58251 13.0837C6.45994 12.9123 6.28865 12.7818 6.09091 12.7091C5.89897 12.6244 5.68606 12.5991 5.47963 12.6365C5.27319 12.674 5.0827 12.7724 4.93273 12.9191L4.89455 12.9573C4.77634 13.0756 4.63598 13.1695 4.48147 13.2335C4.32696 13.2976 4.16135 13.3305 3.99409 13.3305C3.82683 13.3305 3.66122 13.2976 3.50671 13.2335C3.35221 13.1695 3.21184 13.0756 3.09364 12.9573C2.9753 12.8391 2.88143 12.6987 2.81738 12.5442C2.75333 12.3897 2.72036 12.2241 2.72036 12.0568C2.72036 11.8896 2.75333 11.7239 2.81738 11.5694C2.88143 11.4149 2.9753 11.2746 3.09364 11.1564L3.13182 11.1182C3.27852 10.9682 3.37694 10.7777 3.41437 10.5713C3.4518 10.3649 3.42653 10.1519 3.34182 9.96C3.26115 9.77178 3.12721 9.61126 2.95648 9.4982C2.78575 9.38513 2.58568 9.32445 2.38091 9.32364H2.27273C1.93518 9.32364 1.61146 9.18955 1.37277 8.95086C1.13409 8.71218 1 8.38846 1 8.05091C1 7.71336 1.13409 7.38964 1.37277 7.15096C1.61146 6.91227 1.93518 6.77818 2.27273 6.77818H2.33C2.54063 6.77325 2.74491 6.70508 2.91628 6.58251C3.08765 6.45994 3.21818 6.28865 3.29091 6.09091C3.37562 5.89897 3.40089 5.68606 3.36346 5.47963C3.32603 5.27319 3.22761 5.0827 3.08091 4.93273L3.04273 4.89455C2.92439 4.77634 2.83052 4.63598 2.76647 4.48147C2.70242 4.32696 2.66945 4.16135 2.66945 3.99409C2.66945 3.82683 2.70242 3.66122 2.76647 3.50671C2.83052 3.35221 2.92439 3.21184 3.04273 3.09364C3.16093 2.9753 3.3013 2.88143 3.4558 2.81738C3.61031 2.75333 3.77593 2.72036 3.94318 2.72036C4.11044 2.72036 4.27605 2.75333 4.43056 2.81738C4.58507 2.88143 4.72543 2.9753 4.84364 3.09364L4.88182 3.13182C5.0318 3.27852 5.22228 3.37694 5.42872 3.41437C5.63515 3.4518 5.84806 3.42653 6.04 3.34182H6.09091C6.27913 3.26115 6.43965 3.12721 6.55271 2.95648C6.66578 2.78575 6.72646 2.58568 6.72727 2.38091V2.27273C6.72727 1.93518 6.86136 1.61146 7.10005 1.37277C7.33873 1.13409 7.66245 1 8 1C8.33755 1 8.66127 1.13409 8.89995 1.37277C9.13864 1.61146 9.27273 1.93518 9.27273 2.27273V2.33C9.27354 2.53477 9.33422 2.73484 9.44729 2.90557C9.56035 3.0763 9.72087 3.21024 9.90909 3.29091C10.101 3.37562 10.3139 3.40089 10.5204 3.36346C10.7268 3.32603 10.9173 3.22761 11.0673 3.08091L11.1055 3.04273C11.2237 2.92439 11.364 2.83052 11.5185 2.76647C11.673 2.70242 11.8387 2.66945 12.0059 2.66945C12.1732 2.66945 12.3388 2.70242 12.4933 2.76647C12.6478 2.83052 12.7882 2.92439 12.9064 3.04273C13.0247 3.16093 13.1186 3.3013 13.1826 3.4558C13.2467 3.61031 13.2796 3.77593 13.2796 3.94318C13.2796 4.11044 13.2467 4.27605 13.1826 4.43056C13.1186 4.58507 13.0247 4.72543 12.9064 4.84364L12.8682 4.88182C12.7215 5.0318 12.6231 5.22228 12.5856 5.42872C12.5482 5.63515 12.5735 5.84806 12.6582 6.04V6.09091C12.7388 6.27913 12.8728 6.43965 13.0435 6.55271C13.2143 6.66578 13.4143 6.72646 13.6191 6.72727H13.7273C14.0648 6.72727 14.3885 6.86136 14.6272 7.10005C14.8659 7.33873 15 7.66245 15 8C15 8.33755 14.8659 8.66127 14.6272 8.89995C14.3885 9.13864 14.0648 9.27273 13.7273 9.27273H13.67C13.4652 9.27354 13.2652 9.33422 13.0944 9.44729C12.9237 9.56035 12.7898 9.72087 12.7091 9.90909V9.90909Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_3711_1275">
<rect width="16" height="16" fill="white" />
<clipPath id="clip0_1195_12945">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

10
packages/nc-gui/assets/nc-icons/users.svg

@ -1,6 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.3333 14V12.6667C15.3328 12.0758 15.1362 11.5019 14.7742 11.0349C14.4122 10.5679 13.9053 10.2344 13.3333 10.0867" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3334 14V12.6667C11.3334 11.9594 11.0525 11.2811 10.5524 10.781C10.0523 10.281 9.37399 10 8.66675 10H3.33341C2.62617 10 1.94789 10.281 1.4478 10.781C0.9477 11.2811 0.666748 11.9594 0.666748 12.6667V14" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 2.08667C11.2404 2.23354 11.7488 2.56714 12.1118 3.03488C12.4749 3.50262 12.672 4.07789 12.672 4.67C12.672 5.26212 12.4749 5.83739 12.1118 6.30513C11.7488 6.77287 11.2404 7.10647 10.6667 7.25334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.99992 7.33333C7.47268 7.33333 8.66659 6.13943 8.66659 4.66667C8.66659 3.19391 7.47268 2 5.99992 2C4.52716 2 3.33325 3.19391 3.33325 4.66667C3.33325 6.13943 4.52716 7.33333 5.99992 7.33333Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<g id="users">
<path id="Vector" d="M15.3333 14V12.6667C15.3329 12.0758 15.1362 11.5019 14.7742 11.0349C14.4122 10.5679 13.9054 10.2344 13.3333 10.0867" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M11.3334 14V12.6667C11.3334 11.9594 11.0524 11.2811 10.5523 10.781C10.0522 10.281 9.37393 10 8.66669 10H3.33335C2.62611 10 1.94783 10.281 1.44774 10.781C0.947639 11.2811 0.666687 11.9594 0.666687 12.6667V14" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M10.6667 2.08667C11.2403 2.23354 11.7487 2.56714 12.1118 3.03488C12.4748 3.50262 12.6719 4.07789 12.6719 4.67C12.6719 5.26212 12.4748 5.83739 12.1118 6.30513C11.7487 6.77287 11.2403 7.10647 10.6667 7.25334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_4" d="M5.99998 7.33333C7.47274 7.33333 8.66665 6.13943 8.66665 4.66667C8.66665 3.19391 7.47274 2 5.99998 2C4.52722 2 3.33331 3.19391 3.33331 4.66667C3.33331 6.13943 4.52722 7.33333 5.99998 7.33333Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

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

@ -95,8 +95,6 @@ const { activeProjectId } = storeToRefs(useBases())
const { baseUrl } = useBase()
const toggleDialog = inject(ToggleDialogInj, () => {})
const { $e } = useNuxtApp()
const isOptionsOpen = ref(false)
@ -525,6 +523,10 @@ watch(
},
)
const openBaseSettings = async (baseId: string) => {
await navigateTo(`/nc/${baseId}?page=base-settings`)
}
const showNodeTooltip = ref(true)
</script>
@ -745,7 +747,7 @@ const showNodeTooltip = ref(true)
key="teamAndSettings"
data-testid="nc-sidebar-base-settings"
class="nc-sidebar-base-base-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, base.id)"
@click="openBaseSettings(base.id)"
>
<div v-e="['c:base:settings']" class="flex gap-2 items-center">
<GeneralIcon icon="settings" class="group-hover:text-black" />

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

@ -1,82 +0,0 @@
<script setup lang="ts">
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import { onMounted } from '@vue/runtime-core'
const { includeM2M, showNull } = useGlobal()
const baseStore = useBase()
const basesStore = useBases()
const { loadTables, hasEmptyOrNullFilters } = baseStore
const { base } = storeToRefs(baseStore)
const _projectId = inject(ProjectIdInj, undefined)
const baseId = computed(() => _projectId?.value ?? base.value?.id)
const { t } = useI18n()
watch(includeM2M, async () => await loadTables())
const showNullAndEmptyInFilter = ref()
onMounted(async () => {
await basesStore.loadProject(baseId.value!, true)
showNullAndEmptyInFilter.value = basesStore.getProjectMeta(baseId.value!)?.showNullAndEmptyInFilter
})
async function showNullAndEmptyInFilterOnChange(evt: CheckboxChangeEvent) {
const base = basesStore.bases.get(baseId.value!)
if (!base) throw new Error(`Base ${baseId.value} not found`)
const meta = basesStore.getProjectMeta(baseId.value!) ?? {}
// users cannot hide null & empty option if there is existing null / empty filters
if (!evt.target.checked) {
if (await hasEmptyOrNullFilters()) {
showNullAndEmptyInFilter.value = true
message.warning(t('msg.error.nullFilterExists'))
}
}
const newProjectMeta = {
...meta,
showNullAndEmptyInFilter: showNullAndEmptyInFilter.value,
}
// update local state
base.meta = newProjectMeta
// update db
await basesStore.updateProject(baseId.value!, {
meta: JSON.stringify(newProjectMeta),
})
}
</script>
<template>
<div class="flex flex-row w-full">
<div class="flex flex-col w-full">
<div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show M2M Tables -->
<a-checkbox v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc">
{{ $t('msg.info.showM2mTables') }} <br />
<span class="text-gray-500">{{ $t('msg.info.showM2mTablesDesc') }}</span>
</a-checkbox>
</div>
<div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show NULL -->
<a-checkbox v-model:checked="showNull" v-e="['c:settings:show-null']" class="nc-settings-show-null">
{{ $t('msg.info.showNullInCells') }} <br />
<span class="text-gray-500">{{ $t('msg.info.showNullInCellsDesc') }}</span>
</a-checkbox>
</div>
<div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show NULL and EMPTY in Filters -->
<a-checkbox
v-model:checked="showNullAndEmptyInFilter"
v-e="['c:settings:show-null-and-empty-in-filter']"
class="nc-settings-show-null-and-empty-in-filter"
@change="showNullAndEmptyInFilterOnChange"
>
{{ $t('msg.info.showNullAndEmptyInFilter') }} <br />
<span class="text-gray-500">{{ $t('msg.info.showNullAndEmptyInFilterDesc') }}</span>
</a-checkbox>
</div>
</div>
</div>
</template>

7
packages/nc-gui/components/dashboard/settings/base/Snapshots.vue

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<div></div>
</template>
<style scoped lang="scss"></style>

102
packages/nc-gui/components/dashboard/settings/base/Visibility.vue

@ -0,0 +1,102 @@
<script setup lang="ts">
const { t } = useI18n()
const baseStore = useBase()
const basesStore = useBases()
const { base } = storeToRefs(baseStore)
const _projectId = inject(ProjectIdInj, undefined)
const { loadTables, hasEmptyOrNullFilters } = baseStore
const baseId = computed(() => _projectId?.value ?? base.value?.id)
const showNullAndEmptyInFilter = ref()
const { includeM2M, showNull } = useGlobal()
watch(includeM2M, async () => await loadTables())
onMounted(async () => {
await basesStore.loadProject(baseId.value!, true)
showNullAndEmptyInFilter.value = basesStore.getProjectMeta(baseId.value!)?.showNullAndEmptyInFilter
})
async function showNullAndEmptyInFilterOnChange(evt: boolean) {
const base = basesStore.bases.get(baseId.value!)
if (!base) throw new Error(`Base ${baseId.value} not found`)
const meta = basesStore.getProjectMeta(baseId.value!) ?? {}
// users cannot hide null & empty option if there is existing null / empty filters
if (!evt) {
if (await hasEmptyOrNullFilters()) {
showNullAndEmptyInFilter.value = true
message.warning(t('msg.error.nullFilterExists'))
}
}
const newProjectMeta = {
...meta,
showNullAndEmptyInFilter: showNullAndEmptyInFilter.value,
}
// update local state
base.meta = newProjectMeta
// update db
await basesStore.updateProject(baseId.value!, {
meta: JSON.stringify(newProjectMeta),
})
}
</script>
<template>
<div data-testid="nc-settings-subtab-visibility" class="item-card flex flex-col w-full">
<div class="text-nc-content-gray-emphasis font-semibold text-lg">
{{ $t('labels.visibilityAndDataHandling') }}
</div>
<div class="text-nc-content-gray-subtle2 mt-2 leading-5">
{{ $t('labels.visibilityConfigLabel') }}
</div>
<div class="flex flex-col border-1 rounded-lg mt-6 border-nc-border-gray-medium">
<div class="flex w-full px-3 py-2 gap-2 flex-col">
<div class="flex w-full gap-1 items-center">
<NcSwitch v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc-m2m">
<span class="text-nc-content-gray font-semibold flex-1">
{{ $t('msg.info.showM2mTables') }}
</span>
</NcSwitch>
</div>
<span class="text-gray-500 pl-10">{{ $t('msg.info.showM2mTablesDesc') }}</span>
</div>
<div class="flex w-full px-3 border-t-1 border-nc-border-gray-medium py-2 gap-2 flex-col">
<div class="flex w-full gap-1 items-center">
<NcSwitch v-model:checked="showNull" v-e="['c:settings:show-null']" class="nc-settings-show-null">
<span class="text-nc-content-gray font-semibold flex-1">
{{ $t('msg.info.showNullInCells') }}
</span>
</NcSwitch>
</div>
<span class="text-gray-500 pl-10">{{ $t('msg.info.showNullInCellsDesc') }}</span>
</div>
<div class="flex w-full px-3 py-2 border-t-1 border-nc-border-gray-medium gap-2 flex-col">
<div class="flex w-full gap-1 items-center">
<NcSwitch
v-model:checked="showNullAndEmptyInFilter"
v-e="['c:settings:show-null-and-empty-in-filter']"
class="nc-settings-show-null-and-empty-in-filter"
@change="showNullAndEmptyInFilterOnChange"
>
<span class="text-nc-content-gray font-semibold flex-1">
{{ $t('msg.info.showNullAndEmptyInFilter') }}
</span>
</NcSwitch>
</div>
<span class="text-gray-500 pl-10">{{ $t('msg.info.showNullAndEmptyInFilterDesc') }}</span>
</div>
</div>
</div>
</template>
<style scoped lang="scss"></style>

84
packages/nc-gui/components/dashboard/settings/base/index.vue

@ -0,0 +1,84 @@
<script setup lang="ts">
const { isUIAllowed } = useRoles()
const hasPermissionForSnapshots = computed(() => isUIAllowed('manageSnapshot'))
const { isFeatureEnabled } = useBetaFeatureToggle()
const router = useRouter()
const activeMenu = ref(
isEeUI && isFeatureEnabled(FEATURE_FLAG.BASE_SNAPSHOTS) && hasPermissionForSnapshots.value ? 'snapshots' : 'visibility',
)
const selectMenu = (option: string) => {
if (!hasPermissionForSnapshots.value && option === 'snapshots') {
return
}
router.push({
query: {
...router.currentRoute.value.query,
tab: option,
},
})
activeMenu.value = option
}
onMounted(() => {
const query = router.currentRoute.value.query
if (query && query.tab && ['snapshots', 'visibility'].includes(query.tab as string)) {
selectMenu(query.tab as string)
}
})
</script>
<template>
<div class="flex p-5 nc-base-settings justify-center overflow-auto gap-8">
<!-- Left Pane -->
<div class="flex flex-col">
<div class="h-full w-60">
<div
v-if="isEeUI && hasPermissionForSnapshots && isFeatureEnabled(FEATURE_FLAG.BASE_SNAPSHOTS)"
data-testid="snapshots-tab"
:class="{
'active-menu': activeMenu === 'snapshots',
}"
class="gap-3 !hover:bg-gray-50 transition-all text-nc-content-gray flex rounded-lg items-center cursor-pointer py-1.5 px-3"
@click="selectMenu('snapshots')"
>
<GeneralIcon icon="camera" />
<span>
{{ $t('general.snapshots') }}
</span>
</div>
<div
:class="{
'active-menu': activeMenu === 'visibility',
}"
class="gap-3 !hover:bg-gray-50 transition-all text-nc-content-gray flex rounded-lg items-center cursor-pointer py-1.5 px-3"
data-testid="visibility-tab"
@click="selectMenu('visibility')"
>
<GeneralIcon icon="ncEye" />
<span>
{{ $t('labels.visibilityAndDataHandling') }}
</span>
</div>
</div>
</div>
<!-- Data Pane -->
<div class="flex flex-col w-[760px]">
<DashboardSettingsBaseSnapshots v-if="activeMenu === 'snapshots'" />
<DashboardSettingsBaseVisibility v-if="activeMenu === 'visibility'" />
</div>
</div>
</template>
<style lang="scss" scoped>
.active-menu {
@apply !bg-brand-50 font-semibold !text-nc-content-brand-disabled;
}
</style>

22
packages/nc-gui/components/general/AddBaseButton.vue

@ -1,22 +0,0 @@
<script setup lang="ts">
const { isUIAllowed } = useRoles()
const { t } = useI18n()
const toggleDialog = inject(ToggleDialogInj, () => {})
</script>
<template>
<div
v-if="isUIAllowed('settingsPage')"
class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)"
@click="toggleDialog(true, undefined, undefined, baseId)"
>
<div>
<div class="flex items-center space-x-1">
<component :is="iconMap.users" class="mr-1 nc-new-source" />
<div>{{ t('title.teamAndSettings') }}</div>
</div>
</div>
</div>
</template>

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

@ -58,8 +58,10 @@ watch(
projectPageTab.value = 'collaborator'
} else if (newVal === 'data-source') {
projectPageTab.value = 'data-source'
} else {
} else if (newVal === 'allTable') {
projectPageTab.value = 'allTable'
} else {
projectPageTab.value = 'base-settings'
}
return
}
@ -166,7 +168,7 @@ watch(
<a-tab-pane v-if="isUIAllowed('newUser', { roles: baseRoles }) && !isSharedBase" key="collaborator">
<template #tab>
<div class="tab-title" data-testid="proj-view-tab__access-settings">
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
<GeneralIcon icon="users" />
<div>{{ $t('labels.members') }}</div>
<div
v-if="userCount"
@ -185,7 +187,7 @@ watch(
<a-tab-pane v-if="isUIAllowed('sourceCreate') && base.id" key="data-source">
<template #tab>
<div class="tab-title" data-testid="proj-view-tab__data-sources">
<GeneralIcon icon="database" />
<GeneralIcon icon="ncDatabase" />
<div>{{ $t('labels.dataSources') }}</div>
<div
v-if="base.sources?.length"
@ -201,6 +203,15 @@ watch(
</template>
<DashboardSettingsDataSources v-model:state="baseSettingsState" :base-id="base.id" class="max-h-full" />
</a-tab-pane>
<a-tab-pane v-if="isUIAllowed('baseMiscSettings')" key="base-settings">
<template #tab>
<div class="tab-title" data-testid="proj-view-tab__base-settings">
<GeneralIcon icon="ncSettings" />
<div>{{ $t('activity.settings') }}</div>
</div>
</template>
<DashboardSettingsBase :base-id="base.id!" class="max-h-full" />
</a-tab-pane>
</a-tabs>
</div>
</div>

7
packages/nc-gui/composables/useBetaFeatureToggle.ts

@ -50,6 +50,13 @@ const FEATURES = [
enabled: false,
isEngineering: true,
},
{
id: 'base_snapshots',
title: 'Enable Base Snapshots',
description: 'Snapshots serve as comprehensive backups of your base, capturing its state at the time of creation.',
enabled: false,
isEngineering: true,
},
]
export const FEATURE_FLAG = Object.fromEntries(FEATURES.map((feature) => [feature.id.toUpperCase(), feature.id])) as Record<

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

@ -50,7 +50,6 @@ export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injecti
export const SharedViewPasswordInj: InjectionKey<Ref<string | null>> = Symbol('shared-view-password-injection')
export const CellUrlDisableOverlayInj: InjectionKey<Ref<boolean>> = Symbol('cell-url-disable-url')
export const DropZoneRef: InjectionKey<Ref<Element | undefined>> = Symbol('drop-zone-ref')
export const ToggleDialogInj: InjectionKey<Function> = Symbol('toggle-dialog-injection')
export const CellClickHookInj: InjectionKey<EventHook<MouseEvent> | undefined> = Symbol('cell-click-injection')
export const SaveRowInj: InjectionKey<(() => void) | undefined> = Symbol('save-row-injection')
export const CurrentCellInj: InjectionKey<Ref<Element | undefined>> = Symbol('current-cell-injection')

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

@ -93,6 +93,9 @@
"none": "None"
},
"general": {
"snapshot": "Snapshot",
"snapshots": "Snapshots",
"baseSnapshots": "Base Snapshots",
"featurePreview": "Feature Preview",
"scripts": "Scripts",
"configure": "Configure",
@ -648,6 +651,18 @@
"lockedByUser": "Locked by {user}"
},
"labels": {
"snapshotCreationFailed": "Snapshot creation failed",
"snapshotCreationFailedDescription": "Failed to create your base snapshot. Try again later.",
"snapshotCooldownDescription": "Snapshots can only be taken three hours apart.",
"snapshotCooldownWarning": "Snapshot cooldown remaining",
"snapshotLimitDescription": "You can only maintain 2 base snapshots at a time. Upgrade your plan for additional snapshots.",
"snapshotLimitReached": "Snapshot limit reached",
"confirmRestore": "Confirm Restore",
"visibilityAndDataHandling": "Visibility & Data Handling",
"visibilityConfigLabel": "Base specific additional configurations to customise data display & default behaviours.",
"snapShotSubText": "Snapshots serve as comprehensive backups of your base, capturing its state at the time of creation. Restoring a snapshot creates a new instance of the base in the designated workspace.",
"newSnapshot": "New Snapshot",
"searchASnapshot": "Search a snapshot",
"continue": "Continue",
"toggleExperimentalFeature": "Enable or disable experimental features with ease, allowing you to explore and evaluate upcoming functionalities.",
"modifiedOn": "Modified on",

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

@ -53,6 +53,7 @@ const rolePermissions = {
[ProjectRoles.OWNER]: {
include: {
baseDelete: true,
manageSnapshot: true,
},
},
[ProjectRoles.CREATOR]: {

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

@ -4,14 +4,6 @@ definePageMeta({
hasSidebar: true,
})
const dialogOpen = ref(false)
const openDialogKey = ref<string>('')
const dataSourcesState = ref<string>('')
const baseId = ref<string>()
const basesStore = useBases()
const { populateWorkspace } = useWorkspace()
@ -101,15 +93,6 @@ onMounted(() => {
}
})
})
function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) {
dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key || ''
dataSourcesState.value = dsState || ''
baseId.value = pId || ''
}
provide(ToggleDialogInj, toggleDialog)
</script>
<template>
@ -128,12 +111,6 @@ provide(ToggleDialogInj, toggleDialog)
<NuxtPage />
</template>
</NuxtLayout>
<LazyDashboardSettingsModal
v-model:model-value="dialogOpen"
v-model:open-key="openDialogKey"
v-model:data-sources-state="dataSourcesState"
:base-id="baseId"
/>
<DlgSharedBaseDuplicate v-if="isUIAllowed('baseDuplicate')" v-model="isDuplicateDlgOpen" />
</div>
</template>

17
packages/nc-gui/pages/index/[typeOrId]/[baseId].vue

@ -3,23 +3,6 @@ definePageMeta({
hideHeader: true,
hasSidebar: true,
})
const dialogOpen = ref(false)
const openDialogKey = ref<string>('')
const dataSourcesState = ref<string>('')
const baseId = ref<string>()
function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) {
dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key || ''
dataSourcesState.value = dsState || ''
baseId.value = pId || ''
}
provide(ToggleDialogInj, toggleDialog)
</script>
<template>

2
packages/nc-gui/store/config.ts

@ -18,7 +18,7 @@ export const useConfigStore = defineStore('configStore', () => {
const isMobileMode = ref(isViewPortMobile())
const projectPageTab = ref<'allTable' | 'collaborator' | 'data-source'>('allTable')
const projectPageTab = ref<'allTable' | 'collaborator' | 'data-source' | 'base-settings'>('allTable')
const onViewPortResize = () => {
isMobileMode.value = isViewPortMobile()

25
packages/nocodb/src/interface/Jobs.ts

@ -1,4 +1,5 @@
import type { AttachmentResType, PublicAttachmentScope, SupportedExportCharset, UserType } from 'nocodb-sdk';
import type { AttachmentResType, PublicAttachmentScope, SupportedExportCharset, UserType, SnapshotType } from 'nocodb-sdk';
import type { NcContext, NcRequest } from '~/interface/config';
export const JOBS_QUEUE = 'jobs';
@ -28,6 +29,8 @@ export enum JobTypes {
AttachmentCleanUp = 'attachment-clean-up',
InitMigrationJobs = 'init-migration-jobs',
UseWorker = 'use-worker',
CreateSnapshot = 'create-snapshot',
RestoreSnapshot = 'restore-snapshot',
}
export const SKIP_STORING_JOB_META = [
@ -115,6 +118,7 @@ export interface DuplicateBaseJobData extends JobData {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
excludeComments?: boolean;
};
}
@ -127,6 +131,7 @@ export interface DuplicateModelJobData extends JobData {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
excludeComments?: boolean;
};
}
@ -164,3 +169,21 @@ export interface ThumbnailGeneratorJobData extends JobData {
attachments: AttachmentResType[];
scope?: PublicAttachmentScope;
}
export interface CreateSnapshotJobData extends JobData {
sourceId: string;
snapshotBaseId: string;
req: NcRequest;
snapshot: SnapshotType;
}
export interface RestoreSnapshotJobData extends JobData {
sourceId: string;
targetBaseId: string;
targetContext: {
workspace_id: string;
base_id: string;
}
snapshot: SnapshotType;
req: NcRequest;
}

1
packages/nocodb/src/meta/meta.service.ts

@ -329,6 +329,7 @@ export class MetaService {
[MetaTable.INTEGRATIONS]: 'int',
[MetaTable.FILE_REFERENCES]: 'at',
[MetaTable.COL_BUTTON]: 'btn',
[MetaTable.SNAPSHOT]: 'snap',
};
const prefix = prefixMap[target] || 'nc';

31
packages/nocodb/src/models/Comment.ts

@ -41,6 +41,37 @@ export default class Comment implements CommentType {
return comment && new Comment(comment);
}
public static async listByModel(
context: NcContext,
fk_model_id: string,
pagination?: { limit: number; offset: number },
ncMeta = Noco.ncMeta,
): Promise<Comment[]> {
const comments = await ncMeta.metaList2(
context.workspace_id,
context.base_id,
MetaTable.COMMENTS,
{
condition: {
fk_model_id,
},
orderBy: {
id: 'asc'
},
limit: pagination?.limit,
offset: pagination?.offset,
xcCondition: {
_or: [
{ is_deleted: { eq: null } },
{ is_deleted: { eq: true } },
]
}
}
);
return comments.map(comment => new Comment(comment));
}
public static async list(
context: NcContext,
{

140
packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts

@ -4,7 +4,7 @@ import debug from 'debug';
import { isLinksOrLTAR, isVirtualCol, RelationTypes } from 'nocodb-sdk';
import { Injectable } from '@nestjs/common';
import type { Job } from 'bull';
import type { NcContext } from '~/interface/config';
import type { NcContext, NcRequest } from '~/interface/config';
import type {
DuplicateBaseJobData,
DuplicateColumnJobData,
@ -35,112 +35,152 @@ export class DuplicateProcessor {
private readonly columnsService: ColumnsService,
) {}
async duplicateBase(job: Job<DuplicateBaseJobData>) {
this.debugLog(`job started for ${job.id} (${JobTypes.DuplicateBase})`);
async duplicateBaseJob(
{
sourceBase,
targetBase,
dataSource,
req,
context,
options,
operation,
targetContext: _targetContext
}: {
sourceBase: Base; // Base to duplicate
targetBase: Base; // Base to duplicate to
dataSource: Source; // Data source to duplicate from
req: NcRequest;
context: NcContext // Context of the base to duplicate
targetContext?: NcContext // Context of the base to duplicate to
options: {
excludeData?: boolean;
excludeHooks?: boolean;
excludeViews?: boolean;
excludeComments?: boolean;
}
operation: string
}) {
const hrTime = initTime();
const { context, sourceId, dupProjectId, req, options } = job.data;
const baseId = context.base_id;
// workspace templates placeholder user
if (req.user?.id === '1') {
delete req.user;
const targetContext = _targetContext ?? {
workspace_id: targetBase.fk_workspace_id,
base_id: targetBase.id,
}
const excludeData = options?.excludeData || false;
const excludeHooks = options?.excludeHooks || false;
const excludeViews = options?.excludeViews || false;
const base = await Base.get(context, baseId);
const dupProject = await Base.get(context, dupProjectId);
const source = await Source.get(context, sourceId);
const targetContext = {
workspace_id: dupProject.fk_workspace_id,
base_id: dupProject.id,
};
try {
if (!base || !dupProject || !source) {
if(!sourceBase || !targetBase || !dataSource) {
throw new Error(`Base or source not found!`);
}
const user = (req as any).user;
const models = (await source.getModels(context)).filter(
// TODO revert this when issue with cache is fixed
(m) => m.source_id === source.id && !m.mm && m.type === 'table',
const models = (await dataSource.getModels(context)).filter(
(m) => m.source_id === dataSource.id && !m.mm && m.type === 'table',
);
const exportedModels = await this.exportService.serializeModels(context, {
modelIds: models.map((m) => m.id),
excludeViews,
excludeHooks,
excludeData,
...options
});
elapsedTime(
hrTime,
`serialize models schema for ${source.base_id}::${source.id}`,
'duplicateBase',
`serialize models schema for ${dataSource.base_id}::${dataSource.id}`,
operation,
);
if (!exportedModels) {
throw new Error(`Export failed for source '${source.id}'`);
throw new Error(`Export failed for source '${dataSource.id}'`);
}
await dupProject.getSources();
await targetBase.getSources();
const dupBase = dupProject.sources[0];
const targetBaseSource = targetBase.sources[0];
const idMap = await this.importService.importModels(targetContext, {
user,
baseId: dupProject.id,
sourceId: dupBase.id,
baseId: targetBase.id,
sourceId: targetBaseSource.id,
data: exportedModels,
req: req,
});
elapsedTime(hrTime, `import models schema`, 'duplicateBase');
elapsedTime(hrTime, `import models schema`, operation);
if (!idMap) {
throw new Error(`Import failed for source '${source.id}'`);
throw new Error(`Import failed for source '${dataSource.id}'`);
}
if (!excludeData) {
if (!options?.excludeData) {
await this.importModelsData(targetContext, context, {
idMap,
sourceProject: base,
sourceProject: sourceBase,
sourceModels: models,
destProject: dupProject,
destBase: dupBase,
destProject: targetBase,
destBase: targetBaseSource,
hrTime,
req,
});
}
await this.projectsService.baseUpdate(targetContext, {
baseId: dupProject.id,
baseId: targetBase.id,
base: {
status: null,
},
user: req.user,
req,
});
} catch (e) {
if (dupProject?.id) {
} catch(err) {
if (targetBase?.id) {
await this.projectsService.baseSoftDelete(targetContext, {
baseId: dupProject.id,
baseId: targetBase.id,
user: req.user,
req,
});
}
throw e;
throw err;
}
}
async duplicateBase(job: Job<DuplicateBaseJobData>) {
this.debugLog(`job started for ${job.id} (${JobTypes.DuplicateBase})`);
const { context, sourceId, dupProjectId, req, options } = job.data;
const baseId = context.base_id;
// workspace templates placeholder user
if (req.user?.id === '1') {
delete req.user;
}
this.debugLog(`job completed for ${job.id} (${JobTypes.DuplicateBase})`);
const excludeData = options?.excludeData || false;
const excludeHooks = options?.excludeHooks || false;
const excludeViews = options?.excludeViews || false;
const excludeComments = options?.excludeComments || excludeData || false;
const base = await Base.get(context, baseId);
const dupProject = await Base.get(context, dupProjectId);
const source = await Source.get(context, sourceId);
await this.duplicateBaseJob({
sourceBase: base,
targetBase: dupProject,
dataSource: source,
req,
context,
options: {
excludeData,
excludeHooks,
excludeViews,
excludeComments
},
operation: JobTypes.DuplicateBase
})
return { id: dupProject.id };
}
@ -157,6 +197,7 @@ export class DuplicateProcessor {
const excludeData = options?.excludeData || false;
const excludeHooks = options?.excludeHooks || false;
const excludeViews = options?.excludeViews || false;
const excludeComments = options?.excludeComments || excludeData || false;
const base = await Base.get(context, baseId);
const source = await Source.get(context, sourceId);
@ -184,6 +225,7 @@ export class DuplicateProcessor {
excludeViews,
excludeHooks,
excludeData,
excludeComments,
})
)[0];

44
packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts

@ -7,7 +7,7 @@ import { elapsedTime, initTime } from '../../helpers';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
import type { NcContext } from '~/interface/config';
import type { LinkToAnotherRecordColumn } from '~/models';
import { Base, Filter, Hook, Model, Source, View } from '~/models';
import { Base, Comment, Filter, Hook, Model, Source, View } from '~/models';
import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
import {
getViewAndModelByAliasOrId,
@ -36,6 +36,7 @@ export class ExportService {
excludeViews?: boolean;
excludeHooks?: boolean;
excludeData?: boolean;
excludeComments?: boolean;
},
) {
const { modelIds } = param;
@ -43,6 +44,8 @@ export class ExportService {
const excludeData = param?.excludeData || false;
const excludeViews = param?.excludeViews || false;
const excludeHooks = param?.excludeHooks || false;
const excludeComments =
param?.excludeComments || param?.excludeData || false;
const serializedModels = [];
@ -377,6 +380,44 @@ export class ExportService {
}
}
const serializedComments = [];
if (!excludeComments) {
const READ_BATCH_SIZE = 100;
let comments: Comment[] = [];
let offset = 0;
while (true) {
const batchComments = await Comment.listByModel(context, model.id, {
limit: READ_BATCH_SIZE + 1,
offset
});
comments.push(...batchComments.slice(0, READ_BATCH_SIZE));
if (batchComments.length <= READ_BATCH_SIZE) break;
offset += READ_BATCH_SIZE;
}
for (const comment of comments) {
idMap.set(comment.id, `${idMap.get(model.id)}::${comment.id}`);
serializedComments.push({
id: idMap.get(comment.id),
fk_model_id: idMap.get(comment.fk_model_id),
row_id: comment.row_id,
comment: comment.comment,
parent_comment_id: comment.parent_comment_id
? idMap.get(comment.parent_comment_id)
: null,
created_by: comment.created_by,
resolved_by: comment.resolved_by,
created_by_email: comment.created_by_email,
resolved_by_email: comment.resolved_by_email,
});
}
}
serializedModels.push({
model: {
id: idMap.get(model.id),
@ -443,6 +484,7 @@ export class ExportService {
view: view.view,
})),
hooks: serializedHooks,
comments: serializedComments,
});
}

36
packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts

@ -19,7 +19,7 @@ import type {
View,
} from '~/models';
import type { NcContext, NcRequest } from '~/interface/config';
import { Hook } from '~/models';
import { Comment, Hook } from '~/models';
import { Base, Column, Model, Source } from '~/models';
import {
findWithIdentifier,
@ -80,8 +80,15 @@ export class ImportService {
baseId: string;
sourceId: string;
data:
| { models: { model: any; views: any[]; hooks?: any[] }[] }
| { model: any; views: any[]; hooks?: any[] }[];
| {
models: {
model: any;
views: any[];
hooks?: any[];
comments?: any[];
}[];
}
| { model: any; views: any[]; hooks?: any[]; comments?: any[] }[];
req: NcRequest;
externalModels?: Model[];
existingModel?: Model;
@ -282,6 +289,29 @@ export class ImportService {
}
}
// create comments
for (const data of param.data) {
if (param.existingModel) break;
if (!data?.comments) break;
const modelData = data.model;
const commentsData = data.comments;
const table = tableReferences.get(modelData.id);
for (const commentD of commentsData) {
const comment = await Comment.insert(
context,
withoutId({
...commentD,
fk_model_id: table.id,
parent_comment_id: idMap.get(commentD.parent_comment_id),
}),
);
idMap.set(commentD.id, comment.id);
}
}
elapsedTime(hrTime, 'create tables with static columns', 'importModels');
const referencedColumnSet = [];

41
packages/nocodb/src/schema/swagger.json

@ -24470,7 +24470,7 @@
"title": {
"description": "Base Title",
"example": "My Base",
"maxLength": 128,
"maxLength": 50,
"minLength": 1,
"type": "string"
},
@ -27331,6 +27331,45 @@
}
}
},
"Snapshot": {
"description": "Model for Snapshot",
"type": "object",
"properties": {
"id": {
"$ref": "#/components/schemas/Id",
"description": "Unique ID"
},
"title": {
"type": "string",
"description": "Title of the Snapshot"
},
"base_id": {
"$ref": "#/components/schemas/Id",
"description": "Foreign Key to Base"
},
"snapshot_base_id": {
"$ref": "#/components/schemas/Id",
"description": "Foreign Key to Snapshot Base"
},
"fk_workspace_id": {
"$ref": "#/components/schemas/Id",
"description": "Foreign Key to Workspace"
},
"created_at": {
"format": "date",
"type": "string",
"description": "Date of creation"
},
"created_by" : {
"$ref": "#/components/schemas/Id",
"description": "User ID of the creator"
},
"status": {
"type": "string",
"description": "Status of the Snapshot"
}
}
},
"ExtensionReq": {
"type": "object",
"properties": {

1
packages/nocodb/src/services/bases.service.ts

@ -262,6 +262,7 @@ export class BasesService {
}
if (baseBody?.title.length > 50) {
// Limited for consistent behaviour across identifier names for table, view, columns
NcError.badRequest('Base title exceeds 50 characters');
}

2
packages/nocodb/src/utils/globals.ts

@ -57,6 +57,7 @@ export enum MetaTable {
INTEGRATIONS_STORE = 'nc_integrations_store_v2',
FILE_REFERENCES = 'nc_file_references',
COL_BUTTON = 'nc_col_button_v2',
SNAPSHOT = 'nc_snapshots',
}
export enum MetaTableOldV2 {
@ -190,6 +191,7 @@ export enum CacheScope {
COL_BUTTON = 'colButton',
CMD_PALETTE = 'cmdPalette',
PRODUCT_FEED = 'productFeed',
SNAPSHOT = 'snapshot',
}
export enum CacheGetType {

57
tests/playwright/pages/Dashboard/ProjectView/Settings.ts

@ -0,0 +1,57 @@
import BasePage from '../../Base';
import { ProjectViewPage } from './index';
import { expect } from '@playwright/test';
export class BaseSettingsPage extends BasePage {
readonly dashboard: DashboardPage;
readonly baseView: ProjectViewPage;
constructor(baseView: ProjectViewPage) {
super(baseView.rootPage);
this.baseView = baseView;
}
get() {
return this.baseView.get().locator('.nc-base-settings');
}
async changeTab(tabName: 'snapshots' | 'visibility') {
await this.get().getByTestId(`${tabName}-tab`).click();
await this.rootPage.waitForTimeout(1000);
}
async createSnapshot({ snapshotName }: { snapshotName: string }) {
await this.rootPage.getByTestId('add-new-snapshot').click();
await this.rootPage.waitForTimeout(1000);
await this.rootPage.locator('.new-snapshot-title').fill(snapshotName);
await this.rootPage.getByTestId('create-snapshot-btn').click();
await this.rootPage.waitForTimeout(1000);
}
async deleteSnapshot({ snapshotName }: { snapshotName: string }) {
await this.rootPage.getByTestId(`snapshot-${snapshotName}`).getByTestId('delete-snapshot-btn').click();
await this.rootPage.getByTestId('nc-delete-modal-delete-btn').click();
await this.rootPage.waitForTimeout(1000);
}
async restoreSnapshot({ snapshotName }: { snapshotName: string }) {
await this.rootPage.getByTestId(`snapshot-${snapshotName}`).getByTestId('restore-snapshot-btn').click();
await this.rootPage.getByTestId('confirm-restore-snapshot-btn').click();
await this.rootPage.waitForTimeout(3000);
}
async verifySnapshot({ snapshotName, isVisible }: { snapshotName: string; isVisible: boolean }) {
const snapshot = this.rootPage.getByTestId(`snapshot-${snapshotName}`);
if (isVisible) {
await expect(snapshot).toBeVisible({ visible: true });
} else {
await expect(snapshot).toBeVisible({ visible: false });
}
}
}

3
tests/playwright/pages/Dashboard/ProjectView/index.ts

@ -4,6 +4,7 @@ import BasePage from '../../Base';
import { DataSourcePage } from './DataSourcePage';
import { TablesViewPage } from './TablesViewPage';
import { AccessSettingsPage } from './AccessSettingsPage';
import { BaseSettingsPage } from './Settings';
export class ProjectViewPage extends BasePage {
readonly dashboard: DashboardPage;
@ -12,6 +13,7 @@ export class ProjectViewPage extends BasePage {
readonly dataSources: DataSourcePage;
readonly tables: TablesViewPage;
readonly accessSettings: AccessSettingsPage;
readonly settings: BaseSettingsPage;
// assets
readonly tab_allTables: Locator;
@ -30,6 +32,7 @@ export class ProjectViewPage extends BasePage {
this.tables = new TablesViewPage(this);
this.dataSources = new DataSourcePage(this);
this.accessSettings = new AccessSettingsPage(this);
this.settings = new BaseSettingsPage(this);
this.tab_allTables = this.get().locator('[data-testid="proj-view-tab__all-tables"]');
this.tab_dataSources = this.get().locator('[data-testid="proj-view-tab__data-sources"]');

6
tests/playwright/pages/Dashboard/Settings/Miscellaneous.ts

@ -10,11 +10,11 @@ export class MiscSettingsPage extends BasePage {
}
get() {
return this.settings.get().locator(`[data-testid="nc-settings-subtab-Misc"]`);
return this.settings.get().locator(`[data-testid="nc-settings-subtab-visibility"]`);
}
async clickShowM2MTables() {
const clickAction = () => this.get().locator('input[type="checkbox"]').first().click();
const clickAction = () => this.get().locator('.nc-settings-meta-misc-m2m').first().click();
await this.waitForResponse({
uiAction: clickAction,
requestUrlPathToMatch: 'tables?includeM2M',
@ -24,7 +24,7 @@ export class MiscSettingsPage extends BasePage {
async clickShowNullEmptyFilters() {
await this.waitForResponse({
uiAction: () => this.get().locator('input[type="checkbox"]').last().click(),
uiAction: () => this.rootPage.locator('.nc-settings-show-null-and-empty-in-filter').first().click(),
requestUrlPathToMatch: '/api/v1/db/meta/projects',
httpMethodsToMatch: ['PATCH'],
});

8
tests/playwright/pages/Dashboard/Settings/index.ts

@ -33,7 +33,7 @@ export class SettingsPage extends BasePage {
}
get() {
return this.rootPage.locator('.nc-modal-settings');
return this.rootPage.locator('.nc-base-settings');
}
async selectTab({ tab, subTab }: { tab: SettingTab; subTab?: SettingsSubTab }) {
@ -41,18 +41,12 @@ export class SettingsPage extends BasePage {
if (subTab) await this.get().locator(`li[data-menu-id="${subTab}"]`).click();
}
async selectSubTab({ subTab }: { subTab: SettingsSubTab }) {
await this.get().locator(`li[data-menu-id="${subTab}"]`).click();
}
async close() {
await this.get().locator('[data-testid="settings-modal-close-button"]').click();
await this.get().waitFor({ state: 'hidden' });
}
async toggleNullEmptyFilters() {
await this.selectTab({ tab: SettingTab.ProjectSettings, subTab: SettingsSubTab.Miscellaneous });
await this.miscellaneous.clickShowNullEmptyFilters();
await this.close();
}
}

1
tests/playwright/tests/db/features/erd.spec.ts

@ -41,7 +41,6 @@ test.describe('Erd', () => {
const toggleMM = async () => {
await dashboard.treeView.baseSettings({ title: context.base.title });
await dashboard.settings.miscellaneous.clickShowM2MTables();
await dashboard.settings.close();
};
const openProjectErd = async () => {

11
tests/playwright/tests/db/features/filters.spec.ts

@ -120,6 +120,7 @@ test.describe('Filter Tests: Numerical', () => {
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'numberBased' });
let eqStringDerived = eqString;
let isLikeStringDerived = isLikeString;
@ -309,6 +310,7 @@ test.describe('Filter Tests: Text based', () => {
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'textBased' });
const filterList = [
{
@ -429,6 +431,7 @@ test.describe('Filter Tests: Select based', () => {
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'selectBased' });
const filterList = [
{
@ -561,6 +564,7 @@ test.describe('Filter Tests: Date based', () => {
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'dateTimeBased' });
// records array with time set to 00:00:00; store time in unix epoch
const recordsTimeSetToZero = records.list.map(r => {
@ -865,6 +869,7 @@ test.describe('Filter Tests: AddOn', () => {
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'addOnTypes', networkResponse: false });
const filterList = [
{
@ -970,6 +975,7 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'Country', networkResponse: false });
// add filter for CityList column
const filterList = [
@ -1008,6 +1014,7 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'City', networkResponse: false });
// add filter for CityList column
const filterList = [
@ -1047,6 +1054,7 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'City', networkResponse: false });
// add filter for CityList column
const filterList = [
@ -1149,6 +1157,7 @@ test.describe('Filter Tests: Toggle button', () => {
// Enable NULL & EMPTY button
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'Country', networkResponse: false });
// Verify filter options
await verifyFilterOperatorList({
@ -1180,6 +1189,7 @@ test.describe('Filter Tests: Toggle button', () => {
// Disable NULL & EMPTY button
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'Country', networkResponse: false });
// wait for toast message
await dashboard.verifyToast({ message: 'Null / Empty filters exist. Please remove them first.' });
@ -1189,6 +1199,7 @@ test.describe('Filter Tests: Toggle button', () => {
// Disable NULL & EMPTY button
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
await dashboard.treeView.openTable({ title: 'Country', networkResponse: false });
});
});

Loading…
Cancel
Save