Browse Source

Merge branch 'develop' into feat/kanban-view

pull/3818/head
Wing-Kam Wong 2 years ago
parent
commit
82a4f5e045
  1. 9
      .all-contributorsrc
  2. 247
      README.md
  3. 4
      packages/nc-gui/components/general/Sponsors.test.ts
  4. 4
      packages/nc-gui/components/smartsheet-column/EditOrAdd.vue
  5. 1
      packages/nc-gui/components/smartsheet-column/FormulaOptions.vue
  6. 17
      packages/nc-gui/components/smartsheet-column/LinkedToAnotherRecordOptions.vue
  7. 1
      packages/nc-gui/components/smartsheet-column/RollupOptions.vue
  8. 11
      packages/nc-gui/components/smartsheet-column/SelectOptions.vue
  9. 8
      packages/nc-gui/components/smartsheet-header/Menu.vue
  10. 6
      packages/nc-gui/components/smartsheet-toolbar/LockType.vue
  11. 3
      packages/nc-gui/components/smartsheet-toolbar/SortListMenu.vue
  12. 13
      packages/nc-gui/components/smartsheet/Cell.vue
  13. 49
      packages/nc-gui/components/smartsheet/Grid.vue
  14. 18
      packages/nc-gui/components/smartsheet/Row.vue
  15. 15
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  16. 39
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  17. 11
      packages/nc-gui/components/tabs/Smartsheet.vue
  18. 20
      packages/nc-gui/components/tabs/auth/ApiTokenManagement.vue
  19. 6
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  20. 7
      packages/nc-gui/components/virtual-cell/HasMany.vue
  21. 7
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  22. 9
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  23. 21
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  24. 1
      packages/nc-gui/composables/useColumnCreateStore.ts
  25. 16
      packages/nc-gui/composables/useExpandedFormStore.ts
  26. 17
      packages/nc-gui/composables/useLTARStore.ts
  27. 18
      packages/nc-gui/composables/useSmartsheetRowStore.ts
  28. 2
      packages/nc-gui/composables/useTable.ts
  29. 27
      packages/nc-gui/composables/useViewData.ts
  30. 1
      packages/nc-gui/context/index.ts
  31. 2
      packages/nc-gui/package-lock.json
  32. 10
      packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue
  33. 2
      packages/nc-gui/pages/signup/[[token]].vue
  34. 40
      packages/nc-gui/plugins/initializeFeedbackForm.ts
  35. 2
      packages/nc-lib-gui/package.json
  36. 4
      packages/nocodb-sdk/package-lock.json
  37. 2
      packages/nocodb-sdk/package.json
  38. 5
      packages/nocodb-sdk/src/lib/Api.ts
  39. 20
      packages/nocodb/package-lock.json
  40. 4
      packages/nocodb/package.json
  41. 6
      packages/nocodb/src/lib/meta/api/utilApis.ts
  42. 18
      packages/nocodb/src/lib/models/FormViewColumn.ts
  43. 18
      packages/nocodb/src/lib/models/GalleryViewColumn.ts
  44. 18
      packages/nocodb/src/lib/models/GridViewColumn.ts
  45. 2
      packages/nocodb/src/lib/utils/NcConfigFactory.ts
  46. 13
      scripts/cypress/integration/common/3a_filter_sort_fields_operations.js
  47. 359
      scripts/cypress/integration/common/3f_link_to_another_record.js
  48. 2
      scripts/cypress/integration/test/restTableOps.js
  49. 737
      scripts/cypress/support/commands.js
  50. 4
      scripts/cypress/support/page_objects/mainPage.js
  51. 20
      scripts/sdk/swagger.json

9
.all-contributorsrc

@ -855,6 +855,15 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "asheerrizvi",
"name": "Asheer Rizvi",
"avatar_url": "https://avatars.githubusercontent.com/u/17976252?v=4",
"profile": "http://asheerrizvi.com",
"contributions": [
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

247
README.md

@ -330,128 +330,131 @@ Our mission is to provide the most powerful no-code interface for databases whic
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
<table> <table>
<tr> <tbody>
<td align="center"><a href="https://github.com/o1lab"><img src="https://avatars.githubusercontent.com/u/5435402?v=4?s=100" width="100px;" alt=""/><br /><sub><b>o1lab</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=o1lab" title="Code">💻</a></td> <tr>
<td align="center"><a href="https://github.com/pranavxc"><img src="https://avatars.githubusercontent.com/u/61551451?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pranav C</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=pranavxc" title="Code">💻</a></td> <td align="center"><a href="https://github.com/o1lab"><img src="https://avatars.githubusercontent.com/u/5435402?v=4?s=100" width="100px;" alt=""/><br /><sub><b>o1lab</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=o1lab" title="Code">💻</a></td>
<td align="center"><a href="http://bvkatwijk.nl/"><img src="https://avatars.githubusercontent.com/u/18490578?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Boris van Katwijk</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bvkatwijk" title="Code">💻</a></td> <td align="center"><a href="https://github.com/pranavxc"><img src="https://avatars.githubusercontent.com/u/61551451?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pranav C</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=pranavxc" title="Code">💻</a></td>
<td align="center"><a href="https://stackshare.io/markuman/my-stack"><img src="https://avatars.githubusercontent.com/u/3920157?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Markus Bergholz</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=markuman" title="Code">💻</a></td> <td align="center"><a href="http://bvkatwijk.nl/"><img src="https://avatars.githubusercontent.com/u/18490578?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Boris van Katwijk</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bvkatwijk" title="Code">💻</a></td>
<td align="center"><a href="https://daniel-ruf.de/"><img src="https://avatars.githubusercontent.com/u/827205?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Ruf</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=DanielRuf" title="Code">💻</a></td> <td align="center"><a href="https://stackshare.io/markuman/my-stack"><img src="https://avatars.githubusercontent.com/u/3920157?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Markus Bergholz</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=markuman" title="Code">💻</a></td>
<td align="center"><a href="http://bertverhelst.ga/"><img src="https://avatars.githubusercontent.com/u/1710840?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bert Verhelst</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bertyhell" title="Code">💻</a></td> <td align="center"><a href="https://daniel-ruf.de/"><img src="https://avatars.githubusercontent.com/u/827205?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Ruf</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=DanielRuf" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/chocholand"><img src="https://avatars.githubusercontent.com/u/6572227?v=4?s=100" width="100px;" alt=""/><br /><sub><b>JaeWon</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=chocholand" title="Code">💻</a></td> <td align="center"><a href="http://bertverhelst.ga/"><img src="https://avatars.githubusercontent.com/u/1710840?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bert Verhelst</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bertyhell" title="Code">💻</a></td>
</tr> <td align="center"><a href="https://github.com/chocholand"><img src="https://avatars.githubusercontent.com/u/6572227?v=4?s=100" width="100px;" alt=""/><br /><sub><b>JaeWon</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=chocholand" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xflotus</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=0xflotus" title="Code">💻</a></td> <tr>
<td align="center"><a href="http://www.simonguionniere.com/"><img src="https://avatars.githubusercontent.com/u/3633017?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simon Guionniere</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=sguionni" title="Code">💻</a></td> <td align="center"><a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xflotus</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=0xflotus" title="Code">💻</a></td>
<td align="center"><a href="https://clients.extremeshok.com/"><img src="https://avatars.githubusercontent.com/u/5957328?v=4?s=100" width="100px;" alt=""/><br /><sub><b>eXtremeSHOK</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=extremeshok" title="Code">💻</a></td> <td align="center"><a href="http://www.simonguionniere.com/"><img src="https://avatars.githubusercontent.com/u/3633017?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simon Guionniere</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=sguionni" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/v2io"><img src="https://avatars.githubusercontent.com/u/48987429?v=4?s=100" width="100px;" alt=""/><br /><sub><b>v2io</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=v2io" title="Code">💻</a></td> <td align="center"><a href="https://clients.extremeshok.com/"><img src="https://avatars.githubusercontent.com/u/5957328?v=4?s=100" width="100px;" alt=""/><br /><sub><b>eXtremeSHOK</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=extremeshok" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/soaserele"><img src="https://avatars.githubusercontent.com/u/1093368?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Stanislav Oaserele</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=soaserele" title="Code">💻</a></td> <td align="center"><a href="https://github.com/v2io"><img src="https://avatars.githubusercontent.com/u/48987429?v=4?s=100" width="100px;" alt=""/><br /><sub><b>v2io</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=v2io" title="Code">💻</a></td>
<td align="center"><a href="https://ans4175.dev/"><img src="https://avatars.githubusercontent.com/u/3961872?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ahmad Anshorimuslim Syuhada</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ans-4175" title="Code">💻</a></td> <td align="center"><a href="https://github.com/soaserele"><img src="https://avatars.githubusercontent.com/u/1093368?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Stanislav Oaserele</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=soaserele" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lotas"><img src="https://avatars.githubusercontent.com/u/83861?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yaraslau Kurmyza</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=lotas" title="Code">💻</a></td> <td align="center"><a href="https://ans4175.dev/"><img src="https://avatars.githubusercontent.com/u/3961872?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ahmad Anshorimuslim Syuhada</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ans-4175" title="Code">💻</a></td>
</tr> <td align="center"><a href="https://github.com/lotas"><img src="https://avatars.githubusercontent.com/u/83861?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yaraslau Kurmyza</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=lotas" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="http://stackexchange.com/users/1677570/ferrybig"><img src="https://avatars.githubusercontent.com/u/1576684?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fernando van Loenhout</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ferrybig" title="Code">💻</a></td> <tr>
<td align="center"><a href="http://blog.quidquid.fr/"><img src="https://avatars.githubusercontent.com/u/1001585?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jrevault</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=jrevault" title="Code">💻</a></td> <td align="center"><a href="http://stackexchange.com/users/1677570/ferrybig"><img src="https://avatars.githubusercontent.com/u/1576684?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fernando van Loenhout</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ferrybig" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/atilacamurca"><img src="https://avatars.githubusercontent.com/u/508624?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Átila Camurça Alves</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=atilacamurca" title="Code">💻</a></td> <td align="center"><a href="http://blog.quidquid.fr/"><img src="https://avatars.githubusercontent.com/u/1001585?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jrevault</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=jrevault" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/simonbowen"><img src="https://avatars.githubusercontent.com/u/8931?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simon Bowen</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=simonbowen" title="Code">💻</a></td> <td align="center"><a href="https://github.com/atilacamurca"><img src="https://avatars.githubusercontent.com/u/508624?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Átila Camurça Alves</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=atilacamurca" title="Code">💻</a></td>
<td align="center"><a href="https://wingk-wong.blogspot.com/"><img src="https://avatars.githubusercontent.com/u/35857179?v=4?s=100" width="100px;" alt=""/><br /><sub><b>աɨռɢӄաօռɢ</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=wingkwong" title="Code">💻</a></td> <td align="center"><a href="https://github.com/simonbowen"><img src="https://avatars.githubusercontent.com/u/8931?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simon Bowen</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=simonbowen" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ferdiga"><img src="https://avatars.githubusercontent.com/u/6248560?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ferdinand Gassauer</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ferdiga" title="Code">💻</a></td> <td align="center"><a href="https://wingk-wong.blogspot.com/"><img src="https://avatars.githubusercontent.com/u/35857179?v=4?s=100" width="100px;" alt=""/><br /><sub><b>աɨռɢӄաօռɢ</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=wingkwong" title="Code">💻</a></td>
<td align="center"><a href="https://daneke.ru/"><img src="https://avatars.githubusercontent.com/u/4980165?v=4?s=100" width="100px;" alt=""/><br /><sub><b>George Daneke</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=Flatroy" title="Code">💻</a></td> <td align="center"><a href="https://github.com/ferdiga"><img src="https://avatars.githubusercontent.com/u/6248560?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ferdinand Gassauer</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ferdiga" title="Code">💻</a></td>
</tr> <td align="center"><a href="https://daneke.ru/"><img src="https://avatars.githubusercontent.com/u/4980165?v=4?s=100" width="100px;" alt=""/><br /><sub><b>George Daneke</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=Flatroy" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://jwillmer.de/"><img src="https://avatars.githubusercontent.com/u/1503577?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jens Willmer</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=jwillmer" title="Code">💻</a></td> <tr>
<td align="center"><a href="http://bhanu.io/"><img src="https://avatars.githubusercontent.com/u/2958857?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bhanu Pratap Chaudhary</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bhanuc" title="Code">💻</a></td> <td align="center"><a href="https://jwillmer.de/"><img src="https://avatars.githubusercontent.com/u/1503577?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jens Willmer</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=jwillmer" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jwetzell"><img src="https://avatars.githubusercontent.com/u/18341515?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joel Wetzell</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=jwetzell" title="Code">💻</a></td> <td align="center"><a href="http://bhanu.io/"><img src="https://avatars.githubusercontent.com/u/2958857?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bhanu Pratap Chaudhary</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bhanuc" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SebGTx"><img src="https://avatars.githubusercontent.com/u/8062146?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SebGTx</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=SebGTx" title="Code">💻</a></td> <td align="center"><a href="https://github.com/jwetzell"><img src="https://avatars.githubusercontent.com/u/18341515?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joel Wetzell</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=jwetzell" title="Code">💻</a></td>
<td align="center"><a href="https://farazpatankar.com/"><img src="https://avatars.githubusercontent.com/u/10681116?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Faraz Patankar</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=FarazPatankar" title="Code">💻</a></td> <td align="center"><a href="https://github.com/SebGTx"><img src="https://avatars.githubusercontent.com/u/8062146?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SebGTx</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=SebGTx" title="Code">💻</a></td>
<td align="center"><a href="https://pixplix.com/"><img src="https://avatars.githubusercontent.com/u/71349937?v=4?s=100" width="100px;" alt=""/><br /><sub><b>PixPlix</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=pixplix" title="Code">💻</a></td> <td align="center"><a href="https://farazpatankar.com/"><img src="https://avatars.githubusercontent.com/u/10681116?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Faraz Patankar</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=FarazPatankar" title="Code">💻</a></td>
<td align="center"><a href="http://alejandro.giacometti.me/"><img src="https://avatars.githubusercontent.com/u/31504?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alejandro Giacometti</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=janrito" title="Code">💻</a></td> <td align="center"><a href="https://pixplix.com/"><img src="https://avatars.githubusercontent.com/u/71349937?v=4?s=100" width="100px;" alt=""/><br /><sub><b>PixPlix</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=pixplix" title="Code">💻</a></td>
</tr> <td align="center"><a href="http://alejandro.giacometti.me/"><img src="https://avatars.githubusercontent.com/u/31504?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alejandro Giacometti</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=janrito" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://brunomoreira.opo.pt"><img src="https://avatars.githubusercontent.com/u/3017910?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bruno Moreira</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bmscmoreira" title="Code">💻</a></td> <tr>
<td align="center"><a href="https://github.com/AztrexDX"><img src="https://avatars.githubusercontent.com/u/86340924?v=4?s=100" width="100px;" alt=""/><br /><sub><b>AztrexDX</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=AztrexDX" title="Code">💻</a></td> <td align="center"><a href="https://brunomoreira.opo.pt"><img src="https://avatars.githubusercontent.com/u/3017910?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bruno Moreira</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bmscmoreira" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ejose19"><img src="https://avatars.githubusercontent.com/u/8742215?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ejose19</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ejose19" title="Code">💻</a></td> <td align="center"><a href="https://github.com/AztrexDX"><img src="https://avatars.githubusercontent.com/u/86340924?v=4?s=100" width="100px;" alt=""/><br /><sub><b>AztrexDX</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=AztrexDX" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/maximeag"><img src="https://avatars.githubusercontent.com/u/3855368?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maxime</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=maximeag" title="Code">💻</a></td> <td align="center"><a href="https://github.com/ejose19"><img src="https://avatars.githubusercontent.com/u/8742215?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ejose19</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ejose19" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dstala"><img src="https://avatars.githubusercontent.com/u/86527202?v=4?s=100" width="100px;" alt=""/><br /><sub><b>dstala</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dstala" title="Code">💻</a></td> <td align="center"><a href="https://github.com/maximeag"><img src="https://avatars.githubusercontent.com/u/3855368?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maxime</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=maximeag" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/loftwah"><img src="https://avatars.githubusercontent.com/u/19922556?v=4?s=100" width="100px;" alt=""/><br /><sub><b>loftwah</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=loftwah" title="Code">💻</a></td> <td align="center"><a href="https://github.com/dstala"><img src="https://avatars.githubusercontent.com/u/86527202?v=4?s=100" width="100px;" alt=""/><br /><sub><b>dstala</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dstala" title="Code">💻</a></td>
<td align="center"><a href="https://museosabiertos.org"><img src="https://avatars.githubusercontent.com/u/693328?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martin Gersbach</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=mrtngrsbch" title="Code">💻</a></td> <td align="center"><a href="https://github.com/loftwah"><img src="https://avatars.githubusercontent.com/u/19922556?v=4?s=100" width="100px;" alt=""/><br /><sub><b>loftwah</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=loftwah" title="Code">💻</a></td>
</tr> <td align="center"><a href="https://museosabiertos.org"><img src="https://avatars.githubusercontent.com/u/693328?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martin Gersbach</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=mrtngrsbch" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://github.com/ArjenR"><img src="https://avatars.githubusercontent.com/u/4269186?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ArjenR</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ArjenR" title="Code">💻</a></td> <tr>
<td align="center"><a href="https://github.com/kunggom"><img src="https://avatars.githubusercontent.com/u/32009637?v=4?s=100" width="100px;" alt=""/><br /><sub><b>조진식 (Jo Jinsik)</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=kunggom" title="Code">💻</a></td> <td align="center"><a href="https://github.com/ArjenR"><img src="https://avatars.githubusercontent.com/u/4269186?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ArjenR</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ArjenR" title="Code">💻</a></td>
<td align="center"><a href="http://www.cuobiezi.net"><img src="https://avatars.githubusercontent.com/u/90968567?v=4?s=100" width="100px;" alt=""/><br /><sub><b>tianchunfeng</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=tianberg" title="Code">💻</a></td> <td align="center"><a href="https://github.com/kunggom"><img src="https://avatars.githubusercontent.com/u/32009637?v=4?s=100" width="100px;" alt=""/><br /><sub><b>조진식 (Jo Jinsik)</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=kunggom" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/cthulberg"><img src="https://avatars.githubusercontent.com/u/5301275?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrea</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=cthulberg" title="Code">💻</a></td> <td align="center"><a href="http://www.cuobiezi.net"><img src="https://avatars.githubusercontent.com/u/90968567?v=4?s=100" width="100px;" alt=""/><br /><sub><b>tianchunfeng</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=tianberg" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/eevleevs"><img src="https://avatars.githubusercontent.com/u/5012744?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Malventi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=eevleevs" title="Code">💻</a></td> <td align="center"><a href="https://github.com/cthulberg"><img src="https://avatars.githubusercontent.com/u/5301275?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrea</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=cthulberg" title="Code">💻</a></td>
<td align="center"><a href="https://dev-z.github.io"><img src="https://avatars.githubusercontent.com/u/8604312?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Md. Ishtiaque Zafar</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dev-z" title="Code">💻</a></td> <td align="center"><a href="https://github.com/eevleevs"><img src="https://avatars.githubusercontent.com/u/5012744?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Malventi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=eevleevs" title="Code">💻</a></td>
<td align="center"><a href="http://www.chaslui.com"><img src="https://avatars.githubusercontent.com/u/10083758?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ChasLui</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ChasLui" title="Code">💻</a></td> <td align="center"><a href="https://dev-z.github.io"><img src="https://avatars.githubusercontent.com/u/8604312?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Md. Ishtiaque Zafar</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dev-z" title="Code">💻</a></td>
</tr> <td align="center"><a href="http://www.chaslui.com"><img src="https://avatars.githubusercontent.com/u/10083758?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ChasLui</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ChasLui" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://www.linkedin.com/in/zhansayam/"><img src="https://avatars.githubusercontent.com/u/41486762?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Zhansaya Maksut</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ZhansayaM" title="Code">💻</a></td> <tr>
<td align="center"><a href="https://github.com/agkfri"><img src="https://avatars.githubusercontent.com/u/37952138?v=4?s=100" width="100px;" alt=""/><br /><sub><b>agkfri</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=agkfri" title="Code">💻</a></td> <td align="center"><a href="https://www.linkedin.com/in/zhansayam/"><img src="https://avatars.githubusercontent.com/u/41486762?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Zhansaya Maksut</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ZhansayaM" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iqiziqi"><img src="https://avatars.githubusercontent.com/u/8640316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ziqi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=iqiziqi" title="Code">💻</a></td> <td align="center"><a href="https://github.com/agkfri"><img src="https://avatars.githubusercontent.com/u/37952138?v=4?s=100" width="100px;" alt=""/><br /><sub><b>agkfri</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=agkfri" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/AllanSiqueira"><img src="https://avatars.githubusercontent.com/u/14025084?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Siqueira</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=AllanSiqueira" title="Code">💻</a></td> <td align="center"><a href="https://github.com/iqiziqi"><img src="https://avatars.githubusercontent.com/u/8640316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ziqi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=iqiziqi" title="Code">💻</a></td>
<td align="center"><a href="https://creatify.my.id/"><img src="https://avatars.githubusercontent.com/u/54095238?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fatih</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ahmadfatihin" title="Code">💻</a></td> <td align="center"><a href="https://github.com/AllanSiqueira"><img src="https://avatars.githubusercontent.com/u/14025084?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Siqueira</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=AllanSiqueira" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/roman-rezinkin"><img src="https://avatars.githubusercontent.com/u/17882264?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Roman Rezinkin</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=roman-rezinkin" title="Code">💻</a></td> <td align="center"><a href="https://creatify.my.id/"><img src="https://avatars.githubusercontent.com/u/54095238?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fatih</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=ahmadfatihin" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/fragalcer"><img src="https://avatars.githubusercontent.com/u/31025299?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Francisco Gallardo</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=fragalcer" title="Code">💻</a></td> <td align="center"><a href="https://github.com/roman-rezinkin"><img src="https://avatars.githubusercontent.com/u/17882264?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Roman Rezinkin</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=roman-rezinkin" title="Code">💻</a></td>
</tr> <td align="center"><a href="https://github.com/fragalcer"><img src="https://avatars.githubusercontent.com/u/31025299?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Francisco Gallardo</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=fragalcer" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://github.com/sesam"><img src="https://avatars.githubusercontent.com/u/8921?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simon B.</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=sesam" title="Code">💻</a></td> <tr>
<td align="center"><a href="https://github.com/lielfr"><img src="https://avatars.githubusercontent.com/u/360928?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Liel Fridman</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=lielfr" title="Code">💻</a></td> <td align="center"><a href="https://github.com/sesam"><img src="https://avatars.githubusercontent.com/u/8921?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simon B.</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=sesam" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/rubjo"><img src="https://avatars.githubusercontent.com/u/42270947?v=4?s=100" width="100px;" alt=""/><br /><sub><b>rubjo</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=rubjo" title="Code">💻</a></td> <td align="center"><a href="https://github.com/lielfr"><img src="https://avatars.githubusercontent.com/u/360928?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Liel Fridman</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=lielfr" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/kinga-marszalkowska"><img src="https://avatars.githubusercontent.com/u/64398325?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kinga Marszałkowska</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=kinga-marszalkowska" title="Code">💻</a></td> <td align="center"><a href="https://github.com/rubjo"><img src="https://avatars.githubusercontent.com/u/42270947?v=4?s=100" width="100px;" alt=""/><br /><sub><b>rubjo</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=rubjo" title="Code">💻</a></td>
<td align="center"><a href="https://nimbusec.com"><img src="https://avatars.githubusercontent.com/u/10920640?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christof Horschitz</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dahawk" title="Code">💻</a></td> <td align="center"><a href="https://github.com/kinga-marszalkowska"><img src="https://avatars.githubusercontent.com/u/64398325?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kinga Marszałkowska</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=kinga-marszalkowska" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bebora"><img src="https://avatars.githubusercontent.com/u/32399075?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simone</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bebora" title="Code">💻</a></td> <td align="center"><a href="https://nimbusec.com"><img src="https://avatars.githubusercontent.com/u/10920640?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christof Horschitz</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dahawk" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/tyonirwansyah"><img src="https://avatars.githubusercontent.com/u/73389687?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tyo Nirwansyah</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=tyonirwansyah" title="Code">💻</a></td> <td align="center"><a href="https://github.com/bebora"><img src="https://avatars.githubusercontent.com/u/32399075?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simone</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bebora" title="Code">💻</a></td>
</tr> <td align="center"><a href="https://github.com/tyonirwansyah"><img src="https://avatars.githubusercontent.com/u/73389687?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tyo Nirwansyah</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=tyonirwansyah" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://github.com/jiione"><img src="https://avatars.githubusercontent.com/u/83341978?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jiwon</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=jiione" title="Code">💻</a></td> <tr>
<td align="center"><a href="http://quantimo.do"><img src="https://avatars.githubusercontent.com/u/2808553?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mike P. Sinn</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=mikepsinn" title="Code">💻</a></td> <td align="center"><a href="https://github.com/jiione"><img src="https://avatars.githubusercontent.com/u/83341978?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jiwon</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=jiione" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/candideu"><img src="https://avatars.githubusercontent.com/u/55474996?v=4?s=100" width="100px;" alt=""/><br /><sub><b>candideu</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=candideu" title="Code">💻</a></td> <td align="center"><a href="http://quantimo.do"><img src="https://avatars.githubusercontent.com/u/2808553?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mike P. Sinn</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=mikepsinn" title="Code">💻</a></td>
<td align="center"><a href="http://siderealart.me"><img src="https://avatars.githubusercontent.com/u/30827929?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SiderealArt</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=SiderealArt" title="Code">💻</a></td> <td align="center"><a href="https://github.com/candideu"><img src="https://avatars.githubusercontent.com/u/55474996?v=4?s=100" width="100px;" alt=""/><br /><sub><b>candideu</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=candideu" title="Code">💻</a></td>
<td align="center"><a href="http://vijayrathore.me"><img src="https://avatars.githubusercontent.com/u/17380265?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vijay Rathore</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=vijayrathore8492" title="Code">💻</a></td> <td align="center"><a href="http://siderealart.me"><img src="https://avatars.githubusercontent.com/u/30827929?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SiderealArt</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=SiderealArt" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/John-Appleseed"><img src="https://avatars.githubusercontent.com/u/7055847?v=4?s=100" width="100px;" alt=""/><br /><sub><b>John Appleseed</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=John-Appleseed" title="Code">💻</a></td> <td align="center"><a href="http://vijayrathore.me"><img src="https://avatars.githubusercontent.com/u/17380265?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vijay Rathore</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=vijayrathore8492" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Korayem"><img src="https://avatars.githubusercontent.com/u/198332?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Salem Korayem</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=Korayem" title="Code">💻</a></td> <td align="center"><a href="https://github.com/John-Appleseed"><img src="https://avatars.githubusercontent.com/u/7055847?v=4?s=100" width="100px;" alt=""/><br /><sub><b>John Appleseed</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=John-Appleseed" title="Code">💻</a></td>
</tr> <td align="center"><a href="https://github.com/Korayem"><img src="https://avatars.githubusercontent.com/u/198332?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Salem Korayem</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=Korayem" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://github.com/dubiao"><img src="https://avatars.githubusercontent.com/u/4001793?v=4?s=100" width="100px;" alt=""/><br /><sub><b></b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dubiao" title="Code">💻</a></td> <tr>
<td align="center"><a href="https://github.com/willnewii"><img src="https://avatars.githubusercontent.com/u/652003?v=4?s=100" width="100px;" alt=""/><br /><sub><b>诗人的咸鱼</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=willnewii" title="Code">💻</a></td> <td align="center"><a href="https://github.com/dubiao"><img src="https://avatars.githubusercontent.com/u/4001793?v=4?s=100" width="100px;" alt=""/><br /><sub><b></b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dubiao" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bitbytejoy"><img src="https://avatars.githubusercontent.com/u/11807034?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bitbytejoy</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bitbytejoy" title="Code">💻</a></td> <td align="center"><a href="https://github.com/willnewii"><img src="https://avatars.githubusercontent.com/u/652003?v=4?s=100" width="100px;" alt=""/><br /><sub><b>诗人的咸鱼</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=willnewii" title="Code">💻</a></td>
<td align="center"><a href="http://blog.pan93.com"><img src="https://avatars.githubusercontent.com/u/28441561?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pan93412</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=pan93412" title="Code">💻</a></td> <td align="center"><a href="https://github.com/bitbytejoy"><img src="https://avatars.githubusercontent.com/u/11807034?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bitbytejoy</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=bitbytejoy" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/LancerComet"><img src="https://avatars.githubusercontent.com/u/10321350?v=4?s=100" width="100px;" alt=""/><br /><sub><b>LancerComet</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=LancerComet" title="Code">💻</a></td> <td align="center"><a href="http://blog.pan93.com"><img src="https://avatars.githubusercontent.com/u/28441561?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pan93412</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=pan93412" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/mertmit"><img src="https://avatars.githubusercontent.com/u/59797957?v=4?s=100" width="100px;" alt=""/><br /><sub><b>mertmit</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=mertmit" title="Code">💻</a></td> <td align="center"><a href="https://github.com/LancerComet"><img src="https://avatars.githubusercontent.com/u/10321350?v=4?s=100" width="100px;" alt=""/><br /><sub><b>LancerComet</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=LancerComet" title="Code">💻</a></td>
<td align="center"><a href="https://blog.atompi.com"><img src="https://avatars.githubusercontent.com/u/6419682?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Atom Pi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=atompi" title="Code">💻</a></td> <td align="center"><a href="https://github.com/mertmit"><img src="https://avatars.githubusercontent.com/u/59797957?v=4?s=100" width="100px;" alt=""/><br /><sub><b>mertmit</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=mertmit" title="Code">💻</a></td>
</tr> <td align="center"><a href="https://blog.atompi.com"><img src="https://avatars.githubusercontent.com/u/6419682?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Atom Pi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=atompi" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://github.com/OskarsPakers"><img src="https://avatars.githubusercontent.com/u/3343347?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Oskars</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=OskarsPakers" title="Code">💻</a></td> <tr>
<td align="center"><a href="http://dolibit.de"><img src="https://avatars.githubusercontent.com/u/45215329?v=4?s=100" width="100px;" alt=""/><br /><sub><b>UT from dolibit</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dolibit-ut" title="Code">💻</a></td> <td align="center"><a href="https://github.com/OskarsPakers"><img src="https://avatars.githubusercontent.com/u/3343347?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Oskars</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=OskarsPakers" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/blucky"><img src="https://avatars.githubusercontent.com/u/42397?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Blucky</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=blucky" title="Code">💻</a></td> <td align="center"><a href="http://dolibit.de"><img src="https://avatars.githubusercontent.com/u/45215329?v=4?s=100" width="100px;" alt=""/><br /><sub><b>UT from dolibit</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dolibit-ut" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/TsjipTsjip"><img src="https://avatars.githubusercontent.com/u/19798667?v=4?s=100" width="100px;" alt=""/><br /><sub><b>TsjipTsjip</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=TsjipTsjip" title="Code">💻</a></td> <td align="center"><a href="https://github.com/blucky"><img src="https://avatars.githubusercontent.com/u/42397?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Blucky</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=blucky" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dhrrgn"><img src="https://avatars.githubusercontent.com/u/149921?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dan Horrigan</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dhrrgn" title="Code">💻</a></td> <td align="center"><a href="https://github.com/TsjipTsjip"><img src="https://avatars.githubusercontent.com/u/19798667?v=4?s=100" width="100px;" alt=""/><br /><sub><b>TsjipTsjip</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=TsjipTsjip" title="Code">💻</a></td>
<td align="center"><a href="https://amitjoki.github.io"><img src="https://avatars.githubusercontent.com/u/5158554?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Amit Joki</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=AmitJoki" title="Code">💻</a></td> <td align="center"><a href="https://github.com/dhrrgn"><img src="https://avatars.githubusercontent.com/u/149921?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dan Horrigan</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dhrrgn" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/tympaniplayer"><img src="https://avatars.githubusercontent.com/u/1745731?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nate</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=tympaniplayer" title="Code">💻</a></td> <td align="center"><a href="https://amitjoki.github.io"><img src="https://avatars.githubusercontent.com/u/5158554?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Amit Joki</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=AmitJoki" title="Code">💻</a></td>
</tr> <td align="center"><a href="https://github.com/tympaniplayer"><img src="https://avatars.githubusercontent.com/u/1745731?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nate</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=tympaniplayer" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://github.com/RobinFrcd"><img src="https://avatars.githubusercontent.com/u/29704178?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Robin Fourcade</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=RobinFrcd" title="Code">💻</a></td> <tr>
<td align="center"><a href="https://github.com/zprial"><img src="https://avatars.githubusercontent.com/u/33095380?v=4?s=100" width="100px;" alt=""/><br /><sub><b>zprial</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=zprial" title="Code">💻</a></td> <td align="center"><a href="https://github.com/RobinFrcd"><img src="https://avatars.githubusercontent.com/u/29704178?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Robin Fourcade</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=RobinFrcd" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nilsreichardt"><img src="https://avatars.githubusercontent.com/u/24459435?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nils Reichardt</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=nilsreichardt" title="Code">💻</a></td> <td align="center"><a href="https://github.com/zprial"><img src="https://avatars.githubusercontent.com/u/33095380?v=4?s=100" width="100px;" alt=""/><br /><sub><b>zprial</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=zprial" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iamnamananand996"><img src="https://avatars.githubusercontent.com/u/31537362?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Naman Anand</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=iamnamananand996" title="Code">💻</a></td> <td align="center"><a href="https://github.com/nilsreichardt"><img src="https://avatars.githubusercontent.com/u/24459435?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nils Reichardt</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=nilsreichardt" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/GeoffMaciolek"><img src="https://avatars.githubusercontent.com/u/10995633?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Geo Maciolek</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=GeoffMaciolek" title="Code">💻</a></td> <td align="center"><a href="https://github.com/iamnamananand996"><img src="https://avatars.githubusercontent.com/u/31537362?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Naman Anand</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=iamnamananand996" title="Code">💻</a></td>
<td align="center"><a href="http://blog.mukyu.tw/"><img src="https://avatars.githubusercontent.com/u/6008539?v=4?s=100" width="100px;" alt=""/><br /><sub><b>神楽坂帕琪</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=mudream4869" title="Code">💻</a></td> <td align="center"><a href="https://github.com/GeoffMaciolek"><img src="https://avatars.githubusercontent.com/u/10995633?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Geo Maciolek</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=GeoffMaciolek" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/titouancreach"><img src="https://avatars.githubusercontent.com/u/3995719?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Titouan CREACH</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=titouancreach" title="Code">💻</a></td> <td align="center"><a href="http://blog.mukyu.tw/"><img src="https://avatars.githubusercontent.com/u/6008539?v=4?s=100" width="100px;" alt=""/><br /><sub><b>神楽坂帕琪</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=mudream4869" title="Code">💻</a></td>
</tr> <td align="center"><a href="https://github.com/titouancreach"><img src="https://avatars.githubusercontent.com/u/3995719?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Titouan CREACH</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=titouancreach" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://www.youyi.io"><img src="https://avatars.githubusercontent.com/u/49471274?v=4?s=100" width="100px;" alt=""/><br /><sub><b>youyiio</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=youyiio" title="Code">💻</a></td> <tr>
<td align="center"><a href="https://github.com/RK311y"><img src="https://avatars.githubusercontent.com/u/65210753?v=4?s=100" width="100px;" alt=""/><br /><sub><b>River Kelly</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=RK311y" title="Code">💻</a></td> <td align="center"><a href="https://www.youyi.io"><img src="https://avatars.githubusercontent.com/u/49471274?v=4?s=100" width="100px;" alt=""/><br /><sub><b>youyiio</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=youyiio" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/LepkoQQ"><img src="https://avatars.githubusercontent.com/u/2662937?v=4?s=100" width="100px;" alt=""/><br /><sub><b>LepkoQQ</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=LepkoQQ" title="Code">💻</a></td> <td align="center"><a href="https://github.com/RK311y"><img src="https://avatars.githubusercontent.com/u/65210753?v=4?s=100" width="100px;" alt=""/><br /><sub><b>River Kelly</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=RK311y" title="Code">💻</a></td>
<td align="center"><a href="https://cornernewclub.fr"><img src="https://avatars.githubusercontent.com/u/56829191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>quentin</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=QuentinDstl" title="Code">💻</a></td> <td align="center"><a href="https://github.com/LepkoQQ"><img src="https://avatars.githubusercontent.com/u/2662937?v=4?s=100" width="100px;" alt=""/><br /><sub><b>LepkoQQ</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=LepkoQQ" title="Code">💻</a></td>
<td align="center"><a href="http://cande.me"><img src="https://avatars.githubusercontent.com/u/5407915?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cande</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=cande1gut" title="Code">💻</a></td> <td align="center"><a href="https://cornernewclub.fr"><img src="https://avatars.githubusercontent.com/u/56829191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>quentin</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=QuentinDstl" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/seokjunjin"><img src="https://avatars.githubusercontent.com/u/46950889?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Seokjun Jin</b></sub></a><br /><a href="#translation-seokjunjin" title="Translation">🌍</a></td> <td align="center"><a href="http://cande.me"><img src="https://avatars.githubusercontent.com/u/5407915?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cande</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=cande1gut" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/systemctls"><img src="https://avatars.githubusercontent.com/u/37177191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jinxm</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=systemctls" title="Code">💻</a></td> <td align="center"><a href="https://github.com/seokjunjin"><img src="https://avatars.githubusercontent.com/u/46950889?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Seokjun Jin</b></sub></a><br /><a href="#translation-seokjunjin" title="Translation">🌍</a></td>
</tr> <td align="center"><a href="https://github.com/systemctls"><img src="https://avatars.githubusercontent.com/u/37177191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jinxm</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=systemctls" title="Code">💻</a></td>
<tr> </tr>
<td align="center"><a href="https://yohanboniface.me"><img src="https://avatars.githubusercontent.com/u/146023?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yohan Boniface</b></sub></a><br /><a href="#translation-yohanboniface" title="Translation">🌍</a></td> <tr>
<td align="center"><a href="https://github.com/drsantam"><img src="https://avatars.githubusercontent.com/u/10681456?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Santam Chakraborty</b></sub></a><br /><a href="#translation-drsantam" title="Translation">🌍</a></td> <td align="center"><a href="https://yohanboniface.me"><img src="https://avatars.githubusercontent.com/u/146023?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yohan Boniface</b></sub></a><br /><a href="#translation-yohanboniface" title="Translation">🌍</a></td>
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=eltociear" title="Code">💻</a></td> <td align="center"><a href="https://github.com/drsantam"><img src="https://avatars.githubusercontent.com/u/10681456?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Santam Chakraborty</b></sub></a><br /><a href="#translation-drsantam" title="Translation">🌍</a></td>
</tr> <td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=eltociear" title="Code">💻</a></td>
<td align="center"><a href="http://asheerrizvi.com"><img src="https://avatars.githubusercontent.com/u/17976252?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Asheer Rizvi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=asheerrizvi" title="Code">💻</a></td>
</tr>
</tbody>
</table> </table>
<!-- markdownlint-restore --> <!-- markdownlint-restore -->

4
packages/nc-gui/components/general/Sponsors.test.ts

@ -1,16 +1,14 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import { expect, test } from 'vitest' import { expect, test } from 'vitest'
import Sponsors from './Sponsors.vue' import Sponsors from './Sponsors.vue'
import { createVuetifyPlugin } from '~/plugins/vuetify'
import { createI18nPlugin } from '~/plugins/a.i18n' import { createI18nPlugin } from '~/plugins/a.i18n'
const mountComponent = async (nav: boolean) => { const mountComponent = async (nav: boolean) => {
const vuetify = createVuetifyPlugin()
const i18n = await createI18nPlugin() const i18n = await createI18nPlugin()
const wrapper = mount(Sponsors, { const wrapper = mount(Sponsors, {
global: { global: {
plugins: [vuetify, i18n], plugins: [i18n],
}, },
props: { props: {
nav, nav,

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

@ -53,7 +53,9 @@ const reloadMetaAndData = async () => {
} }
async function onSubmit() { async function onSubmit() {
await addOrUpdate(reloadMetaAndData) const saved = await addOrUpdate(reloadMetaAndData)
if (!saved) return
// add delay to complete the minimize transition // add delay to complete the minimize transition
setTimeout(() => { setTimeout(() => {

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

@ -50,6 +50,7 @@ const validators = {
{ {
validator: (_: any, formula: any) => { validator: (_: any, formula: any) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if (!formula?.trim()) return reject(new Error('Required'))
const res = parseAndValidateFormula(formula) const res = parseAndValidateFormula(formula)
if (res !== true) { if (res !== true) {
return reject(new Error(res)) return reject(new Error(res))

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

@ -52,15 +52,24 @@ const refTables = $computed(() => {
<template> <template>
<div class="w-full flex flex-col mb-2 mt-4"> <div class="w-full flex flex-col mb-2 mt-4">
<div class="border-2 p-6"> <div class="border-2 p-6">
<a-form-item v-bind="validateInfos.type"> <a-form-item v-bind="validateInfos.type" class="nc-ltar-relation-type">
<a-radio-group v-model:value="vModel.type" name="type" v-bind="validateInfos.type"> <a-radio-group v-model:value="vModel.type" name="type" v-bind="validateInfos.type">
<a-radio value="hm">Has Many</a-radio> <a-radio value="hm">Has Many</a-radio>
<a-radio value="mm">Many To Many</a-radio> <a-radio value="mm">Many To Many</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item class="flex w-full pb-2 mt-4" :label="$t('labels.childTable')" v-bind="validateInfos.childId"> <a-form-item
<a-select v-model:value="vModel.childId" @change="onDataTypeChange"> class="flex w-full pb-2 mt-4 nc-ltar-child-table"
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.id"> :label="$t('labels.childTable')"
v-bind="validateInfos.childId"
>
<a-select
v-model:value="vModel.childId"
show-search
:filter-option="(value, option) => option.key.toLowerCase().includes(value.toLowerCase())"
@change="onDataTypeChange"
>
<a-select-option v-for="table in refTables" :key="table.title" :value="table.id">
{{ table.title }} {{ table.title }}
</a-select-option> </a-select-option>
</a-select> </a-select>

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

@ -35,7 +35,6 @@ const aggrFunctionsList = [
{ text: 'min', value: 'min' }, { text: 'min', value: 'min' },
{ text: 'max', value: 'max' }, { text: 'max', value: 'max' },
{ text: 'avg', value: 'avg' }, { text: 'avg', value: 'avg' },
{ text: 'min', value: 'min' },
{ text: 'sum', value: 'sum' }, { text: 'sum', value: 'sum' },
{ text: 'countDistinct', value: 'countDistinct' }, { text: 'countDistinct', value: 'countDistinct' },
{ text: 'sumDistinct', value: 'sumDistinct' }, { text: 'sumDistinct', value: 'sumDistinct' },

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

@ -15,7 +15,7 @@ const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow()
let options = $ref<any[]>([]) let options = $ref<any[]>([])
const colorMenus = $ref<any>({}) const colorMenus = $ref<any>({})
@ -103,13 +103,20 @@ watch(inputs, () => {
</template> </template>
<MdiArrowDownDropCircle :style="{ 'font-size': '1.5em', 'color': element.color }" class="mr-2" /> <MdiArrowDownDropCircle :style="{ 'font-size': '1.5em', 'color': element.color }" class="mr-2" />
</a-dropdown> </a-dropdown>
<a-input ref="inputs" v-model:value="element.title" class="caption" /> <a-input ref="inputs" v-model:value="element.title" class="caption" />
<MdiClose class="ml-2" :style="{ color: 'red' }" @click="removeOption(index)" /> <MdiClose class="ml-2" :style="{ color: 'red' }" @click="removeOption(index)" />
</div> </div>
</template> </template>
<template #footer> <template #footer>
<div v-if="validateInfos?.['colOptions.options']?.help?.[0]?.[0]" class="text-error text-[10px] my-2">
{{ validateInfos['colOptions.options'].help[0][0] }}
</div>
<a-button type="dashed" class="w-full caption mt-2" @click="addNewOption()"> <a-button type="dashed" class="w-full caption mt-2" @click="addNewOption()">
<div class="flex items-center"><MdiPlusIcon /><span class="flex-auto">Add option</span></div> <div class="flex items-center">
<MdiPlusIcon />
<span class="flex-auto">Add option</span>
</div>
</a-button> </a-button>
</template> </template>
</Draggable> </Draggable>

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

@ -1,5 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Modal, message } from 'ant-design-vue' import { Modal, message } from 'ant-design-vue'
import type { LinkToAnotherRecordType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { ColumnInj, IsLockedInj, MetaInj, extractSdkResponseErrorMsg, inject, useI18n, useMetas, useNuxtApp } from '#imports' import { ColumnInj, IsLockedInj, MetaInj, extractSdkResponseErrorMsg, inject, useI18n, useMetas, useNuxtApp } from '#imports'
const { virtual = false } = defineProps<{ virtual?: boolean }>() const { virtual = false } = defineProps<{ virtual?: boolean }>()
@ -29,6 +31,12 @@ const deleteColumn = () =>
await $api.dbTableColumn.delete(column?.value?.id as string) await $api.dbTableColumn.delete(column?.value?.id as string)
await getMeta(meta?.value?.id as string, true) await getMeta(meta?.value?.id as string, true)
/** force-reload related table meta if deleted column is a LTAR and not linked to same table */
if (column?.value?.uidt === UITypes.LinkToAnotherRecord && column.value?.colOptions) {
await getMeta((column.value?.colOptions as LinkToAnotherRecordType).fk_related_model_id!, true)
}
$e('a:column:delete') $e('a:column:delete')
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))

6
packages/nc-gui/components/smartsheet-toolbar/LockType.vue

@ -13,17 +13,17 @@ const types = {
[LockType.Personal]: { [LockType.Personal]: {
title: 'title.personalView', title: 'title.personalView',
icon: MdiAccountIcon, icon: MdiAccountIcon,
subtitle: 'msg.info.collabView', subtitle: 'msg.info.personalView',
}, },
[LockType.Collaborative]: { [LockType.Collaborative]: {
title: 'title.collabView', title: 'title.collabView',
icon: MdiAccountGroupIcon, icon: MdiAccountGroupIcon,
subtitle: 'msg.info.lockedView', subtitle: 'msg.info.collabView',
}, },
[LockType.Locked]: { [LockType.Locked]: {
title: 'title.lockedView', title: 'title.lockedView',
icon: MdiLockOutlineIcon, icon: MdiLockOutlineIcon,
subtitle: 'msg.info.personalView', subtitle: 'msg.info.lockedView',
}, },
} }

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

@ -41,7 +41,7 @@ watch(
</script> </script>
<template> <template>
<a-dropdown offset-y class="" :trigger="['click']"> <a-dropdown offset-y class="" :trigger="['click']" overlay-class-name="sort-menu-overlay">
<div :class="{ 'nc-badge nc-active-btn': sorts?.length }"> <div :class="{ 'nc-badge nc-active-btn': sorts?.length }">
<a-button v-t="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked" <a-button v-t="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked"
><div class="flex items-center gap-1"> ><div class="flex items-center gap-1">
@ -73,6 +73,7 @@ watch(
:label="$t('labels.operation')" :label="$t('labels.operation')"
@click.stop @click.stop
@select="saveOrUpdate(sort, i)" @select="saveOrUpdate(sort, i)"
dropdown-class-name="sort-dir-dropdown"
> >
<a-select-option <a-select-option
v-for="(option, j) in getSortDirectionOptions(columnByID[sort.fk_column_id]?.uidt)" v-for="(option, j) in getSortDirectionOptions(columnByID[sort.fk_column_id]?.uidt)"

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

@ -15,6 +15,7 @@ import {
toRef, toRef,
useColumn, useColumn,
useDebounceFn, useDebounceFn,
useSmartsheetRowStoreOrThrow,
useVModel, useVModel,
} from '#imports' } from '#imports'
import { NavigateDir } from '~/lib' import { NavigateDir } from '~/lib'
@ -57,10 +58,10 @@ const isPublic = inject(IsPublicInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
let changed = $ref(false) const { currentRow } = useSmartsheetRowStoreOrThrow()
const syncValue = useDebounceFn(function () { const syncValue = useDebounceFn(function () {
changed = false currentRow.value.rowMeta.changed = false
emit('save') emit('save')
}, 1000) }, 1000)
@ -89,13 +90,13 @@ const vModel = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => { set: (val) => {
if (val !== props.modelValue) { if (val !== props.modelValue) {
changed = true currentRow.value.rowMeta.changed = true
emit('update:modelValue', val) emit('update:modelValue', val)
if (isAutoSaved) { if (isAutoSaved) {
syncValue() syncValue()
} else if (!isManualSaved) { } else if (!isManualSaved) {
emit('save') emit('save')
changed = true currentRow.value.rowMeta.changed = true
} }
} }
}, },
@ -129,9 +130,9 @@ const {
const syncAndNavigate = (dir: NavigateDir) => { const syncAndNavigate = (dir: NavigateDir) => {
if (isJSON.value) return if (isJSON.value) return
if (changed) { if (currentRow.value.rowMeta.changed) {
emit('save') emit('save')
changed = false currentRow.value.rowMeta.changed = false
} }
emit('navigate', dir) emit('navigate', dir)
} }

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

@ -91,7 +91,7 @@ const {
deleteRow, deleteRow,
deleteSelectedRows, deleteSelectedRows,
selectedAllRecords, selectedAllRecords,
removeLastEmptyRow, removeRowIfNew,
} = useViewData(meta, view as any, xWhere) } = useViewData(meta, view as any, xWhere)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view as any) const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view as any)
@ -113,15 +113,18 @@ reloadViewDataHook?.on(async () => {
await loadData() await loadData()
}) })
const expandForm = (row: Row, state?: Record<string, any>) => { const skipRowRemovalOnCancel = ref(false)
const expandForm = (row: Row, state?: Record<string, any>, fromToolbar = false) => {
expandedFormRow.value = row expandedFormRow.value = row
expandedFormRowState.value = state expandedFormRowState.value = state
expandedFormDlg.value = true expandedFormDlg.value = true
skipRowRemovalOnCancel.value = !fromToolbar
} }
openNewRecordFormHook?.on(async () => { openNewRecordFormHook?.on(async () => {
const newRow = await addEmptyRow() const newRow = await addEmptyRow()
expandForm(newRow) expandForm(newRow, undefined, true)
}) })
const selectCell = (row: number, col: number) => { const selectCell = (row: number, col: number) => {
@ -324,6 +327,34 @@ const showContextMenu = (e: MouseEvent, target?: { row: number; col: number }) =
contextMenuTarget.value = target contextMenuTarget.value = target
} }
} }
const rowRefs = $ref<any[]>()
/** save/update records before unmounting the component */
onBeforeUnmount(async () => {
let index = -1
for (const currentRow of data.value) {
index++
/** if new record save row and save the LTAR cells */
if (currentRow.rowMeta.new) {
const syncLTARRefs = rowRefs[index]!.syncLTARRefs
const savedRow = await updateOrSaveRow(currentRow, '')
await syncLTARRefs(savedRow)
currentRow.rowMeta.changed = false
continue
}
/** if existing row check updated cell and invoke update method */
if (currentRow.rowMeta.changed) {
currentRow.rowMeta.changed = false
for (const field of meta?.value.columns ?? []) {
if (isVirtualCol(field)) continue
if (currentRow.row[field.title!] !== currentRow.oldRow[field.title!]) {
await updateOrSaveRow(currentRow, field.title!)
}
}
}
}
})
</script> </script>
<template> <template>
@ -399,7 +430,7 @@ const showContextMenu = (e: MouseEvent, target?: { row: number; col: number }) =
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<SmartsheetRow v-for="(row, rowIndex) of data" :key="rowIndex" :row="row"> <SmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }"> <template #default="{ state }">
<tr class="nc-grid-row"> <tr class="nc-grid-row">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1"> <td key="row-index" class="caption nc-grid-cell pl-5 pr-1">
@ -489,10 +520,6 @@ const showContextMenu = (e: MouseEvent, target?: { row: number; col: number }) =
</template> </template>
</SmartsheetRow> </SmartsheetRow>
<!--
TODO: add relationType !== 'bt' ?
v1: <tr v-if="!isView && !isLocked && !isPublicView && isEditable && relationType !== 'bt'">
-->
<tr v-if="!isView && !isLocked && isUIAllowed('xcDatatableEditable') && !isSqlView"> <tr v-if="!isView && !isLocked && isUIAllowed('xcDatatableEditable') && !isSqlView">
<td <td
v-t="['c:row:add:grid-bottom']" v-t="['c:row:add:grid-bottom']"
@ -552,7 +579,11 @@ const showContextMenu = (e: MouseEvent, target?: { row: number; col: number }) =
:row="expandedFormRow" :row="expandedFormRow"
:state="expandedFormRowState" :state="expandedFormRowState"
:meta="meta" :meta="meta"
@cancel="removeLastEmptyRow" @update:model-value="
() => {
if (!skipRowRemovalOnCancel) removeRowIfNew(expandedFormRow)
}
"
/> />
</div> </div>
</template> </template>

18
packages/nc-gui/components/smartsheet/Row.vue

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Row } from '~/composables' import type { Row } from '~/composables'
import { useProvideSmartsheetRowStore, useSmartsheetStoreOrThrow } from '#imports' import { ReloadRowDataHookInj, useProvideSmartsheetRowStore, useSmartsheetStoreOrThrow } from '#imports'
interface Props { interface Props {
row: Row row: Row
@ -21,6 +21,22 @@ watch(isNew, async (nextVal, prevVal) => {
currentRow.value.oldRow = { ...currentRow.value.row, ...state.value } currentRow.value.oldRow = { ...currentRow.value.row, ...state.value }
} }
}) })
const reloadViewDataTrigger = inject(ReloadViewDataHookInj)!
// override reload trigger and use it to reload row
const reloadHook = createEventHook()
reloadHook.on(() => {
if (isNew.value) return
reloadViewDataTrigger?.trigger()
})
provide(ReloadRowDataHookInj, reloadHook)
defineExpose({
syncLTARRefs,
})
</script> </script>
<template> <template>

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

@ -1,20 +1,29 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useExpandedFormStoreOrThrow, useSmartsheetRowStoreOrThrow, useSmartsheetStoreOrThrow, useUIPermission } from '#imports' import {
ReloadRowDataHookInj,
useExpandedFormStoreOrThrow,
useSmartsheetRowStoreOrThrow,
useSmartsheetStoreOrThrow,
useUIPermission,
} from '#imports'
const emit = defineEmits(['cancel']) const emit = defineEmits(['cancel'])
const { meta, isSqlView } = useSmartsheetStoreOrThrow() const { meta, isSqlView } = useSmartsheetStoreOrThrow()
const { commentsDrawer, primaryValue, save: _save } = useExpandedFormStoreOrThrow() const { commentsDrawer, primaryValue, save: _save, loadRow } = useExpandedFormStoreOrThrow()
const { isNew, syncLTARRefs } = useSmartsheetRowStoreOrThrow() const { isNew, syncLTARRefs } = useSmartsheetRowStoreOrThrow()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const save = async () => { const save = async () => {
if (isNew.value) { if (isNew.value) {
const data = await _save() const data = await _save()
await syncLTARRefs(data) await syncLTARRefs(data)
reloadTrigger?.trigger()
} else { } else {
await _save() await _save()
} }
@ -46,7 +55,7 @@ const iconColor = '#1890ff'
<template #title> <template #title>
<div class="text-center w-full">{{ $t('general.reload') }}</div> <div class="text-center w-full">{{ $t('general.reload') }}</div>
</template> </template>
<mdi-reload class="cursor-pointer select-none text-gray-500" /> <mdi-reload class="cursor-pointer select-none text-gray-500" @click="loadRow" />
</a-tooltip> </a-tooltip>
<a-tooltip v-if="!isSqlView" placement="bottom"> <a-tooltip v-if="!isSqlView" placement="bottom">
<!-- Toggle comments draw --> <!-- Toggle comments draw -->

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

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk' import type { TableType, ViewType } from 'nocodb-sdk'
import { isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import Cell from '../Cell.vue' import Cell from '../Cell.vue'
@ -10,14 +10,11 @@ import {
FieldsInj, FieldsInj,
IsFormInj, IsFormInj,
MetaInj, MetaInj,
NOCO, ReloadRowDataHookInj,
computedInject, computedInject,
extractPkFromRow,
provide, provide,
ref, ref,
toRef, toRef,
useNuxtApp,
useProject,
useProvideExpandedFormStore, useProvideExpandedFormStore,
useProvideSmartsheetStore, useProvideSmartsheetStore,
useVModel, useVModel,
@ -53,25 +50,10 @@ const fields = computedInject(FieldsInj, (_fields) => {
provide(MetaInj, meta) provide(MetaInj, meta)
const { commentsDrawer, changedColumns, state: rowState, isNew } = useProvideExpandedFormStore(meta, row) const { commentsDrawer, changedColumns, state: rowState, isNew, loadRow } = useProvideExpandedFormStore(meta, row)
const { $api } = useNuxtApp()
if (props.loadRow) { if (props.loadRow) {
const { project } = useProject() await loadRow()
const { sharedView } = useSharedView() as Record<string, any>
row.value.row = await $api.dbTableRow.read(
NOCO,
(project?.value?.id || sharedView.value.view.project_id) as string,
meta.value.title,
extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]),
)
row.value.oldRow = { ...row.value.row }
row.value.rowMeta = {}
} }
useProvideSmartsheetStore(ref({}) as Ref<ViewType>, meta) useProvideSmartsheetStore(ref({}) as Ref<ViewType>, meta)
@ -98,6 +80,19 @@ const onClose = () => {
if (row.value?.rowMeta?.new) emits('cancel') if (row.value?.rowMeta?.new) emits('cancel')
isExpanded.value = false isExpanded.value = false
} }
const reloadParentRowHook = inject(ReloadRowDataHookInj, createEventHook())
// override reload trigger and use it to reload grid and the form itself
const reloadHook = createEventHook()
reloadHook.on(() => {
reloadParentRowHook?.trigger()
if (isNew.value) return
loadRow()
})
provide(ReloadRowDataHookInj, reloadHook)
</script> </script>
<script lang="ts"> <script lang="ts">

11
packages/nc-gui/components/tabs/Smartsheet.vue

@ -21,6 +21,10 @@ import {
import type { TabItem } from '~/composables' import type { TabItem } from '~/composables'
const { activeTab } = defineProps<{
activeTab: TabItem
}>()
const { metas } = useMetas() const { metas } = useMetas()
const activeView = ref() const activeView = ref()
@ -29,11 +33,8 @@ const el = ref<typeof SmartsheetGrid>()
const fields = ref<ColumnType[]>([]) const fields = ref<ColumnType[]>([])
const tabMeta = inject( provide(TabMetaInj, ref(activeTab))
TabMetaInj, const meta = computed<TableType>(() => metas.value?.[activeTab?.id as string])
computed(() => ({} as TabItem)),
)
const meta = computed<TableType>(() => metas.value?.[tabMeta?.value?.id as string])
const reloadEventHook = createEventHook<void>() const reloadEventHook = createEventHook<void>()
const openNewRecordFormHook = createEventHook<void>() const openNewRecordFormHook = createEventHook<void>()

20
packages/nc-gui/components/tabs/auth/ApiTokenManagement.vue

@ -106,17 +106,29 @@ onMounted(() => {
</template> </template>
</a-button> </a-button>
<!-- Generate Token --> <!-- Generate Token -->
<div class="flex flex-row justify-center w-full -mt-1"> <div class="flex flex-row justify-center w-full -mt-1 mb-3">
<a-typography-title :level="5">{{ $t('title.generateToken') }}</a-typography-title> <a-typography-title :level="5">{{ $t('title.generateToken') }}</a-typography-title>
</div> </div>
<!-- Description --> <!-- Description -->
<div class="flex flex-col mt-3 justify-center space-y-6"> <a-form
ref="form"
:model="selectedTokenData"
name="basic"
layout="vertical"
class="flex flex-col justify-center space-y-6"
no-style
autocomplete="off"
@finish="generateToken"
>
<a-input v-model:value="selectedTokenData.description" :placeholder="$t('labels.description')" /> <a-input v-model:value="selectedTokenData.description" :placeholder="$t('labels.description')" />
<!-- Generate --> <!-- Generate -->
<div class="flex flex-row justify-center"> <div class="flex flex-row justify-center">
<a-button type="primary" @click="generateToken"> {{ $t('general.generate') }} </a-button> <a-button type="primary" html-type="submit">
{{ $t('general.generate') }}
</a-button>
</div> </div>
</div> </a-form>
</div> </div>
</a-modal> </a-modal>
<a-modal v-model:visible="showDeleteTokenModal" :closable="false" width="28rem" centered :footer="null"> <a-modal v-model:visible="showDeleteTokenModal" :closable="false" width="28rem" centered :footer="null">

6
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -6,7 +6,7 @@ import {
CellValueInj, CellValueInj,
ColumnInj, ColumnInj,
ReadonlyInj, ReadonlyInj,
ReloadViewDataHookInj, ReloadRowDataHookInj,
RowInj, RowInj,
defineAsyncComponent, defineAsyncComponent,
inject, inject,
@ -24,7 +24,7 @@ const ListItems = defineAsyncComponent(() => import('./components/ListItems.vue'
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)! const reloadRowTrigger = inject(ReloadRowDataHookInj, createEventHook())
const cellValue = inject(CellValueInj, ref<any>(null)) const cellValue = inject(CellValueInj, ref<any>(null))
@ -48,7 +48,7 @@ const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvid
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
isNew, isNew,
reloadTrigger.trigger, reloadRowTrigger.trigger,
) )
await loadRelatedTableMeta() await loadRelatedTableMeta()

7
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -7,7 +7,7 @@ import {
IsFormInj, IsFormInj,
IsLockedInj, IsLockedInj,
ReadonlyInj, ReadonlyInj,
ReloadViewDataHookInj, ReloadRowDataHookInj,
RowInj, RowInj,
computed, computed,
defineAsyncComponent, defineAsyncComponent,
@ -30,7 +30,7 @@ const cellValue = inject(CellValueInj)!
const row = inject(RowInj)! const row = inject(RowInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)! const reloadRowTrigger = inject(ReloadRowDataHookInj, createEventHook())
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj)
@ -50,7 +50,7 @@ const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvid
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
isNew, isNew,
reloadTrigger.trigger, reloadRowTrigger.trigger,
) )
await loadRelatedTableMeta() await loadRelatedTableMeta()
@ -112,6 +112,7 @@ const unlinkRef = async (rec: Record<string, any>) => {
<ListChildItems <ListChildItems
v-model="childListDlg" v-model="childListDlg"
:cell-value="localCellValue"
@attach-record=" @attach-record="
() => { () => {
childListDlg = false childListDlg = false

7
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -7,7 +7,7 @@ import {
IsFormInj, IsFormInj,
IsLockedInj, IsLockedInj,
ReadonlyInj, ReadonlyInj,
ReloadViewDataHookInj, ReloadRowDataHookInj,
RowInj, RowInj,
computed, computed,
inject, inject,
@ -29,7 +29,7 @@ const row = inject(RowInj)!
const cellValue = inject(CellValueInj)! const cellValue = inject(CellValueInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)! const reloadRowTrigger = inject(ReloadRowDataHookInj, createEventHook())
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj)
@ -48,7 +48,7 @@ const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvid
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
isNew, isNew,
reloadTrigger.trigger, reloadRowTrigger.trigger,
) )
await loadRelatedTableMeta() await loadRelatedTableMeta()
@ -112,6 +112,7 @@ const unlinkRef = async (rec: Record<string, any>) => {
<ListChildItems <ListChildItems
v-model="childListDlg" v-model="childListDlg"
:cell-value="localCellValue"
@attach-record=" @attach-record="
() => { () => {
childListDlg = false childListDlg = false

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

@ -13,7 +13,7 @@ import {
} from '#imports' } from '#imports'
import { IsPublicInj } from '~/context' import { IsPublicInj } from '~/context'
const props = defineProps<{ modelValue?: boolean }>() const props = defineProps<{ modelValue?: boolean; cellValue: any }>()
const emit = defineEmits(['update:modelValue', 'attachRecord']) const emit = defineEmits(['update:modelValue', 'attachRecord'])
@ -77,6 +77,13 @@ const container = computed(() =>
const expandedFormDlg = ref(false) const expandedFormDlg = ref(false)
const expandedFormRow = ref() const expandedFormRow = ref()
watch(
() => props.cellValue,
() => {
if (!isNew.value) loadChildrenList()
},
)
</script> </script>
<template> <template>

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

@ -64,6 +64,7 @@ const expandedFormDlg = ref(false)
/** populate initial state for a new row which is parent/child of current record */ /** populate initial state for a new row which is parent/child of current record */
const newRowState = computed(() => { const newRowState = computed(() => {
if (isNew.value) return {}
const colOpt = (column?.value as ColumnType)?.colOptions as LinkToAnotherRecordType const colOpt = (column?.value as ColumnType)?.colOptions as LinkToAnotherRecordType
const colInRelatedTable: ColumnType | undefined = relatedTableMeta?.value?.columns?.find((col) => { const colInRelatedTable: ColumnType | undefined = relatedTableMeta?.value?.columns?.find((col) => {
if (col.uidt !== UITypes.LinkToAnotherRecord) return false if (col.uidt !== UITypes.LinkToAnotherRecord) return false
@ -94,6 +95,12 @@ const newRowState = computed(() => {
} }
} }
}) })
// if it's an existing record close the list
// after new record creation since it's already linking while creating
watch(expandedFormDlg, (nexVal) => {
if (!nexVal && !isNew.value) vModel.value = false
})
</script> </script>
<template> <template>
@ -109,9 +116,9 @@ const newRowState = computed(() => {
<div class="flex-1" /> <div class="flex-1" />
<MdiReload class="cursor-pointer text-gray-500 nc-reload" @click="loadChildrenExcludedList" /> <MdiReload class="cursor-pointer text-gray-500 nc-reload" @click="loadChildrenExcludedList" />
<!-- Add new record --> <!-- Add new record -->
<a-button v-if="!isPublic" type="primary" size="small" @click="expandedFormDlg = true">{{ <a-button v-if="!isPublic" type="primary" size="small" @click="expandedFormDlg = true">
$t('activity.addNewRecord') {{ $t('activity.addNewRecord') }}
}}</a-button> </a-button>
</div> </div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows"> <template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0 scrollbar-thin-dull px-12"> <div class="flex-1 overflow-auto min-h-0 scrollbar-thin-dull px-12">
@ -121,10 +128,10 @@ const newRowState = computed(() => {
class="!my-4 cursor-pointer hover:(!bg-gray-200/50 shadow-md) group" class="!my-4 cursor-pointer hover:(!bg-gray-200/50 shadow-md) group"
@click="linkRow(refRow)" @click="linkRow(refRow)"
> >
{{ refRow[relatedTablePrimaryValueProp] {{ refRow[relatedTablePrimaryValueProp] }}
}}<span class="hidden group-hover:(inline) text-gray-400 text-[11px] ml-1" <span class="hidden group-hover:(inline) text-gray-400 text-[11px] ml-1">
>({{ $t('labels.primaryKey') }} : {{ getRelatedTableRowId(refRow) }})</span ({{ $t('labels.primaryKey') }} : {{ getRelatedTableRowId(refRow) }})
> </span>
</a-card> </a-card>
</div> </div>
<div class="flex justify-center mt-6"> <div class="flex justify-center mt-6">

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

@ -206,6 +206,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
$e('a:column:add', { datatype: formState.value.uidt }) $e('a:column:add', { datatype: formState.value.uidt })
} }
onSuccess?.() onSuccess?.()
return true
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }

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

@ -30,6 +30,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const changedColumns = ref(new Set<string>()) const changedColumns = ref(new Set<string>())
const { project } = useProject() const { project } = useProject()
const rowStore = useProvideSmartsheetRowStore(meta, row) const rowStore = useProvideSmartsheetRowStore(meta, row)
const { sharedView } = useSharedView() as Record<string, any>
const activeView = inject(ActiveViewInj) const activeView = inject(ActiveViewInj)
const { loadKanbanData } = useKanbanViewData(meta, activeView as any) const { loadKanbanData } = useKanbanViewData(meta, activeView as any)
@ -179,6 +180,20 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
return data return data
} }
const loadRow = async () => {
const record = await $api.dbTableRow.read(
NOCO,
(project?.value?.id || sharedView.value.view.project_id) as string,
meta.value.title,
extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]),
)
Object.assign(row.value, {
row: record,
oldRow: { ...record },
rowMeta: {},
})
}
return { return {
...rowStore, ...rowStore,
commentsOnly, commentsOnly,
@ -194,6 +209,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
primaryValue, primaryValue,
save, save,
changedColumns, changedColumns,
loadRow,
} }
}, 'expanded-form-store') }, 'expanded-form-store')

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

@ -193,7 +193,22 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
onOk: async () => { onOk: async () => {
const id = getRelatedTableRowId(row) const id = getRelatedTableRowId(row)
try { try {
$api.dbTableRow.delete(NOCO, projectId, relatedTableMeta.value.id as string, id as string) const res: { message?: string[] } = await $api.dbTableRow.delete(
NOCO,
projectId,
relatedTableMeta.value.id as string,
id as string,
)
if (res.message) {
message.info(
`Row delete failed: ${`Unable to delete row with ID ${id} because of the following:
\n${res.message.join('\n')}.\n
Clear the data first & try again`})}`,
)
return false
}
reloadData?.() reloadData?.()
/** reload child list if not a new row */ /** reload child list if not a new row */

18
packages/nc-gui/composables/useSmartsheetRowStore.ts

@ -29,6 +29,8 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
const { metas } = useMetas() const { metas } = useMetas()
const currentRow = ref(row)
// state // state
const state = ref<Record<string, Record<string, any> | Record<string, any>[] | null>>({}) const state = ref<Record<string, Record<string, any> | Record<string, any>[] | null>>({})
@ -109,6 +111,20 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
} }
} }
const loadRow = async () => {
const record = await $api.dbTableRow.read(
NOCO,
project?.value?.id as string,
meta.value.title,
extractPkFromRow(ref(row).value?.row, meta.value.columns as ColumnType[]),
)
Object.assign(ref(row).value, {
row: record,
oldRow: { ...record },
rowMeta: {},
})
}
return { return {
row, row,
state, state,
@ -117,6 +133,8 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
addLTARRef, addLTARRef,
removeLTARRef, removeLTARRef,
syncLTARRefs, syncLTARRefs,
loadRow,
currentRow,
} }
}, 'smartsheet-row-store') }, 'smartsheet-row-store')

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

@ -68,7 +68,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
cancelText: t('general.no'), cancelText: t('general.no'),
async onOk() { async onOk() {
try { try {
const meta = (await getMeta(table.id as string)) as TableType const meta = (await getMeta(table.id as string, true)) as TableType
const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord) const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord)
if (relationColumns?.length) { if (relationColumns?.length) {

27
packages/nc-gui/composables/useViewData.ts

@ -22,6 +22,7 @@ export interface Row {
new?: boolean new?: boolean
selected?: boolean selected?: boolean
commentCount?: number commentCount?: number
changed?: boolean
} }
} }
@ -113,7 +114,7 @@ export function useViewData(
}) })
const queryParams = computed(() => ({ const queryParams = computed(() => ({
offset: (paginationData.value?.page ?? 0) - 1, offset: ((paginationData.value?.page ?? 0) - 1) * (paginationData.value?.pageSize ?? 25),
limit: paginationData.value?.pageSize ?? 25, limit: paginationData.value?.pageSize ?? 25,
where: where?.value ?? '', where: where?.value ?? '',
})) }))
@ -200,6 +201,7 @@ export function useViewData(
if ((!project?.value?.id || !meta?.value?.id || !viewMeta?.value?.id) && !isPublic.value) return if ((!project?.value?.id || !meta?.value?.id || !viewMeta?.value?.id) && !isPublic.value) return
const response = !isPublic.value const response = !isPublic.value
? await api.dbViewRow.list('noco', project.value.id!, meta!.value.id!, viewMeta!.value.id, { ? await api.dbViewRow.list('noco', project.value.id!, meta!.value.id!, viewMeta!.value.id, {
...queryParams.value,
...params, ...params,
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
@ -292,11 +294,11 @@ export function useViewData(
} }
} }
async function updateOrSaveRow(row: Row, property: string) { async function updateOrSaveRow(row: Row, property?: string) {
if (row.rowMeta.new) { if (row.rowMeta.new) {
await insertRow(row.row, formattedData.value.indexOf(row)) return await insertRow(row.row, formattedData.value.indexOf(row))
} else { } else {
await updateRowProperty(row, property) await updateRowProperty(row, property!)
} }
} }
@ -321,11 +323,9 @@ export function useViewData(
if (res.message) { if (res.message) {
message.info( message.info(
`Row delete failed: ${h('div', { `Row delete failed: ${`Unable to delete row with ID ${id} because of the following:
innerHTML: `<div style="padding:10px 4px">Unable to delete row with ID ${id} because of the following: \n${res.message.join('\n')}.\n
<br><br>${res.message.join('<br>')}<br><br> Clear the data first & try again`})}`,
Clear the data first & try again</div>`,
})}`,
) )
return false return false
} }
@ -425,6 +425,14 @@ export function useViewData(
} }
} }
const removeRowIfNew = (row: Row) => {
const index = formattedData.value.indexOf(row)
if (index > -1 && row.rowMeta.new) {
formattedData.value.splice(index, 1)
}
}
return { return {
error, error,
isLoading, isLoading,
@ -452,6 +460,7 @@ export function useViewData(
aggCommentCount, aggCommentCount,
loadAggCommentsCount, loadAggCommentsCount,
removeLastEmptyRow, removeLastEmptyRow,
removeRowIfNew,
kanbanData, kanbanData,
loadKanbanData, loadKanbanData,
} }

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

@ -23,6 +23,7 @@ export const CellValueInj: InjectionKey<Ref<any>> = Symbol('cell-value-injection
export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection') export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection')
export const ReadonlyInj: InjectionKey<boolean> = Symbol('readonly-injection') export const ReadonlyInj: InjectionKey<boolean> = Symbol('readonly-injection')
export const ReloadViewDataHookInj: InjectionKey<EventHook<void>> = Symbol('reload-view-data-injection') export const ReloadViewDataHookInj: InjectionKey<EventHook<void>> = Symbol('reload-view-data-injection')
export const ReloadRowDataHookInj: InjectionKey<EventHook<void>> = Symbol('reload-row-data-injection')
export const OpenNewRecordFormHookInj: InjectionKey<EventHook<void>> = Symbol('open-new-record-form-injection') export const OpenNewRecordFormHookInj: InjectionKey<EventHook<void>> = Symbol('open-new-record-form-injection')
export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection') export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection')
export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection') export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection')

2
packages/nc-gui/package-lock.json generated

@ -79,7 +79,7 @@
} }
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.96.2", "version": "0.96.3",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

10
packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue

@ -1,8 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TabItem } from '~/composables'
import { TabMetaInj } from '#imports'
const { getMeta } = useMetas() const { getMeta } = useMetas()
const route = useRoute() const route = useRoute()
const loading = ref(true) const loading = ref(true)
const activeTab = inject(
TabMetaInj,
computed(() => ({} as TabItem)),
)
getMeta(route.params.title as string, true).finally(() => { getMeta(route.params.title as string, true).finally(() => {
loading.value = false loading.value = false
}) })
@ -12,7 +20,7 @@ getMeta(route.params.title as string, true).finally(() => {
<div v-if="loading" class="flex items-center justify-center h-full w-full"> <div v-if="loading" class="flex items-center justify-center h-full w-full">
<a-spin size="large" /> <a-spin size="large" />
</div> </div>
<TabsSmartsheet v-else /> <TabsSmartsheet v-else :key="route.params.title" :active-tab="activeTab" />
</template> </template>
<style scoped></style> <style scoped></style>

2
packages/nc-gui/pages/signup/[[token]].vue

@ -113,7 +113,7 @@ function resetError() {
{{ $route.query.redirect_to === '/pricing' ? '& BUY' : '' }} {{ $route.query.redirect_to === '/pricing' ? '& BUY' : '' }}
</h1> </h1>
<h2 v-if="appInfo.firstUser" class="prose !text-primary font-semibold self-center my-4"> <h2 v-if="appInfo.firstUser" class="prose !text-primary font-semibold self-center">
{{ $t('msg.info.signUp.superAdmin') }} {{ $t('msg.info.signUp.superAdmin') }}
</h2> </h2>

40
packages/nc-gui/plugins/initializeFeedbackForm.ts

@ -1,4 +1,3 @@
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { defineNuxtPlugin } from '#app' import { defineNuxtPlugin } from '#app'
@ -8,32 +7,33 @@ const handleFeedbackForm = async () => {
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const fetchFeedbackForm = async (now: Dayjs) => {
try {
const { data: feedbackForm } = await $api.instance.get('/api/v1/feedback_form')
const isFetchedFormDuplicate = currentFeedbackForm.url === feedbackForm.url
currentFeedbackForm = {
url: feedbackForm.url,
lastFormPollDate: now.toISOString(),
createdAt: feedbackForm.created_at,
isHidden: isFetchedFormDuplicate ? currentFeedbackForm.isHidden : false,
}
} catch (e) {
console.error(e)
}
}
const isFirstTimePolling = !currentFeedbackForm.lastFormPollDate const isFirstTimePolling = !currentFeedbackForm.lastFormPollDate
const now = dayjs() const now = dayjs()
const lastFormPolledDate = dayjs(currentFeedbackForm.lastFormPollDate) const lastFormPolledDate = dayjs(currentFeedbackForm.lastFormPollDate)
if (isFirstTimePolling || dayjs.duration(now.diff(lastFormPolledDate)).days() > 0) { if (isFirstTimePolling || dayjs.duration(now.diff(lastFormPolledDate)).days() > 0) {
await fetchFeedbackForm(now) $api.instance
.get('/api/v1/feedback_form')
.then((response) => {
try {
const { data: feedbackForm } = response
if (!feedbackForm.error) {
const isFetchedFormDuplicate = currentFeedbackForm.url === feedbackForm.url
currentFeedbackForm = {
url: feedbackForm.url,
lastFormPollDate: now.toISOString(),
createdAt: feedbackForm.created_at,
isHidden: isFetchedFormDuplicate ? currentFeedbackForm.isHidden : false,
}
}
} catch (e) {}
})
.catch(() => {})
} }
} }
export default defineNuxtPlugin(async () => { export default defineNuxtPlugin(() => {
await handleFeedbackForm() handleFeedbackForm()
}) })

2
packages/nc-lib-gui/package.json

@ -1,6 +1,6 @@
{ {
"name": "nc-lib-gui", "name": "nc-lib-gui",
"version": "0.96.2", "version": "0.96.4",
"description": "NocoDB GUI", "description": "NocoDB GUI",
"author": { "author": {
"name": "NocoDB", "name": "NocoDB",

4
packages/nocodb-sdk/package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "nocodb-sdk", "name": "nocodb-sdk",
"version": "0.96.2", "version": "0.96.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nocodb-sdk", "name": "nocodb-sdk",
"version": "0.96.2", "version": "0.96.4",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

2
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{ {
"name": "nocodb-sdk", "name": "nocodb-sdk",
"version": "0.96.2", "version": "0.96.4",
"description": "NocoDB SDK", "description": "NocoDB SDK",
"main": "build/main/index.js", "main": "build/main/index.js",
"typings": "build/main/index.d.ts", "typings": "build/main/index.d.ts",

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

@ -2456,7 +2456,7 @@ export class Api<
* @name Delete * @name Delete
* @summary Table row delete * @summary Table row delete
* @request DELETE:/api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId} * @request DELETE:/api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId}
* @response `200` `void` OK * @response `200` `any` OK
*/ */
delete: ( delete: (
orgs: string, orgs: string,
@ -2465,9 +2465,10 @@ export class Api<
rowId: string, rowId: string,
params: RequestParams = {} params: RequestParams = {}
) => ) =>
this.request<void, any>({ this.request<any, any>({
path: `/api/v1/db/data/${orgs}/${projectName}/${tableName}/${rowId}`, path: `/api/v1/db/data/${orgs}/${projectName}/${tableName}/${rowId}`,
method: 'DELETE', method: 'DELETE',
format: 'json',
...params, ...params,
}), }),

20
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "nocodb", "name": "nocodb",
"version": "0.96.2", "version": "0.96.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nocodb", "name": "nocodb",
"version": "0.96.2", "version": "0.96.4",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@google-cloud/storage": "^5.7.2", "@google-cloud/storage": "^5.7.2",
@ -71,7 +71,7 @@
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-common": "0.0.6", "nc-common": "0.0.6",
"nc-help": "0.2.68", "nc-help": "0.2.68",
"nc-lib-gui": "0.96.2", "nc-lib-gui": "0.96.4",
"nc-plugin": "0.1.2", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",
@ -173,7 +173,7 @@
} }
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.96.2", "version": "0.96.3",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -15340,9 +15340,9 @@
} }
}, },
"node_modules/nc-lib-gui": { "node_modules/nc-lib-gui": {
"version": "0.96.2", "version": "0.96.4",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.96.2.tgz", "resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.96.4.tgz",
"integrity": "sha512-b5UB9Znwz7FhnBn3hPKd/ENIHsTDAZ1vDvEJ01hLsKpE1idlU4AXAvyiFTQ/OW0vUSWBbN226Q3yqM6ORa3u2A==", "integrity": "sha512-kkEvZGUAdRI8juA8VZkDj0ojeQbedjIC/eGZoiZVMDsWX6dcthxubQp80kYAzhk5DXnH8eCoIp0/Bo8S3gcU5g==",
"dependencies": { "dependencies": {
"axios": "^0.19.2", "axios": "^0.19.2",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
@ -36773,9 +36773,9 @@
} }
}, },
"nc-lib-gui": { "nc-lib-gui": {
"version": "0.96.2", "version": "0.96.4",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.96.2.tgz", "resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.96.4.tgz",
"integrity": "sha512-b5UB9Znwz7FhnBn3hPKd/ENIHsTDAZ1vDvEJ01hLsKpE1idlU4AXAvyiFTQ/OW0vUSWBbN226Q3yqM6ORa3u2A==", "integrity": "sha512-kkEvZGUAdRI8juA8VZkDj0ojeQbedjIC/eGZoiZVMDsWX6dcthxubQp80kYAzhk5DXnH8eCoIp0/Bo8S3gcU5g==",
"requires": { "requires": {
"axios": "^0.19.2", "axios": "^0.19.2",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",

4
packages/nocodb/package.json

@ -1,6 +1,6 @@
{ {
"name": "nocodb", "name": "nocodb",
"version": "0.96.2", "version": "0.96.4",
"description": "NocoDB", "description": "NocoDB",
"main": "dist/bundle.js", "main": "dist/bundle.js",
"repository": "https://github.com/nocodb/nocodb", "repository": "https://github.com/nocodb/nocodb",
@ -157,7 +157,7 @@
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-common": "0.0.6", "nc-common": "0.0.6",
"nc-help": "0.2.68", "nc-help": "0.2.68",
"nc-lib-gui": "0.96.2", "nc-lib-gui": "0.96.4",
"nc-plugin": "0.1.2", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",

6
packages/nocodb/src/lib/meta/api/utilApis.ts

@ -82,12 +82,14 @@ export async function versionInfo(_req: Request, res: Response) {
export async function feedbackFormGet(_req: Request, res: Response) { export async function feedbackFormGet(_req: Request, res: Response) {
axios axios
.get('https://nocodb.com/api/v1/feedback_form') .get('https://nocodb.com/api/v1/feedback_form', {
timeout: 5000,
})
.then((response) => { .then((response) => {
res.json(response.data); res.json(response.data);
}) })
.catch((e) => { .catch((e) => {
res.status(500).json({ error: e.message }); res.json({ error: e.message });
}); });
} }

18
packages/nocodb/src/lib/models/FormViewColumn.ts

@ -76,11 +76,19 @@ export default class FormViewColumn implements FormColumnType {
await NocoCache.set(`${CacheScope.FORM_VIEW_COLUMN}:${fk_column_id}`, id); await NocoCache.set(`${CacheScope.FORM_VIEW_COLUMN}:${fk_column_id}`, id);
await NocoCache.appendToList( // if cache is not present skip pushing it into the list to avoid unexpected behaviour
CacheScope.FORM_VIEW_COLUMN, if (
[column.fk_view_id], (
`${CacheScope.FORM_VIEW_COLUMN}:${id}` await NocoCache.getList(CacheScope.FORM_VIEW_COLUMN, [
); column.fk_view_id,
])
)?.length
)
await NocoCache.appendToList(
CacheScope.FORM_VIEW_COLUMN,
[column.fk_view_id],
`${CacheScope.FORM_VIEW_COLUMN}:${id}`
);
return this.get(id, ncMeta); return this.get(id, ncMeta);
} }

18
packages/nocodb/src/lib/models/GalleryViewColumn.ts

@ -72,11 +72,19 @@ export default class GalleryViewColumn {
id id
); );
await NocoCache.appendToList( // if cache is not present skip pushing it into the list to avoid unexpected behaviour
CacheScope.GALLERY_VIEW_COLUMN, if (
[column.fk_view_id], (
`${CacheScope.GALLERY_VIEW_COLUMN}:${id}` await NocoCache.getList(CacheScope.GALLERY_VIEW_COLUMN, [
); column.fk_view_id,
])
)?.length
)
await NocoCache.appendToList(
CacheScope.GALLERY_VIEW_COLUMN,
[column.fk_view_id],
`${CacheScope.GALLERY_VIEW_COLUMN}:${id}`
);
return this.get(id, ncMeta); return this.get(id, ncMeta);
} }

18
packages/nocodb/src/lib/models/GridViewColumn.ts

@ -93,11 +93,19 @@ export default class GridViewColumn implements GridColumnType {
await NocoCache.set(`${CacheScope.GRID_VIEW_COLUMN}:${fk_column_id}`, id); await NocoCache.set(`${CacheScope.GRID_VIEW_COLUMN}:${fk_column_id}`, id);
await NocoCache.appendToList( // if cache is not present skip pushing it into the list to avoid unexpected behaviour
CacheScope.GRID_VIEW_COLUMN, if (
[column.fk_view_id], (
`${CacheScope.GRID_VIEW_COLUMN}:${id}` await NocoCache.getList(CacheScope.GRID_VIEW_COLUMN, [
); column.fk_view_id,
])
)?.length
)
await NocoCache.appendToList(
CacheScope.GRID_VIEW_COLUMN,
[column.fk_view_id],
`${CacheScope.GRID_VIEW_COLUMN}:${id}`
);
return this.get(id, ncMeta); return this.get(id, ncMeta);
} }

2
packages/nocodb/src/lib/utils/NcConfigFactory.ts

@ -214,6 +214,7 @@ export default class NcConfigFactory implements NcConfig {
...defaultConnectionConfig, ...defaultConnectionConfig,
...parsedQuery, ...parsedQuery,
host: url.hostname, host: url.hostname,
port: +url.port,
}, },
// pool: { // pool: {
// min: 1, // min: 1,
@ -325,6 +326,7 @@ export default class NcConfigFactory implements NcConfig {
...defaultConnectionConfig, ...defaultConnectionConfig,
...parsedQuery, ...parsedQuery,
host: url.hostname, host: url.hostname,
port: +url.port,
}, },
acquireConnectionTimeout: 600000, acquireConnectionTimeout: 600000,
...(url.searchParams.has('search_path') ...(url.searchParams.has('search_path')

13
scripts/cypress/integration/common/3a_filter_sort_fields_operations.js

@ -1,4 +1,5 @@
import { mainPage } from "../../support/page_objects/mainPage"; import { mainPage } from "../../support/page_objects/mainPage";
import { loginPage} from "../../support/page_objects/navigation";
import { isTestSuiteActive } from "../../support/page_objects/projectConstants"; import { isTestSuiteActive } from "../../support/page_objects/projectConstants";
export const genTest = (apiType, dbType) => { export const genTest = (apiType, dbType) => {
@ -6,14 +7,7 @@ export const genTest = (apiType, dbType) => {
describe(`${apiType.toUpperCase()} api - Filter, Fields, Sort`, () => { describe(`${apiType.toUpperCase()} api - Filter, Fields, Sort`, () => {
before(() => { before(() => {
cy.fileHook(); // loginPage.loginAndOpenProject(apiType, dbType);
mainPage.tabReset();
// // kludge: wait for page load to finish
// cy.wait(1000);
// // close team & auth tab
// cy.get('button.ant-tabs-tab-remove').should('exist').click();
// cy.wait(1000);
// open country table // open country table
cy.openTableTab("Country", 25); cy.openTableTab("Country", 25);
@ -25,7 +19,6 @@ export const genTest = (apiType, dbType) => {
describe(`Pagination`, () => { describe(`Pagination`, () => {
beforeEach(() => { beforeEach(() => {
cy.fileHook();
}) })
// check pagination // check pagination
@ -95,6 +88,8 @@ export const genTest = (apiType, dbType) => {
// verify // verify
cy.get(`:nth-child(10) > [data-title="Country"]`).should("not.exist"); cy.get(`:nth-child(10) > [data-title="Country"]`).should("not.exist");
mainPage.getPagination(1).click();
}); });
// create new row using right click menu option // create new row using right click menu option

359
scripts/cypress/integration/common/3f_link_to_another_record.js

@ -0,0 +1,359 @@
import { mainPage } from "../../support/page_objects/mainPage";
import { loginPage } from "../../support/page_objects/navigation";
import { isTestSuiteActive } from "../../support/page_objects/projectConstants";
export const genTest = (apiType, dbType) => {
if (!isTestSuiteActive(apiType, dbType)) return;
describe(`${apiType.toUpperCase()} api - RollUp column`, () => {
function fetchParentFromLabel(label) {
cy.get("label").contains(label).parents(".ant-row").click();
}
// Insert new row
function addRow(index, cellValue) {
cy.get('.nc-grid-add-new-cell').should('exist').click();
mainPage.getCell('Title', index)
.dblclick().then(($el) => {
cy.wrap($el).find('input')
.clear()
.type(`${cellValue}{enter}`);
});
mainPage.getCell('Title', index)
.contains(cellValue).should('exist');
}
// Insert LTAR column
//
function addLtarColumn(columnName, foreignTable, relationType) {
// + icon
cy.get(".nc-grid tr > th:last .nc-icon").click();
// Column name
cy.getActiveMenu().find('input.nc-column-name-input', { timeout: 3000 })
.should('exist')
.clear()
.type(columnName);
// Column type
cy.get(".nc-column-type-input").last()
.click()
.type("Link");
cy.getActiveSelection()
.find('.ant-select-item-option')
.contains("LinkToAnotherRecord").click();
// relation type (hm/ mm)
cy.get('.nc-ltar-relation-type')
.find('.ant-radio')
.eq(relationType==='hm'?0:1)
.click();
// Foreign table
fetchParentFromLabel("Child table");
cy.get(".nc-ltar-child-table")
.last()
.click()
.type(foreignTable);
cy.getActiveSelection()
.find('.ant-select-item-option')
.contains(foreignTable)
.click();
// Save
cy.get(".ant-btn-primary")
.contains("Save")
.should('exist')
.click();
// Toast
cy.toastWait(`Column created`);
// Verify
cy.get(`th[data-title="${columnName}"]`)
.should("exist");
};
// Content verification for LTAR cell
// Validates only 1st chip contents
//
function verifyLtarCell(columnName, index, cellValue) {
cy.get(`:nth-child(${index}) > [data-title="${columnName}"]`)
.find('.chip')
.eq(0)
.contains(cellValue)
.should("exist");
}
// Unlink LTAR cell
//
function ltarUnlink(columnName, index) {
// Click on cell to enable unlink icon
cy.get(`:nth-child(${index}) > [data-title="${columnName}"]`).last()
.click()
// Click on unlink icon
cy.get(`:nth-child(${index}) > [data-title="${columnName}"]`).last()
.find('.unlink-icon')
.should("exist")
.click();
// Glitch; hence wait
cy.wait(1000);
}
before(() => {
// required for standalone test
// loginPage.loginAndOpenProject(apiType, dbType);
cy.createTable("Sheet1");
cy.createTable("Sheet2");
cy.saveLocalStorage();
});
beforeEach(() => {
cy.restoreLocalStorage();
});
after(() => {
// Cleanup
//
cy.openTableTab("Sheet1", 0);
mainPage.deleteColumn("Link1-2hm");
mainPage.deleteColumn("Link1-2mm");
mainPage.deleteColumn("Sheet2");
cy.deleteTable("Sheet1");
cy.deleteTable("Sheet2");
});
///////////////////////////////////////////////////
// Test case
it("Create Link columns", () => {
cy.openTableTab("Sheet1", 0);
addRow(1, "1a");
addRow(2, "1b");
addRow(3, "1c");
addLtarColumn("Link1-2hm", "Sheet2", "hm");
addLtarColumn("Link1-2mm", "Sheet2", "mm");
cy.closeTableTab("Sheet1");
cy.openTableTab("Sheet2", 0);
addLtarColumn("Link2-1hm", "Sheet1", "hm");
cy.closeTableTab("Sheet2");
// Sheet2 now has all 3 column categories : HM, BT, MM
//
});
// Expand form [Add new row]
//
it("Add HM, BT, MM Link, Expand form", () => {
cy.openTableTab("Sheet2", 0);
// Click on `Add new row` button
cy.get(".nc-add-new-row-btn:visible").should("exist");
cy.get(".nc-add-new-row-btn").click();
// Title
cy.get(".nc-expand-col-Title").find(".nc-cell > input")
.should("exist")
.first()
.clear()
.type("2a");
// trigger("mouseover") is required to show the + icon
// didn't seem to work. As a kludge, used click with {force:true}
// additional delay ensures card contents are available before clicking
//
// BT
cy.get(".nc-expand-col-Sheet1").find(".nc-action-icon")
.should("exist")
.click({ force: true });
cy.wait(1000);
cy.getActiveModal()
.find('.ant-card').should('exist')
.eq(0).click();
// MM
cy.get(".nc-expand-col-Sheet1.List").find(".ant-btn-primary").click();
cy.wait(1000);
cy.getActiveModal()
.find('.ant-card').should('exist')
.eq(0).click();
// HM
cy.get(".nc-expand-col-Link2-1hm")
.find(".ant-btn-primary")
.click();
cy.wait(1000);
cy.getActiveModal()
.find('.ant-card').should('exist')
.eq(0).click();
// Save row
cy.getActiveDrawer()
.find("button")
.contains("Save row")
.click({ force: true });
// Toast
cy.toastWait("updated successfully");
// Close modal
cy.get("body").type("{esc}");
})
// In cell insert
it("Add HM, BT, MM Link, In cell form", () => {
// Insert row with `Title` field, rest of links are empty
addRow(2, "2b");
// BT
mainPage.getCell("Sheet1", 2)
.find(".nc-action-icon")
.click({ force: true });
cy.getActiveModal()
.find('.ant-card')
.should('exist')
.eq(1)
.click();
cy.wait(1000);
// MM
mainPage.getCell("Sheet1 List", 2)
.find(".nc-action-icon")
.last()
.click({ force: true });
cy.getActiveModal()
.find('.ant-card')
.should('exist')
.eq(1)
.click();
cy.wait(1000);
// HM
mainPage.getCell("Link2-1hm", 2)
.find(".nc-action-icon")
.last()
.click({ force: true });
cy.getActiveModal()
.find('.ant-card')
.should('exist')
.eq(1)
.click();
});
// Existing row, expand record
it("Add HM, BT, MM Link, expand record", () => {
addRow(3, "2c");
cy.get(".nc-row-expand").eq(2).click({ force: true });
// BT
cy.wait(1000);
cy.get(".nc-expand-col-Sheet1")
.find(".nc-action-icon")
.should("exist")
.click({ force: true });
cy.wait(1000);
cy.getActiveModal()
.find('.ant-card').should('exist')
.eq(2).click();
// MM
cy.get(".nc-expand-col-Sheet1.List")
.find(".ant-btn-primary").click();
cy.wait(1000);
cy.getActiveModal()
.find('.ant-card').should('exist')
.eq(2).click();
cy.wait(1000);
// HM
cy.get(".nc-expand-col-Link2-1hm")
.find(".ant-btn-primary").click();
cy.wait(1000);
cy.getActiveModal()
.find('.ant-card').should('exist')
.eq(2).click();
cy.wait(1000);
cy.getActiveDrawer()
.find("button")
.contains("Save row")
.click({ force: true });
// cy.toastWait("updated successfully");
cy.toastWait("No columns to update");
cy.get("body").type("{esc}");
verifyLtarCell("Sheet1", 1, "1a");
verifyLtarCell("Sheet1", 2, "1b");
verifyLtarCell("Sheet1", 3, "1c");
verifyLtarCell("Sheet1 List", 1, "1a");
verifyLtarCell("Sheet1 List", 2, "1b");
verifyLtarCell("Sheet1 List", 3, "1c");
verifyLtarCell("Link2-1hm", 1, "1a");
verifyLtarCell("Link2-1hm", 2, "1b");
verifyLtarCell("Link2-1hm", 3, "1c");
cy.closeTableTab("Sheet2");
});
it("Verification", () => {
cy.openTableTab("Sheet1", 3);
verifyLtarCell("Link1-2hm", 1, "2a");
verifyLtarCell("Link1-2hm", 2, "2b");
verifyLtarCell("Link1-2hm", 3, "2c");
verifyLtarCell("Link1-2mm", 1, "2a");
verifyLtarCell("Link1-2mm", 2, "2b");
verifyLtarCell("Link1-2mm", 3, "2c");
verifyLtarCell("Sheet2", 1, "2a");
verifyLtarCell("Sheet2", 2, "2b");
verifyLtarCell("Sheet2", 3, "2c");
cy.closeTableTab("Sheet1");
})
it("Unlink", () => {
cy.openTableTab("Sheet1", 3);
ltarUnlink("Link1-2hm", 1);
ltarUnlink("Link1-2hm", 2);
ltarUnlink("Link1-2hm", 3);
ltarUnlink("Link1-2mm", 1);
ltarUnlink("Link1-2mm", 2);
ltarUnlink("Link1-2mm", 3);
ltarUnlink("Sheet2", 1);
ltarUnlink("Sheet2", 2);
ltarUnlink("Sheet2", 3);
cy.closeTableTab("Sheet1");
});
});
};
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Raju Udava <sivadstala@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

2
scripts/cypress/integration/test/restTableOps.js

@ -12,6 +12,7 @@ let t3b = require("../common/3b_formula_column");
let t3c = require("../common/3c_lookup_column"); let t3c = require("../common/3c_lookup_column");
let t3d = require("../common/3d_rollup_column"); let t3d = require("../common/3d_rollup_column");
let t3e = require("../common/3e_duration_column"); let t3e = require("../common/3e_duration_column");
let t3f = require("../common/3f_link_to_another_record");
const { const {
setCurrentMode, setCurrentMode,
} = require("../../support/page_objects/projectConstants"); } = require("../../support/page_objects/projectConstants");
@ -32,6 +33,7 @@ const nocoTestSuite = (apiType, dbType) => {
t3c.genTest(apiType, dbType); t3c.genTest(apiType, dbType);
t3d.genTest(apiType, dbType); t3d.genTest(apiType, dbType);
// NcGUI v2 t3e.genTest(apiType, dbType); // NcGUI v2 t3e.genTest(apiType, dbType);
t3f.genTest(apiType, dbType);
}; };
nocoTestSuite("rest", "mysql"); nocoTestSuite("rest", "mysql");

737
scripts/cypress/support/commands.js

@ -24,398 +24,401 @@
// -- This will overwrite an existing command -- // -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import "cypress-file-upload"; import 'cypress-file-upload';
import { isXcdb, isPostgres } from "./page_objects/projectConstants"; import { isXcdb, isPostgres } from './page_objects/projectConstants';
require("@4tw/cypress-drag-drop"); require('@4tw/cypress-drag-drop');
// for waiting until page load // for waiting until page load
Cypress.Commands.add("waitForSpinners", () => { Cypress.Commands.add('waitForSpinners', () => {
cy.visit("http://localhost:3000/signup", { cy.visit('http://localhost:3000/signup', {
retryOnNetworkFailure: true, retryOnNetworkFailure: true,
timeout: 1200000, timeout: 1200000,
headers: { headers: {
"Accept-Encoding": "gzip, deflate", 'Accept-Encoding': 'gzip, deflate',
}, },
}); });
cy.get(".nc-form-signup").should("exist") cy.get('.nc-form-signup').should('exist');
}); });
Cypress.Commands.add("signinOrSignup", (_args) => { Cypress.Commands.add('signinOrSignup', (_args) => {
const args = Object.assign( const args = Object.assign(
{ username: "user@nocodb.com", password: "Password123." }, { username: 'user@nocodb.com', password: 'Password123.' },
_args _args
); );
cy.wait(1000); cy.wait(1000);
// signin/signup // signin/signup
cy.get("body").then(($body) => { cy.get('body').then(($body) => {
// cy.wait(1000) // cy.wait(1000)
cy.url().then((url) => { cy.url().then((url) => {
if (!url.includes("/projects")) { if (!url.includes('/projects')) {
// handle initial load // handle initial load
if ($body.find(".welcome-page").length > 0) { if ($body.find('.welcome-page').length > 0) {
cy.wait(8000); cy.wait(8000);
cy.get("body").trigger("mousemove"); cy.get('body').trigger('mousemove');
cy.snip("LetsBegin"); cy.snip('LetsBegin');
cy.contains("Let's Begin").click(); cy.contains('Let\'s Begin').click();
cy.get('input[type="text"]', { timeout: 12000 }).type( cy.get('input[type="text"]', { timeout: 12000 }).type(
args.username args.username
); );
cy.get('input[type="password"]').type(args.password); cy.get('input[type="password"]').type(args.password);
cy.snip("SignUp"); cy.snip('SignUp');
cy.get('button:contains("SIGN UP")').click(); cy.get('button:contains("SIGN UP")').click();
// handle signin // handle signin
} else { } else {
cy.get('input[type="text"]', { timeout: 12000 }).type( cy.get('input[type="text"]', { timeout: 12000 }).type(
args.username args.username
); );
cy.get('input[type="password"]').type(args.password); cy.get('input[type="password"]').type(args.password);
cy.snip("SignIn"); cy.snip('SignIn');
cy.get('button:contains("SIGN IN")').click(); cy.get('button:contains("SIGN IN")').click();
} }
} else if (url.includes("/signin")) { } else if (url.includes('/signin')) {
cy.get('input[type="text"]', { timeout: 12000 }).type( cy.get('input[type="text"]', { timeout: 12000 }).type(
args.username args.username
); );
cy.get('input[type="password"]').type(args.password); cy.get('input[type="password"]').type(args.password);
cy.snip("SignIn"); cy.snip('SignIn');
cy.get('button:contains("SIGN IN")').click(); cy.get('button:contains("SIGN IN")').click();
} }
});
}); });
});
// indicates page-load complete // indicates page-load complete
cy.get(".nc-noco-brand-icon", { timeout: 12000 }).should("exist"); cy.get('.nc-noco-brand-icon', { timeout: 12000 }).should('exist');
}); });
// for opening/creating a rest project // for opening/creating a rest project
Cypress.Commands.add("openOrCreateRestProject", (_args) => { Cypress.Commands.add('openOrCreateRestProject', (_args) => {
const args = Object.assign({ new: false }, _args); const args = Object.assign({ new: false }, _args);
// signin/signup // signin/signup
cy.signinOrSignup(); cy.signinOrSignup();
cy.get(".nc-new-project-menu").should("exist"); cy.get('.nc-new-project-menu').should('exist');
cy.snip("ProjectPage"); cy.snip('ProjectPage');
cy.get("body").then(($body) => { cy.get('body').then(($body) => {
const filter = args.meta const filter = args.meta
? ".nc-meta-project-row" ? '.nc-meta-project-row'
: ":not(.nc-meta-project-row)"; : ':not(.nc-meta-project-row)';
// if project exist open // if project exist open
if ( if (
$body.find(".nc-rest-project-row").filter(filter).length && $body.find('.nc-rest-project-row').filter(filter).length &&
!args.new !args.new
) { ) {
cy.get(".nc-rest-project-row").filter(filter).first().click(); cy.get('.nc-rest-project-row').filter(filter).first().click();
} else { } else {
cy.contains("New Project") cy.contains('New Project')
.trigger("onmouseover") .trigger('onmouseover')
.trigger("mouseenter"); .trigger('mouseenter');
if (args.meta) { if (args.meta) {
cy.get(".nc-create-xc-db-project").click(); cy.get('.nc-create-xc-db-project').click();
cy.url({ timeout: 6000 }).should("contain", "#/project/xcdb"); cy.url({ timeout: 6000 }).should('contain', '#/project/xcdb');
cy.get(".nc-metadb-project-name").type( cy.get('.nc-metadb-project-name').type(
"test_proj" + Date.now() 'test_proj' + Date.now()
); );
cy.contains("button", "Create", { timeout: 3000 }).click(); cy.contains('button', 'Create', { timeout: 3000 }).click();
} else { } else {
cy.get(".nc-create-external-db-project").click(); cy.get('.nc-create-external-db-project').click();
cy.url({ timeout: 6000 }).should("contain", "#/project"); cy.url({ timeout: 6000 }).should('contain', '#/project');
cy.get(".database-field input").click().clear().type("sakila"); cy.get('.database-field input').click().clear().type('sakila');
cy.contains("Test Database Connection").click(); cy.contains('Test Database Connection').click();
cy.contains("Ok & Save Project", { timeout: 3000 }).click(); cy.contains('Ok & Save Project', { timeout: 3000 }).click();
} }
} }
}); });
cy.url({ timeout: 20000 }).should("contain", "#/nc/"); cy.url({ timeout: 20000 }).should('contain', '#/nc/');
}); });
Cypress.Commands.add("refreshTableTab", () => { Cypress.Commands.add('refreshTableTab', () => {
cy.task("log", `[refreshTableTab]`); cy.task('log', `[refreshTableTab]`);
cy.get(".nc-project-tree") cy.get('.nc-project-tree')
.find(".v-list-item__title:contains(Tables)", { timeout: 10000 }) .find('.v-list-item__title:contains(Tables)', { timeout: 10000 })
.should("exist") .should('exist')
.first() .first()
.rightclick({ force: true }); .rightclick({ force: true });
cy.getActiveMenu() cy.getActiveMenu()
.find('[role="menuitem"]') .find('[role="menuitem"]')
.contains("Tables Refresh") .contains('Tables Refresh')
.should("exist") .should('exist')
.click({ force: true }); .click({ force: true });
cy.toastWait("Tables refreshed"); cy.toastWait('Tables refreshed');
}); });
// tn: table name // tn: table name
// rc: row count. validate row count if rc!=0 // rc: row count. validate row count if rc!=0
Cypress.Commands.add("openTableTab", (tn, rc) => { Cypress.Commands.add('openTableTab', (tn, rc) => {
cy.task("log", `[openTableTab] ${tn} ${rc}`); cy.task('log', `[openTableTab] ${tn} ${rc}`);
cy.get(`.nc-project-tree-tbl-${tn}`)
.should('exist')
.first()
.click();
// kludge to make new tab active
// cy.get('.ant-tabs-tab-btn')
// .contains(tn)
// .should('exist')
// .click();
cy.wait(3000);
cy.get('.xc-row-table.nc-grid').should('exist');
// wait for page rendering to complete
if (rc != 0) {
cy.get('.nc-grid-row').should('have.length', rc);
}
});
cy.get(`.nc-project-tree-tbl-${tn}`) Cypress.Commands.add('closeTableTab', (tn) => {
.should("exist") cy.task('log', `[closeTableTab] ${tn}`);
.first() cy.get('.ant-tabs-tab-btn')
.click(); .contains(tn)
.should('exist')
.parent()
.parent()
.parent()
.find('button')
.click();
// subsequent tab open commands will fail if tab is not closed completely
cy.wait(1000);
});
// kludge to make new tab active Cypress.Commands.add('openOrCreateGqlProject', (_args) => {
// cy.get('.ant-tabs-tab-btn') const args = Object.assign({ new: false, meta: false }, _args);
// .contains(tn)
// .should('exist')
// .click();
cy.wait(3000);
cy.get('.xc-row-table.nc-grid').should('exist'); cy.signinOrSignup();
// wait for page rendering to complete cy.get('.nc-new-project-menu').should('exist');
if (rc != 0) { cy.get('body').then(($body) => {
cy.get(".nc-grid-row").should("have.length", rc); const filter = args.meta
? '.nc-meta-project-row'
: ':not(.nc-meta-project-row)';
// if project exist open
if (
$body.find('.nc-graphql-project-row').filter(filter).length &&
!args.new
) {
cy.get('.nc-graphql-project-row').filter(filter).first().click();
} else {
cy.contains('New Project')
.trigger('onmouseover')
.trigger('mouseenter');
if (args.meta) {
cy.get('.nc-create-xc-db-project').click();
cy.url({ timeout: 6000 }).should('contain', '#/project/xcdb');
cy.contains('GRAPHQL APIs').closest('label').click();
cy.get('.nc-metadb-project-name').type(
'test_proj' + Date.now()
);
cy.contains('button', 'Create', { timeout: 3000 }).click();
} else {
cy.get('.nc-create-external-db-project').click();
cy.url({ timeout: 6000 }).should('contain', '#/project');
cy.contains('GRAPHQL APIs').closest('label').click();
cy.get('.database-field input').click().clear().type('sakila');
cy.contains('Test Database Connection').click();
cy.contains('Ok & Save Project').should('exist').click();
}
} }
}); });
cy.url({ timeout: 20000 }).should('contain', '#/nc/');
Cypress.Commands.add("closeTableTab", (tn) => {
cy.task("log", `[closeTableTab] ${tn}`);
cy.get('.ant-tabs-tab-btn')
.contains(tn)
.should('exist')
.parent()
.parent()
.parent()
.find('button')
.click();
// subsequent tab open commands will fail if tab is not closed completely
cy.wait(1000);
});
Cypress.Commands.add("openOrCreateGqlProject", (_args) => {
const args = Object.assign({ new: false, meta: false }, _args);
cy.signinOrSignup();
cy.get(".nc-new-project-menu").should("exist");
cy.get("body").then(($body) => {
const filter = args.meta
? ".nc-meta-project-row"
: ":not(.nc-meta-project-row)";
// if project exist open
if (
$body.find(".nc-graphql-project-row").filter(filter).length &&
!args.new
) {
cy.get(".nc-graphql-project-row").filter(filter).first().click();
} else {
cy.contains("New Project")
.trigger("onmouseover")
.trigger("mouseenter");
if (args.meta) {
cy.get(".nc-create-xc-db-project").click();
cy.url({ timeout: 6000 }).should("contain", "#/project/xcdb");
cy.contains("GRAPHQL APIs").closest("label").click();
cy.get(".nc-metadb-project-name").type(
"test_proj" + Date.now()
);
cy.contains("button", "Create", { timeout: 3000 }).click();
} else {
cy.get(".nc-create-external-db-project").click();
cy.url({ timeout: 6000 }).should("contain", "#/project");
cy.contains("GRAPHQL APIs").closest("label").click();
cy.get(".database-field input").click().clear().type("sakila");
cy.contains("Test Database Connection").click();
cy.contains("Ok & Save Project").should('exist').click();
}
}
});
cy.url({ timeout: 20000 }).should("contain", "#/nc/");
}); });
let LOCAL_STORAGE_MEMORY = {}; let LOCAL_STORAGE_MEMORY = {};
let LOCAL_STORAGE_MEMORY_v2 = {}; let LOCAL_STORAGE_MEMORY_v2 = {};
Cypress.Commands.add("saveLocalStorage", (name) => { Cypress.Commands.add('saveLocalStorage', (name) => {
if(name) { if (name) {
cy.task('log', `[saveLocalStorage] ${name}`); cy.task('log', `[saveLocalStorage] ${name}`);
LOCAL_STORAGE_MEMORY_v2[name] = {} LOCAL_STORAGE_MEMORY_v2[name] = {};
Object.keys(localStorage).forEach((key) => {
LOCAL_STORAGE_MEMORY_v2[name][key] = localStorage[key];
});
return;
}
LOCAL_STORAGE_MEMORY = {};
Object.keys(localStorage).forEach((key) => { Object.keys(localStorage).forEach((key) => {
LOCAL_STORAGE_MEMORY[key] = localStorage[key]; LOCAL_STORAGE_MEMORY_v2[name][key] = localStorage[key];
}); });
cy.printLocalStorage(); return;
}
});
Cypress.Commands.add("restoreLocalStorage", (name) => { LOCAL_STORAGE_MEMORY = {};
if(name) { Object.keys(localStorage).forEach((key) => {
cy.task('log', `[restoreLocalStorage] ${name}`); LOCAL_STORAGE_MEMORY[key] = localStorage[key];
Object.keys(LOCAL_STORAGE_MEMORY_v2[name]).forEach((key) => { });
localStorage.setItem(key, LOCAL_STORAGE_MEMORY_v2[name][key]); cy.printLocalStorage();
});
return;
}
cy.deleteLocalStorage().then(() => { });
Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
});
cy.printLocalStorage(); Cypress.Commands.add('restoreLocalStorage', (name) => {
if (name) {
cy.task('log', `[restoreLocalStorage] ${name}`);
Object.keys(LOCAL_STORAGE_MEMORY_v2[name]).forEach((key) => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY_v2[name][key]);
}); });
}); return;
}
Cypress.Commands.add("deleteLocalStorage", () => { cy.deleteLocalStorage().then(() => {
Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => { Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => {
localStorage.removeItem(key); localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
}); });
cy.printLocalStorage();
});
});
Cypress.Commands.add('deleteLocalStorage', () => {
Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => {
localStorage.removeItem(key);
});
}); });
Cypress.Commands.add('printLocalStorage', () => { Cypress.Commands.add('printLocalStorage', () => {
cy.task('log', `[printLocalStorage]`); cy.task('log', `[printLocalStorage]`);
cy.task('log', JSON.stringify(localStorage, null, 2)); cy.task('log', JSON.stringify(localStorage, null, 2));
cy.task('log', JSON.stringify(LOCAL_STORAGE_MEMORY, null, 2)); cy.task('log', JSON.stringify(LOCAL_STORAGE_MEMORY, null, 2));
}) });
Cypress.Commands.add("getActiveModal", () => { Cypress.Commands.add('getActiveModal', () => {
return cy.get(".ant-modal-content:visible").last() return cy.get('.ant-modal-content:visible').last();
}); });
Cypress.Commands.add("getActiveMenu", () => { Cypress.Commands.add('getActiveMenu', (overlayClassName) => {
return cy.get(".ant-dropdown-content:visible").last(); if (overlayClassName) {
return cy.get(`${overlayClassName} .ant-dropdown-content:visible`);
}
return cy.get('.ant-dropdown-content:visible').last();
}); });
Cypress.Commands.add("getActivePopUp", () => { Cypress.Commands.add('getActivePopUp', () => {
return cy.get(".ant-menu-submenu-popup:visible").last(); return cy.get('.ant-menu-submenu-popup:visible').last();
}) });
Cypress.Commands.add("getActiveSelection", () => { Cypress.Commands.add('getActiveSelection', () => {
return cy.get(".ant-select-dropdown:visible").last(); return cy.get('.ant-select-dropdown:visible').last();
}) });
Cypress.Commands.add("getActiveDrawer", () => { Cypress.Commands.add('getActiveDrawer', () => {
return cy.get(".ant-drawer-content:visible").last(); return cy.get('.ant-drawer-content:visible').last();
}); });
Cypress.Commands.add("getActivePicker", () => { Cypress.Commands.add('getActivePicker', () => {
return cy.get(".ant-picker-dropdown :visible").last(); return cy.get('.ant-picker-dropdown :visible').last();
}); });
Cypress.Commands.add("createTable", (name) => { Cypress.Commands.add('createTable', (name) => {
cy.task("log", `[createTableTab] ${name}`); cy.task('log', `[createTableTab] ${name}`);
cy.wait(1000); cy.wait(1000);
cy.get('.nc-add-new-table').should('exist').click(); cy.get('.nc-add-new-table').should('exist').click();
cy.wait(1000); cy.wait(1000);
cy.getActiveModal().find(`input[type="text"]:visible`) cy.getActiveModal().find(`input[type="text"]:visible`)
.click() .click()
.clear() .clear()
.type(name) .type(name);
// submit button // submit button
cy.getActiveModal().find("button.ant-btn-primary:visible").click(); cy.getActiveModal().find('button.ant-btn-primary:visible').click();
cy.wait(1000) cy.wait(1000);
cy.get('.xc-row-table.nc-grid').should('exist'); cy.get('.xc-row-table.nc-grid').should('exist');
// cy.get('.ant-tabs-tab-active > .ant-tabs-tab-btn').contains(name).should("exist"); // cy.get('.ant-tabs-tab-active > .ant-tabs-tab-btn').contains(name).should("exist");
cy.url().should("contain", `table/${name}`); cy.url().should('contain', `table/${name}`);
cy.get(`.nc-project-tree-tbl-${name}`).should("exist"); cy.get(`.nc-project-tree-tbl-${name}`).should('exist');
cy.wait(1000) cy.wait(1000);
}); });
Cypress.Commands.add("deleteTable", (name, dbType) => { Cypress.Commands.add('deleteTable', (name, dbType) => {
cy.get(`.nc-project-tree-tbl-${name}`).should("exist").rightclick(); cy.get(`.nc-project-tree-tbl-${name}`).should('exist').rightclick();
cy.getActiveMenu().find('[role="menuitem"]').contains("Delete").click(); cy.getActiveMenu().find('[role="menuitem"]').contains('Delete').click();
cy.getActiveModal().find("button").contains("Yes").click(); cy.getActiveModal().find('button').contains('Yes').click();
cy.toastWait(`Deleted table successfully`); cy.toastWait(`Deleted table successfully`);
}); });
Cypress.Commands.add("renameTable", (oldName, newName) => { Cypress.Commands.add('renameTable', (oldName, newName) => {
// right click on project table name // right click on project table name
cy.get(`.nc-project-tree-tbl-${oldName}`) cy.get(`.nc-project-tree-tbl-${oldName}`)
.should('exist') .should('exist')
.first() .first()
.rightclick(); .rightclick();
// choose rename option from menu // choose rename option from menu
cy.getActiveMenu() cy.getActiveMenu()
.find('[role="menuitem"]') .find('[role="menuitem"]')
.contains("Rename") .contains('Rename')
.click({ force: true }); .click({ force: true });
// feed new name // feed new name
cy.getActiveModal().find("input").clear().type(newName); cy.getActiveModal().find('input').clear().type(newName);
// submit // submit
cy.getActiveModal().find("button").contains("Submit").click(); cy.getActiveModal().find('button').contains('Submit').click();
cy.toastWait("Table renamed successfully"); cy.toastWait('Table renamed successfully');
}); });
Cypress.Commands.add("createColumn", (table, columnName) => { Cypress.Commands.add('createColumn', (table, columnName) => {
cy.get(".nc-project-tree") cy.get('.nc-project-tree')
.find(".v-list-item__title:contains(Tables)") .find('.v-list-item__title:contains(Tables)')
.should('exist') .should('exist')
.first() .first()
.click(); .click();
cy.get(".nc-project-tree") cy.get('.nc-project-tree')
.contains(table) .contains(table)
.should('exist') .should('exist')
.first() .first()
.click({ force: true }); .click({ force: true });
cy.get(`.project-tab:contains(${table}):visible`).should("exist"); cy.get(`.project-tab:contains(${table}):visible`).should('exist');
cy.get(".v-window-item--active .nc-grid tr > th:last button").click({ cy.get('.v-window-item--active .nc-grid tr > th:last button').click({
force: true, force: true,
}); });
cy.get(".nc-column-name-input input").clear().type(columnName); cy.get('.nc-column-name-input input').clear().type(columnName);
cy.getActiveMenu("Menu_CreateColumn"); cy.getActiveMenu('Menu_CreateColumn');
cy.get(".nc-col-create-or-edit-card").contains("Save").click(); cy.get('.nc-col-create-or-edit-card').contains('Save').click();
cy.get("th:contains(new_column)").should("exist"); cy.get('th:contains(new_column)').should('exist');
}); });
Cypress.Commands.add("toastWait", (msg) => { Cypress.Commands.add('toastWait', (msg) => {
// cy.get('.ant-message-notice-content:visible', { timout: 30000 }).should('exist') // cy.get('.ant-message-notice-content:visible', { timout: 30000 }).should('exist')
cy.get('.ant-message-notice-content:visible', { timout: 30000 }).contains(msg).should('exist') cy.get('.ant-message-notice-content:visible', { timout: 30000 }).contains(msg).should('exist');
cy.get('.ant-message-notice-content:visible', { timout: 12000 }).should('not.exist') cy.get('.ant-message-notice-content:visible', { timout: 12000 }).should('not.exist');
}); });
// vn: view name // vn: view name
// rc: expected row count. validate row count if rc!=0 // rc: expected row count. validate row count if rc!=0
Cypress.Commands.add("openViewsTab", (vn, rc) => { Cypress.Commands.add('openViewsTab', (vn, rc) => {
cy.task("log", `[openViewsTab] ${vn} ${rc}`); cy.task('log', `[openViewsTab] ${vn} ${rc}`);
cy.get(`.nc-project-tree-tbl-${vn}`, { timeout: 10000 }).should("exist") cy.get(`.nc-project-tree-tbl-${vn}`, { timeout: 10000 }).should('exist')
.first() .first()
.click({ force: true }); .click({ force: true });
// kludge to make new tab active // kludge to make new tab active
cy.get('.ant-tabs-tab-btn') cy.get('.ant-tabs-tab-btn')
.contains(vn) .contains(vn)
.should('exist') .should('exist')
.click({ force: true }); .click({ force: true });
// wait for page rendering to complete // wait for page rendering to complete
if (rc != 0) { if (rc != 0) {
cy.get('.xc-row-table.nc-grid').should('exist'); cy.get('.xc-row-table.nc-grid').should('exist');
cy.get(".nc-grid-row").should("have.length", rc); cy.get('.nc-grid-row').should('have.length', rc);
} }
}); });
Cypress.Commands.add("closeViewsTab", (vn) => { Cypress.Commands.add('closeViewsTab', (vn) => {
cy.task("log", `[closeViewsTab] ${vn}`); cy.task('log', `[closeViewsTab] ${vn}`);
cy.get('.ant-tabs-tab-btn') cy.get('.ant-tabs-tab-btn')
.contains(vn) .contains(vn)
.should('exist') .should('exist')
.parent() .parent()
.parent() .parent()
.parent() .parent()
.find('button') .find('button')
.click(); .click();
}); });
// Support for screen-shots // Support for screen-shots
@ -424,66 +427,66 @@ Cypress.Commands.add("closeViewsTab", (vn) => {
let screenShotDb = []; let screenShotDb = [];
// snip entire screen // snip entire screen
Cypress.Commands.add("snip", (filename) => { Cypress.Commands.add('snip', (filename) => {
if ( if (
true === Cypress.env("screenshot") && true === Cypress.env('screenshot') &&
false === screenShotDb.includes(filename) false === screenShotDb.includes(filename)
) { ) {
let storeName = `${screenShotDb.length}_${filename}`; let storeName = `${screenShotDb.length}_${filename}`;
screenShotDb.push(filename); screenShotDb.push(filename);
cy.wait(1000); cy.wait(1000);
cy.screenshot(storeName, { overwrite: true }); cy.screenshot(storeName, { overwrite: true });
} }
}); });
// snip current modal // snip current modal
Cypress.Commands.add("snipActiveModal", (filename) => { Cypress.Commands.add('snipActiveModal', (filename) => {
if ( if (
true === Cypress.env("screenshot") && true === Cypress.env('screenshot') &&
false === screenShotDb.includes(filename) false === screenShotDb.includes(filename)
) { ) {
let storeName = `${screenShotDb.length}_${filename}`; let storeName = `${screenShotDb.length}_${filename}`;
screenShotDb.push(filename); screenShotDb.push(filename);
cy.wait(1000); cy.wait(1000);
// cy.getActiveModal().screenshot(filename, { // cy.getActiveModal().screenshot(filename, {
// padding: 0, // padding: 0,
// overwrite: true, // overwrite: true,
// }); // });
cy.screenshot(storeName, { overwrite: true }); cy.screenshot(storeName, { overwrite: true });
} }
}); });
// snip current menu // snip current menu
Cypress.Commands.add("snipActiveMenu", (filename) => { Cypress.Commands.add('snipActiveMenu', (filename) => {
if ( if (
true === Cypress.env("screenshot") && true === Cypress.env('screenshot') &&
false === screenShotDb.includes(filename) false === screenShotDb.includes(filename)
) { ) {
let storeName = `${screenShotDb.length}_${filename}`; let storeName = `${screenShotDb.length}_${filename}`;
screenShotDb.push(filename); screenShotDb.push(filename);
cy.wait(1000); cy.wait(1000);
// cy.getActiveMenu().screenshot(filename, { // cy.getActiveMenu().screenshot(filename, {
// padding: 0, // padding: 0,
// overwrite: true, // overwrite: true,
// }); // });
cy.screenshot(storeName, { overwrite: true }); cy.screenshot(storeName, { overwrite: true });
} }
}); });
// pre-test file hook // pre-test file hook
Cypress.Commands.add("fileHook", () => { Cypress.Commands.add('fileHook', () => {
window.localStorage.setItem('vueuse-color-scheme', 'light') window.localStorage.setItem('vueuse-color-scheme', 'light');
}); });
Cypress.Commands.add("signOut", () => { Cypress.Commands.add('signOut', () => {
// sign out // sign out
cy.visit(`/`); cy.visit(`/`);
cy.get('.nc-project-page-title', {timeout: 30000}).contains("My Projects").should("be.visible"); cy.get('.nc-project-page-title', { timeout: 30000 }).contains('My Projects').should('be.visible');
cy.get('.nc-menu-accounts', {timeout: 30000}).should('exist').click(); cy.get('.nc-menu-accounts', { timeout: 30000 }).should('exist').click();
cy.getActiveMenu().find('.ant-dropdown-menu-item').eq(1).click(); cy.getActiveMenu().find('.ant-dropdown-menu-item').eq(1).click();
cy.wait(5000); cy.wait(5000);
cy.get('button:contains("SIGN")').should('exist') cy.get('button:contains("SIGN")').should('exist');
}); });

4
scripts/cypress/support/page_objects/mainPage.js

@ -313,7 +313,7 @@ export class _mainPage {
sortField = (field, criteria) => { sortField = (field, criteria) => {
cy.get(".nc-sort-menu-btn").click(); cy.get(".nc-sort-menu-btn").click();
cy.wait(500) cy.wait(500)
cy.getActiveMenu().contains("Add Sort Option").click(); cy.getActiveMenu('.sort-menu-overlay').contains("Add Sort Option").click();
cy.wait(500) cy.wait(500)
// cy.get(".nc-sort-field-select div").first().click().type(field); // cy.get(".nc-sort-field-select div").first().click().type(field);
cy.get(".nc-sort-field-select div").first().click(); cy.get(".nc-sort-field-select div").first().click();
@ -322,7 +322,7 @@ export class _mainPage {
cy.wait(500) cy.wait(500)
cy.get(".nc-sort-dir-select div").first().click(); cy.get(".nc-sort-dir-select div").first().click();
cy.wait(500) cy.wait(500)
cy.get('.ant-select-dropdown:visible').find(`.ant-select-item`).contains(criteria).should('exist').click(); cy.get('.sort-dir-dropdown.ant-select-dropdown:visible').find(`.ant-select-item`).contains(criteria).should('exist').click();
cy.wait(500) cy.wait(500)
cy.get(".nc-sort-menu-btn").click(); cy.get(".nc-sort-menu-btn").click();
cy.wait(500) cy.wait(500)

20
scripts/sdk/swagger.json

@ -3486,7 +3486,12 @@
"operationId": "db-table-row-delete", "operationId": "db-table-row-delete",
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK",
"content": {
"application/json": {
"schema": {}
}
}
} }
}, },
"tags": [ "tags": [
@ -5914,9 +5919,6 @@
"pageInfo": { "pageInfo": {
"$ref": "#/components/schemas/Paginated" "$ref": "#/components/schemas/Paginated"
} }
},
"": {
"type": "string"
} }
}, },
"Base": { "Base": {
@ -6241,8 +6243,7 @@
"type": "string" "type": "string"
}, },
"title": { "title": {
"type": "string", "type": "string"
"required": true
}, },
"deleted": { "deleted": {
"type": "boolean" "type": "boolean"
@ -6286,7 +6287,8 @@
} }
] ]
} }
} },
"required": ["title"]
}, },
"TableInfo": { "TableInfo": {
"title": "Table", "title": "Table",
@ -7074,10 +7076,12 @@
"properties": { "properties": {
"options": { "options": {
"type": "array", "type": "array",
"required": true,
"$ref": "#/components/schemas/SelectOption" "$ref": "#/components/schemas/SelectOption"
} }
}, },
"required": [
"options"
],
"examples": [ "examples": [
{ {
"options": [ "options": [

Loading…
Cancel
Save