Browse Source

chore(gui-v2): sync with refactor/gui-v2

pull/2795/head
Wing-Kam Wong 2 years ago
parent
commit
ff96c6cde1
  1. BIN
      packages/nc-gui-v2/assets/img/404.jpg
  2. BIN
      packages/nc-gui-v2/assets/img/500.jpg
  3. BIN
      packages/nc-gui-v2/assets/img/abcd/320.png
  4. BIN
      packages/nc-gui-v2/assets/img/abcd/adsense.png
  5. BIN
      packages/nc-gui-v2/assets/img/abcd/basecamp.png
  6. BIN
      packages/nc-gui-v2/assets/img/abcd/bigquery.png
  7. BIN
      packages/nc-gui-v2/assets/img/abcd/cassandra.png
  8. BIN
      packages/nc-gui-v2/assets/img/abcd/couchdb.png
  9. BIN
      packages/nc-gui-v2/assets/img/abcd/cratedb.jpg
  10. BIN
      packages/nc-gui-v2/assets/img/abcd/elasticsearch.png
  11. BIN
      packages/nc-gui-v2/assets/img/abcd/fbads.png
  12. BIN
      packages/nc-gui-v2/assets/img/abcd/freshworks.png
  13. BIN
      packages/nc-gui-v2/assets/img/abcd/github.png
  14. BIN
      packages/nc-gui-v2/assets/img/abcd/graphql.png
  15. BIN
      packages/nc-gui-v2/assets/img/abcd/grpc.png
  16. BIN
      packages/nc-gui-v2/assets/img/abcd/mariadb.png
  17. BIN
      packages/nc-gui-v2/assets/img/abcd/mongodb.png
  18. BIN
      packages/nc-gui-v2/assets/img/abcd/mssql.png
  19. BIN
      packages/nc-gui-v2/assets/img/abcd/mysql.png
  20. BIN
      packages/nc-gui-v2/assets/img/abcd/netsuit.png
  21. BIN
      packages/nc-gui-v2/assets/img/abcd/pg.png
  22. BIN
      packages/nc-gui-v2/assets/img/abcd/postman.jpg
  23. 1
      packages/nc-gui-v2/assets/img/abcd/razorpay.svg
  24. BIN
      packages/nc-gui-v2/assets/img/abcd/rest.png
  25. BIN
      packages/nc-gui-v2/assets/img/abcd/salesforce.png
  26. BIN
      packages/nc-gui-v2/assets/img/abcd/sap.png
  27. BIN
      packages/nc-gui-v2/assets/img/abcd/sendgrid.png
  28. BIN
      packages/nc-gui-v2/assets/img/abcd/shopify.png
  29. BIN
      packages/nc-gui-v2/assets/img/abcd/snowflake.png
  30. BIN
      packages/nc-gui-v2/assets/img/abcd/swagger.png
  31. BIN
      packages/nc-gui-v2/assets/img/abcd/twilio.png
  32. BIN
      packages/nc-gui-v2/assets/img/abcd/zohocrm.png
  33. BIN
      packages/nc-gui-v2/assets/img/discourse-icon.png
  34. BIN
      packages/nc-gui-v2/assets/img/grpc-icon-color.png
  35. BIN
      packages/nc-gui-v2/assets/img/logo.png
  36. BIN
      packages/nc-gui-v2/assets/img/signin-google.png
  37. BIN
      packages/nc-gui-v2/assets/img/signup-google.png
  38. 1
      packages/nc-gui-v2/assets/img/temp/251.json
  39. BIN
      packages/nc-gui-v2/assets/img/temp/db/aurora.png
  40. BIN
      packages/nc-gui-v2/assets/img/temp/db/maria.png
  41. BIN
      packages/nc-gui-v2/assets/img/temp/db/maria.png.jpg
  42. BIN
      packages/nc-gui-v2/assets/img/temp/db/mongo.png
  43. BIN
      packages/nc-gui-v2/assets/img/temp/db/mongo.png.jpg
  44. BIN
      packages/nc-gui-v2/assets/img/temp/db/mssql.png
  45. BIN
      packages/nc-gui-v2/assets/img/temp/db/mssql.png.jpg
  46. BIN
      packages/nc-gui-v2/assets/img/temp/db/mysql.png
  47. BIN
      packages/nc-gui-v2/assets/img/temp/db/mysql.png.jpg
  48. BIN
      packages/nc-gui-v2/assets/img/temp/db/oracle.jpeg
  49. BIN
      packages/nc-gui-v2/assets/img/temp/db/oracle.png
  50. BIN
      packages/nc-gui-v2/assets/img/temp/db/oracle.png.jpg
  51. BIN
      packages/nc-gui-v2/assets/img/temp/db/pg.png
  52. BIN
      packages/nc-gui-v2/assets/img/temp/db/pg.png.jpg
  53. BIN
      packages/nc-gui-v2/assets/img/temp/db/postgre.png
  54. BIN
      packages/nc-gui-v2/assets/img/temp/db/postgre.png.jpg
  55. BIN
      packages/nc-gui-v2/assets/img/temp/db/redshift.png
  56. BIN
      packages/nc-gui-v2/assets/img/temp/db/redshift.png.jpg
  57. 67
      packages/nc-gui-v2/assets/img/temp/db/sqlite.svg
  58. BIN
      packages/nc-gui-v2/assets/img/temp/db/sqlserver.png
  59. BIN
      packages/nc-gui-v2/assets/img/temp/db/sqlserver.png.jpg
  60. BIN
      packages/nc-gui-v2/assets/img/temp/salesforce-3-569548.webp
  61. BIN
      packages/nc-gui-v2/assets/img/temp/sap.png
  62. 121
      packages/nc-gui-v2/assets/img/temp/stripe.svg
  63. 3
      packages/nc-gui-v2/assets/style-v2.scss
  64. 475
      packages/nc-gui-v2/assets/style.css
  65. 2
      packages/nc-gui-v2/assets/style/app.styl
  66. 367
      packages/nc-gui-v2/assets/style/style.css
  67. 1
      packages/nc-gui-v2/assets/style/variables.styl
  68. 1
      packages/nc-gui-v2/components.d.ts
  69. 307
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  70. 121
      packages/nc-gui-v2/components/dlg/TableRename.vue
  71. 475
      packages/nc-gui-v2/components/template/Editor.vue
  72. 25
      packages/nc-gui-v2/components/template/utils.ts
  73. 21
      packages/nc-gui-v2/composables/useMetas.ts
  74. 25
      packages/nc-gui-v2/composables/useTabs.ts
  75. 8
      packages/nc-gui-v2/lib/enums.ts
  76. 40
      packages/nc-gui-v2/lib/types.ts
  77. 1
      packages/nc-gui-v2/nuxt.config.ts
  78. 43937
      packages/nc-gui-v2/package-lock.json
  79. 2
      packages/nc-gui-v2/package.json
  80. 69
      packages/nc-gui-v2/pages/index/index.vue
  81. 3
      packages/nc-gui-v2/pages/nc/[projectId].vue
  82. 14
      packages/nc-gui-v2/pages/project/index/create-external.vue
  83. 4
      packages/nc-gui-v2/pages/signin.vue
  84. 6
      packages/nc-gui-v2/pages/signup.vue
  85. 118
      packages/nc-gui-v2/utils/projectCreateUtils.ts
  86. 6
      packages/nc-gui-v2/utils/validation.ts

BIN
packages/nc-gui-v2/assets/img/404.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

BIN
packages/nc-gui-v2/assets/img/500.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/320.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/adsense.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/basecamp.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/bigquery.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/cassandra.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/couchdb.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/cratedb.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/elasticsearch.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/fbads.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/freshworks.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/github.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/graphql.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/grpc.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/mariadb.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/mongodb.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/mssql.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/mysql.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/netsuit.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/pg.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/postman.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

1
packages/nc-gui-v2/assets/img/abcd/razorpay.svg

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="640"><g fill="none" fill-rule="evenodd"><path fill="#3395FF" d="M299.6 262.7l-15.7 58 90-58.3-59 220h60l87-325"/><path fill="#072654" d="M202.6 390l-24.8 92.4h122.7l50.2-188-148 95.5"/></g></svg>

Before

Width:  |  Height:  |  Size: 255 B

BIN
packages/nc-gui-v2/assets/img/abcd/rest.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/salesforce.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/sap.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/sendgrid.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/shopify.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/snowflake.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/swagger.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/twilio.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

BIN
packages/nc-gui-v2/assets/img/abcd/zohocrm.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

BIN
packages/nc-gui-v2/assets/img/discourse-icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

BIN
packages/nc-gui-v2/assets/img/grpc-icon-color.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

BIN
packages/nc-gui-v2/assets/img/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

BIN
packages/nc-gui-v2/assets/img/signin-google.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

BIN
packages/nc-gui-v2/assets/img/signup-google.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

1
packages/nc-gui-v2/assets/img/temp/251.json

@ -1 +0,0 @@
[{"city":"Kabul"}]

BIN
packages/nc-gui-v2/assets/img/temp/db/aurora.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/maria.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/maria.png.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/mongo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/mongo.png.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/mssql.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/mssql.png.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/mysql.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/mysql.png.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/oracle.jpeg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/oracle.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/oracle.png.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/pg.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/pg.png.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/postgre.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/postgre.png.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/redshift.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/redshift.png.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

67
packages/nc-gui-v2/assets/img/temp/db/sqlite.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.7 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/sqlserver.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

BIN
packages/nc-gui-v2/assets/img/temp/db/sqlserver.png.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

BIN
packages/nc-gui-v2/assets/img/temp/salesforce-3-569548.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

BIN
packages/nc-gui-v2/assets/img/temp/sap.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

121
packages/nc-gui-v2/assets/img/temp/stripe.svg

@ -1,121 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="362"
height="151.8"
viewBox="0 0 95.779166 40.163749"
version="1.1"
id="svg5512"
inkscape:version="0.92.0 r15299"
sodipodi:docname="stripe.svg">
<defs
id="defs5506" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.2277778"
inkscape:cx="180"
inkscape:cy="74.899999"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
units="px"
inkscape:window-width="1280"
inkscape:window-height="744"
inkscape:window-x="-4"
inkscape:window-y="-4"
inkscape:window-maximized="1" />
<metadata
id="metadata5509">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(24.946428,-325.0345)">
<g
id="Stripe"
transform="matrix(0.26458333,0,0,0.26458333,-38.969344,315.77409)">
<path
id="path5516"
d="m 414,113.4 c 0,-25.6 -12.4,-45.8 -36.1,-45.8 -23.8,0 -38.2,20.2 -38.2,45.6 0,30.1 17,45.3 41.4,45.3 11.9,0 20.9,-2.7 27.7,-6.5 v -20 c -6.8,3.4 -14.6,5.5 -24.5,5.5 -9.7,0 -18.3,-3.4 -19.4,-15.2 h 48.9 c 0,-1.3 0.2,-6.5 0.2,-8.9 z m -49.4,-9.5 c 0,-11.3 6.9,-16 13.2,-16 6.1,0 12.6,4.7 12.6,16 z"
class="st0"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#6772e5;fill-rule:evenodd" />
<path
id="path5518"
d="m 301.1,67.6 c -9.8,0 -16.1,4.6 -19.6,7.8 l -1.3,-6.2 h -22 v 116.6 l 25,-5.3 0.1,-28.3 c 3.6,2.6 8.9,6.3 17.7,6.3 17.9,0 34.2,-14.4 34.2,-46.1 -0.1,-29 -16.6,-44.8 -34.1,-44.8 z m -6,68.9 c -5.9,0 -9.4,-2.1 -11.8,-4.7 l -0.1,-37.1 c 2.6,-2.9 6.2,-4.9 11.9,-4.9 9.1,0 15.4,10.2 15.4,23.3 0,13.4 -6.2,23.4 -15.4,23.4 z"
class="st0"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#6772e5;fill-rule:evenodd" />
<polygon
id="polygon5520"
points="248.9,56.3 248.9,36 223.8,41.3 223.8,61.7 "
class="st0"
style="clip-rule:evenodd;fill:#6772e5;fill-rule:evenodd" />
<rect
id="rect5522"
height="87.5"
width="25.1"
class="st0"
y="69.300003"
x="223.8"
style="clip-rule:evenodd;fill:#6772e5;fill-rule:evenodd" />
<path
id="path5524"
d="m 196.9,76.7 -1.6,-7.4 h -21.6 v 87.5 h 25 V 97.5 c 5.9,-7.7 15.9,-6.3 19,-5.2 v -23 c -3.2,-1.2 -14.9,-3.4 -20.8,7.4 z"
class="st0"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#6772e5;fill-rule:evenodd" />
<path
id="path5526"
d="m 146.9,47.6 -24.4,5.2 -0.1,80.1 c 0,14.8 11.1,25.7 25.9,25.7 8.2,0 14.2,-1.5 17.5,-3.3 V 135 c -3.2,1.3 -19,5.9 -19,-8.9 V 90.6 h 19 V 69.3 h -19 z"
class="st0"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#6772e5;fill-rule:evenodd" />
<path
id="path5528"
d="m 79.3,94.7 c 0,-3.9 3.2,-5.4 8.5,-5.4 7.6,0 17.2,2.3 24.8,6.4 V 72.2 C 104.3,68.9 96.1,67.6 87.8,67.6 67.5,67.6 54,78.2 54,95.9 c 0,27.6 38,23.2 38,35.1 0,4.6 -4,6.1 -9.6,6.1 -8.3,0 -18.9,-3.4 -27.3,-8 v 23.8 c 9.3,4 18.7,5.7 27.3,5.7 20.8,0 35.1,-10.3 35.1,-28.2 -0.1,-29.8 -38.2,-24.5 -38.2,-35.7 z"
class="st0"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#6772e5;fill-rule:evenodd" />
</g>
</g>
<style
id="style5514"
type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#6772E5;}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

3
packages/nc-gui-v2/assets/style-v2.scss

@ -61,3 +61,6 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
--header-height: 64px;
}
html {
overflow-y: auto !important;
}

475
packages/nc-gui-v2/assets/style.css

@ -1,83 +1,3 @@
.shake-btn {
color: red !important;
animation: shake .4s;
/* When the animation is finished, start again */
animation-iteration-count: infinite;
}
@keyframes shake {
0% {
transform: rotate(2deg);
}
25% {
transform: rotate(1.5deg);
}
50% {
transform: rotate(0deg);
}
75% {
transform: rotate(-1.5deg);
}
100% {
transform: rotate(-2deg);
}
}
.v-treeview--dense .v-treeview-node__root {
min-height: 22px !important;
}
.sortable-drag {
border: 2px solid var(--v-backgroundColor-base) !important;
border-radius: 2px;
}
.v-treeview--dense .v-treeview-node {
/*margin-left: 12px !important;*/
}
/**::-webkit-scrollbar {*/
/* width: .5em !important;*/
/* border-radius: .2rem !important;*/
/*}*/
/**::-webkit-scrollbar-track {*/
/* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3) !important;*/
/*}*/
/**::-webkit-scrollbar-thumb {*/
/* background-color: darkgrey !important;*/
/* outline: 1px solid slategrey !important;*/
/*}*/
/* Let's get this party started */
/*::-webkit-scrollbar {*/
/* width: 8px;*/
/*}*/
/*!* Track *!*/
/*::-webkit-scrollbar-track {*/
/* -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);*/
/* -webkit-border-radius: 8px;*/
/* border-radius: 8px;*/
/*}*/
/*!* Handle *!*/
/*::-webkit-scrollbar-thumb {*/
/* !*border: 5px solid #00000000;*!*/
/* -webkit-border-radius: 8px;*/
/* border-radius: 8px;*/
/* background: rgba(30, 30, 30, 0.4);*/
/* -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);*/
/*}*/
/*::-webkit-scrollbar-thumb:window-inactive {*/
/* background: rgba(30, 30, 30, 0.4);*/
/*}*/
::-webkit-scrollbar {
width: .7em;
height: .7em
@ -97,398 +17,3 @@
border: .15em solid #00000000;
background-clip: padding-box;
}
tbody tr:nth-of-type(odd) {
/*background-color: rgba(0, 0, 0, .1);*/
}
/*.multipane.foo.layout-v .multipane-resizer {*/
/* margin: 0; left: 0; !* reset default styling *!*/
/* width: 15px;*/
/* background: blue;*/
/*}*/
/*.multipane.foo.layout-h .multipane-resizer {*/
/* margin: 0; top: 0; !* reset default styling *!*/
/* height: 15px;*/
/* background: blue;*/
/*}*/
/*.splitpanes--vertical > .splitpanes__splitter {*/
/* width: 10px;*/
/* background: black;*/
/*}*/
/*.splitpanes--horizontal > .splitpanes__splitter {*/
/* height: 10px;*/
/* background: black;*/
/* position: relative;*/
/*}*/
/* sql output table footer */
.small-footer .v-data-footer__select .v-select {
margin: 0px 11px 5px 12px !important;
}
/*.splitpanes {*/
/* display: -webkit-box;*/
/* display: -ms-flexbox;*/
/* display: flex;*/
/* width: 100%;*/
/* height: 100%*/
/*}*/
/*.splitpanes--vertical {*/
/* -webkit-box-orient: horizontal;*/
/* -webkit-box-direction: normal;*/
/* -ms-flex-direction: row;*/
/* flex-direction: row*/
/*}*/
/*.splitpanes--horizontal {*/
/* -webkit-box-orient: vertical;*/
/* -webkit-box-direction: normal;*/
/* -ms-flex-direction: column;*/
/* flex-direction: column*/
/*}*/
/*.splitpanes--dragging * {*/
/* -webkit-user-select: none;*/
/* -moz-user-select: none;*/
/* -ms-user-select: none;*/
/* user-select: none*/
/*}*/
/*.splitpanes__pane {*/
/* width: 100%;*/
/* height: 100%;*/
/* overflow: hidden;*/
/* -webkit-transition: width .2s ease-out, height .2s ease-out;*/
/* transition: width .2s ease-out, height .2s ease-out*/
/*}*/
/*.splitpanes--dragging .splitpanes__pane {*/
/* -webkit-transition: none;*/
/* transition: none*/
/*}*/
/*.splitpanes__splitter {*/
/* -ms-touch-action: none;*/
/* touch-action: none*/
/*}*/
/*.splitpanes--vertical > .splitpanes__splitter {*/
/* min-width: 1px;*/
/* cursor: col-resize*/
/*}*/
/*.splitpanes--horizontal > .splitpanes__splitter {*/
/* min-height: 1px;*/
/* cursor: row-resize*/
/*}*/
.splitpanes.xc-theme .splitpanes__pane {
/*background-color: #f2f2f2*/
}
.splitpanes.xc-theme .splitpanes__splitter {
background-color: dimgrey;
-webkit-box-sizing: border-box;
box-sizing: border-box;
position: relative
}
/*.splitpanes.xc-theme .splitpanes__splitter:after, .splitpanes.xc-theme .splitpanes__splitter:before {*/
/* content: "";*/
/* position: absolute;*/
/* top: 50%;*/
/* left: 50%;*/
/* background-color: rgba(255,255,255, 1);*/
/* -webkit-transition: background-color .3s;*/
/* transition: background-color .3s*/
/*}*/
/*.splitpanes.xc-theme .splitpanes__splitter:hover:after, .splitpanes.xc-theme .splitpanes__splitter:hover:before {*/
/* background-color: rgba(0, 0, 256, .5)*/
/*}*/
.xc-theme.splitpanes .splitpanes .splitpanes__splitter {
z-index: 1
}
.xc-theme.splitpanes--vertical > .splitpanes__splitter, .xc-theme .splitpanes--vertical > .splitpanes__splitter {
width: 2px;
border-left: 1px solid dimgrey;
margin-left: -1px
}
/*.xc-theme.splitpanes--vertical > .splitpanes__splitter:after, .xc-theme .splitpanes--vertical > .splitpanes__splitter:after, .xc-theme.splitpanes--vertical > .splitpanes__splitter:before, .xc-theme .splitpanes--vertical > .splitpanes__splitter:before {*/
/* -webkit-transform: translateY(-50%);*/
/* transform: translateY(-50%);*/
/* width: 1px;*/
/* height: 50px*/
/*}*/
/*.xc-theme.splitpanes--vertical > .splitpanes__splitter:before, .xc-theme .splitpanes--vertical > .splitpanes__splitter:before {*/
/* margin-left: -1px*/
/*}*/
/*.xc-theme.splitpanes--vertical > .splitpanes__splitter:after, .xc-theme .splitpanes--vertical > .splitpanes__splitter:after {*/
/* margin-left: -1px*/
/*}*/
.xc-theme.splitpanes--horizontal > .splitpanes__splitter, .xc-theme .splitpanes--horizontal > .splitpanes__splitter {
height: 2px;
border-top: 1px solid dimgrey;
margin-top: -1px
}
/*.xc-theme.splitpanes--horizontal > .splitpanes__splitter:after, .xc-theme .splitpanes--horizontal > .splitpanes__splitter:after, .xc-theme.splitpanes--horizontal > .splitpanes__splitter:before, .xc-theme .splitpanes--horizontal > .splitpanes__splitter:before {*/
/* -webkit-transform: translateX(-50%);*/
/* transform: translateX(-50%);*/
/* width: 50px;*/
/* height: 1px*/
/*}*/
/*.xc-theme.splitpanes--horizontal > .splitpanes__splitter:before, .xc-theme .splitpanes--horizontal > .splitpanes__splitter:before {*/
/* margin-top: -2px*/
/*}*/
/*.xc-theme.splitpanes--horizontal > .splitpanes__splitter:after, .xc-theme .splitpanes--horizontal > .splitpanes__splitter:after {*/
/* margin-top: -2px*/
/*}*/
/*sql editor multipane*/
.project-tabs > .v-tabs-items > .v-window__container, .project-tabs, .project-container, .sql-editor-tab .v-window__container {
height: 100%;
overflow: auto;
}
.project-tabs > .v-tabs-items {
height: calc(100% - 30px);
overflow: auto;
}
.table-tabs:not(.hidden-tab) > .v-tabs-items {
height: calc(100% - 30px);
overflow: auto;
}
.table-tabs.hidden-tab > .v-tabs-items {
height: 100%;
overflow: auto;
}
.table-tabs > .v-tabs-items > .v-window__container {
height: 100%;
overflow: auto;
}
.action-menu-item {
height: 1rem !important;
}
.action-label.separator {
padding-top: 2px !important;
margin-bottom: 2px !important;
}
/*.monaco-single-line .view-line{*/
/*padding: 16px 0;*/
/*}*/
/*.monaco-single-line .cursor{*/
/*top: 16px !important;*/
/*}*/
/*.monaco-single-line .selected-text{*/
/*top: 16px !important;*/
/*}*/
/*.monaco-single-line .view-overlays > div{*/
/*top: 16px !important;*/
/*}*/
.monaco-single-line .cursor, .monaco-single-line .view-overlays > div {
height: 14px !important;
top: 17px !important;
}
.monaco-single-line .selected-text,
.monaco-single-line .selectionHighlight {
height: 14px !important;
}
.monaco-single-line * {
border: none !important;
}
.monaco-single-line .margin,
.monaco-single-line .margin * {
width: 0 !important;
}
.monaco-single-line .decorationsOverviewRuler {
display: none;
}
.monaco-single-line .scrollbar.horizontal {
display: none;
}
/*.monaco-single-line .monaco-scrollable-element*/
/*{*/
/* left:0 !important;*/
/* padding-left: 27px;*/
/*}*/
.params-table .v-data-table__wrapper {
overflow: visible
}
.toolbar-border-bottom {
border-bottom: 1px solid #7F828B33 !important;
}
.v-step {
z-index: 1000
}
:focus {
outline: none;
}
td .v-input--selection-controls {
margin-top: 0;
}
.d-100 {
width: 100%;
}
.h-100 {
height: 100%;
}
.scroll-auto {
overflow: auto;
}
.xc-tabs .v-tabs-bar {
border-bottom: solid 1px var(--v-primary-lighten2);
}
.xc-tabs .v-tab {
border-right: 1px solid var(--v-primary-lighten2);
}
.xc-border-right {
border-right: 1px solid #7f828b33;
}
.xc-border-bottom {
border-bottom: 1px solid #7f828b33;
}
.xc-border {
border: 1px solid #7f828b33;
}
/*.v-tooltip__content{*/
/* z-index: 99 !important;*/
/*}*/
.theme--dark.v-tabs > .v-tabs-bar .v-tab:not(.v-tab--active) > div > .v-icon {
color: rgba(255, 255, 255, 0.6);
}
/* text input bottom line */
.theme--light.v-text-field > .v-input__control > .v-input__slot:before {
border-color: rgba(0, 0, 0, 0.2) !important;
}
table .v-input__control {
height: auto !important;
}
.v-input__control label {
font-size: inherit;
}
input, textarea, select {
color: var(--v-textColor-base);
}
.advanced-border {
border: 1px dotted pink !important;
border-radius: 4px;
}
/* Toast css */
.toasted .primary, .toasted.toasted-primary {
font-family: "Roboto", sans-serif !important;
font-weight: 400 !important;
}
body.light .toasted .primary.error, body.light .toasted.toasted-primary.error {
background: #ffa2a2 !important;
color: black;
border-radius: 19px;
}
body.light .toasted .primary.success, body.light .toasted.toasted-primary.success {
background: #d1f7c4 !important;
color: black;
border-radius: 19px;
}
body.light .toasted .primary.info, body.light .toasted.toasted-primary.info {
background: #98befa !important;
color: black;
border-radius: 19px;
}
body.dark .toasted .primary.error, body.dark .toasted.toasted-primary.error {
background: #b30000 !important;
border-radius: 19px;
}
body.dark .toasted .primary.success, body.dark .toasted.toasted-primary.success {
background: #258300 !important;
border-radius: 19px;
}
body.dark .toasted .primary.info, body.dark .toasted.toasted-primary.info {
background: #0040bc !important;
border-radius: 19px;
}
.v-date-picker-table {
height: auto !important;
}
.nc-remove-border{
border: none !important;
}

2
packages/nc-gui-v2/assets/style/app.styl

@ -1,2 +0,0 @@
// Import Vuetify styling
//@require '~vuetify/src/stylus/app.styl'

367
packages/nc-gui-v2/assets/style/style.css

@ -1,367 +0,0 @@
.blink_me {
animation: blinker 1.5s linear infinite;
}
body {
color: darkgrey;
}
@keyframes blinker {
60% {
opacity: 0;
}
}
table.v-table tbody td,
table.v-table tbody th,
table.v-table tbody tr {
height: 38px !important;
}
table .v-input__control {
height: 20px !important;
}
.shortkey {
text-decoration: none;
border-bottom: 1px solid orange;
}
table .v-text-field > .v-input__control > .v-input__slot:before {
border-style: none;
}
table .v-text-field > .v-input__control > .v-input__slot:after {
border-style: none;
}
table.v-table thead column,
th,
tr {
height: 38px !important;
font-size: 15px !important;
}
/* tabs height css start */
.project-tabs .v-tabs__container {
max-height: 33px !important;
}
.project-tabs .v-tabs__item,
.v-tabs__div {
max-height: 40px !important;
font-weight: bold !important;
}
.table-tabs .v-tabs__container {
max-height: 32px !important;
}
.table-tabs .v-tabs__item,
.v-tabs__div {
max-height: 32px !important;
font-weight: 500 !important;
font-size: 12px !important;
}
/* tabs height css end */
.divider {
/*border-right: 1px solid #b3d4fc;*/
}
.divider:last-child {
/*border-right: 0;*/
}
.project-container {
/*min-height: calc(100vh - (408px + 1em));*/
/*max-height: calc(100vh - (408px + 1em));*/
overflow-y: auto;
}
.project-logs {
/* position: fixed; */
/* bottom: 0; */
/* right: 0; */
/*min-height: 250px !important;*/
/*max-height: 250px !important;*/
/*overflow-y: auto;*/
/* width: calc(100vw - (500px + 1em)); */
/*border: 2px solid dimgrey;*/
}
.new-project {
/* position: fixed; */
/* bottom: 0; */
/* right: 0; */
/*min-height: 250px !important;*/
max-height: 750px !important;
overflow-y: auto;
/* width: calc(100vw - (500px + 1em)); */
/*border: 1px solid dimgrey;*/
}
.selected-table-row {
background: purple;
}
html {
overflow: hidden;
}
[v-cloak] {
display: none !important;
}
.v-btn {
margin: 0 2px;
}
html {
overflow-y: auto !important;
}
/* nested expansion panel bug */
.v-expansion-panel-header {
min-height: 48px !important;
}
.v-expansion-panel--active > .v-expansion-panel-header {
min-height: 64px !important;
}
/* Table validation message */
/*table .v-input__control {*/
/* position: relative;*/
/*}*/
/*table .v-input__control > .v-text-field__details {*/
/* position: absolute;*/
/* bottom: -4px;*/
/*}*/
/*table edit dialog buttons*/
.v-small-dialog__actions button span {
font-weight: bolder;
}
/*resizer*/
.vertical-resizer {
/*width: 100%;*/
height: 5px;
background: grey;
cursor: ns-resize;
position: relative;
}
.vertical-resizer + * {
overflow-y: auto !important;
}
.horizontal-resizer {
/*width:5px !important;*/
/*background: grey !important;*/
/*cursor: ew-resize;*/
}
.pri-text-color {
color: #616161;
}
.free-gradient {
background: linear-gradient(70deg, black, dimgrey);
}
.premium-gradient {
background: linear-gradient(70deg, darkblue, cornflowerblue);
}
.platinum-gradient {
background: linear-gradient(70deg, orangered, orange);
}
.blue-gradient {
background: linear-gradient(70deg, royalblue, lightskyblue);
}
.green-gradient {
background: linear-gradient(70deg, limegreen, darkseagreen);
}
.pink-gradient {
background: linear-gradient(70deg, hotpink, lightpink);
}
.purple-gradient {
background: linear-gradient(70deg, rebeccapurple, mediumpurple);
}
.orange-gradient {
background: linear-gradient(70deg, darkorange, orange);
}
.black-gradient {
background: linear-gradient(180deg, black, rgb(55, 55, 55));
}
.yellow-orange-gradient {
background: -moz-linear-gradient(left, rgba(241, 231, 103, 1) 0%, rgba(254, 182, 69, 1) 100%);
background: -webkit-linear-gradient(left, rgba(241, 231, 103, 1) 0%, rgba(254, 182, 69, 1) 100%);
background: linear-gradient(to right, rgba(241, 231, 103, 1) 0%, rgba(254, 182, 69, 1) 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f1e767', endColorstr='#feb645', GradientType=1);
}
.white-gradient {
background: linear-gradient(45deg, white, lightskyblue);
}
.banner-bar {
background: linear-gradient(180deg, #4185f4, #4185f4);
}
.banner-gradient {
background: linear-gradient(180deg, #4185f4, #4185f4);
}
.home-cta {
background: linear-gradient(70deg, #ff8319, #ff8319);
}
.home-cta-text {
background: linear-gradient(70deg, #fcbb07, #fcbb07);
}
.img-animation {
animation-name: vibrate;
animation-duration: 1.5s;
animation-timing-function: ease-out;
animation-delay: 0s;
animation-direction: alternate;
animation-iteration-count: infinite;
animation-fill-mode: none;
animation-play-state: running;
}
@keyframes vibrate {
0% {
transform: translateY(10px);
}
100% {
transform: translateY(0);
}
}
.search-overlay .v-overlay__content {
height: 100%;
width: 100%;
}
/* Draggable tree hover color */
.theme--dark .vtl-tree-node:hover {
background-color: rgb(94, 94, 94) !important;
}
.theme--light .vtl-tree-node:hover {
background-color: rgb(240, 240, 240) !important;
}
/*vue tree list icon font */
/*.sql-query-treeview .vtl-icon-folder:before {*/
/* vertical-align: middle;*/
/* font-family: "Material Design Icons" ;*/
/* content: "\FB7B" !important;*/
/*}*/
.api-treeview .vtl-icon-file:before {
display: none;
}
/*.sql-query-treeview .vtl-icon-file:before {*/
/* vertical-align: middle;*/
/* font-family: "Material Design Icons" ;*/
/* content: "\F0C3" !important;*/
/* color: orange;*/
/*}*/
.sql-query-treeview .vtl-tree-node .vtl-caret{
width:1rem;
}
.sql-query-treeview .vtl-tree-node{
padding-top: 5px !important;
padding-bottom: 5px !important;
}
.sql-query-treeview .custom-run-icon{}
.sql-editor .v-expansion-panel-header {
min-height: 36px !important;
padding: 0 0 0 10px;
}
.sql-editor .v-expansion-panel-header--active {
min-height: 40px !important;
}
.api-client .v-expansion-panel-header {
min-height: 36px !important;
padding: 0 0 0 10px;
}
.api-client .v-expansion-panel-header--active {
min-height: 40px !important;
}
.api-treeview .vtl-node-content{
max-width: calc(100% - 44px);
}
/* for expansion panel content wrapper */
.expansion-wrap-0 .v-expansion-panel-content__wrap{
padding: 0;
}
.sql-query-treeview .vtl-operation{
margin-left: 2px !important;;
}
.sql-query-treeview .vtl-tree-node{
display: flex;
}
.sql-query-treeview .vtl-node-content{
flex-grow: 1;
}
.ignore-height-style table .v-input__control {
height: auto !important;
}
.cursor-pointer{
cursor: pointer;
}
/* sorting and filter */
.menu-filter-dropdown {
max-height: 500px;
overflow-y: auto;
}

1
packages/nc-gui-v2/assets/style/variables.styl

@ -1 +0,0 @@
//@require '~vuetify/src/stylus/settings/_variables.styl'

1
packages/nc-gui-v2/components.d.ts vendored

@ -8,6 +8,7 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AAnchorLink: typeof import('ant-design-vue/es')['AnchorLink']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']

307
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -1,31 +1,298 @@
<script setup lang="ts">
import { useProject, useTabs } from '#imports'
import { computed } from '@vue/reactivity'
import { Modal } from 'ant-design-vue'
import { UITypes } from 'nocodb-sdk'
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { useToast } from 'vue-toastification'
import { watchEffect } from '#imports'
import { useNuxtApp, useRoute } from '#app'
import useProject from '~/composables/useProject'
import useTabs from '~/composables/useTabs'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiSettingIcon from '~icons/mdi/cog'
import MdiTable from '~icons/mdi/table'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
import MdiMenuDown from '~icons/mdi/chevron-down'
import MdiPlus from '~icons/mdi/plus-circle-outline'
import MdiDrag from '~icons/mdi/drag-vertical'
import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiAPIDocIcon from '~icons/mdi/open-in-new'
const { tables } = useProject()
const { addTab } = useTabs()
const toast = useToast()
const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const route = useRoute()
const { tables, loadTables } = useProject(route.params.projectId as string)
const { closeTab } = useTabs()
const tablesById = $computed<Record<string, TableType>>(() =>
tables?.value?.reduce((acc: Record<string, TableType>, table: TableType) => {
acc[table.id as string] = table
return acc
}, {}),
)
const settingsDlg = ref(false)
const showTableList = ref(true)
const tableCreateDlg = ref(false)
const tableDeleteDlg = ref(false)
const menuRef = $ref<HTMLLIElement>()
let key = $ref(0)
let sortable: Sortable
// todo: replace with vuedraggable
const initSortable = (el: Element) => {
if (sortable) sortable.destroy()
sortable = Sortable.create(el as HTMLLIElement, {
handle: '.nc-drag-icon',
onEnd: async (evt) => {
const { newIndex = 0, oldIndex = 0 } = evt
const itemEl = evt.item as HTMLLIElement
const item = tablesById[itemEl.dataset.id as string]
// get the html collection of all list items
const children: HTMLCollection = evt.to.children
// get items before and after the moved item
const itemBeforeEl = children[newIndex - 1] as HTMLLIElement
const itemAfterEl = children[newIndex + 1] as HTMLLIElement
// get items meta of before and after the moved item
const itemBefore = itemBeforeEl && tablesById[itemBeforeEl.dataset.id as string]
const itemAfter = itemAfterEl && tablesById[itemAfterEl.dataset.id as string]
// set new order value based on the new order of the items
if (children.length - 1 === evt.newIndex) {
item.order = (itemBefore.order as number) + 1
} else if (newIndex === 0) {
item.order = (itemAfter.order as number) / 2
} else {
item.order = ((itemBefore.order as number) + (itemAfter.order as number)) / 2
}
// update the order of the moved item
tables.value?.splice(newIndex, 0, ...tables.value?.splice(oldIndex, 1))
// force re-render the list
key++
// update the item order
await $api.dbTable.reorder(item.id as string, {
order: item.order as any,
})
},
animation: 150,
})
}
watchEffect(() => {
if (menuRef) {
initSortable(menuRef)
}
})
const icon = (table: TableType) => {
if (table.type === 'table') {
return MdiTableLarge
}
if (table.type === 'view') {
return MdiView
}
}
const apiLink = computed(
() =>
// new URL(
`/api/v1/db/meta/projects/${route.params.projectId}/swagger`,
// todo: get siteUrl
// this.$store.state.project.appInfo && this.$store.state.project.appInfo.ncSiteUrl
// ),
)
const filterQuery = $ref('')
const filteredTables = $computed(() => {
return tables?.value?.filter((table) => !filterQuery || table?.title.toLowerCase()?.includes(filterQuery.toLowerCase()))
})
const contextMenuTarget = reactive<{ type?: 'table' | 'main'; value?: any }>({})
const setMenuContext = (type: 'table' | 'main', value?: any) => {
contextMenuTarget.type = type
contextMenuTarget.value = value
$e('c:table:create:navdraw:right-click')
}
const deleteTable = (table: TableType) => {
$e('c:table:delete')
// 'Click Submit to Delete The table'
Modal.confirm({
title: `Click Yes to Delete The table : ${table.title}`,
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
async onOk() {
const { getMeta, removeMeta } = useMetas()
try {
const meta = (await getMeta(table.id as string)) as TableType
const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord)
if (relationColumns?.length) {
const refColMsgs = await Promise.all(
relationColumns.map(async (c, i) => {
const refMeta = (await getMeta(
(c?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id as string,
)) as TableType
return `${i + 1}. ${c.title} is a LinkToAnotherRecord of ${(refMeta && refMeta.title) || c.title}`
}),
)
toast.info(
h('div', {
innerHTML: `<div style="padding:10px 4px">Unable to delete tables because of the following.
<br><br>${refColMsgs.join('<br>')}<br><br>
Delete them & try again</div>`,
}),
)
return
}
await $api.dbTable.delete(table?.id as string)
closeTab({
type: 'table',
id: table.id,
title: table.title,
})
await loadTables()
removeMeta(table.id as string)
toast.info(`Deleted table ${table.title} successfully`)
$e('a:table:delete')
} catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e))
}
},
})
}
const renameTableDlg = ref(false)
const renameTableMeta = ref()
const showRenameTableDlg = (table: TableType, rightClick = false) => {
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
renameTableMeta.value = table
renameTableDlg.value = true
}
const reloadTables = async () => {
$e('a:table:refresh:navdraw')
await loadTables()
}
const addTableTab = (table: TableType) => {
$e('a:table:open')
addTab({ title: table.title, id: table.id })
}
</script>
<template>
<div class="nc-treeview-container flex flex-column">
<a-menu class="flex-1 overflow-y-auto">
<a-menu-item
v-for="table in tables"
:key="table.id"
class="p-2 text-sm pointer"
@click="addTab({ type: 'table', title: table.title, id: table.id })"
>
{{ table.title }}
</a-menu-item>
</a-menu>
<div class="cursor-pointer nc-team-settings pa-4 flex align-center hover:bg-gray-200/20" @click="settingsDlg = true">
<div class="p-1">
<a-input-search
v-model:value="filterQuery"
size="small"
class="nc-filter-input"
:placeholder="$t('placeholder.searchProjectTree')"
/>
</div>
<a-dropdown :trigger="['contextmenu']">
<div class="p-1 flex-1 overflow-y-auto flex flex-column">
<div
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@click="showTableList = !showTableList"
@contextmenu="setMenuContext('main')"
>
<MdiTable class="mr-1 text-gray-500" />
<span class="flex-grow text-bold"
>{{ $t('objects.tables') }} <template v-if="tables?.length">({{ tables.length }})</template></span
>
<MdiPlus v-t="['c:table:create:navdraw']" class="text-gray-500" @click.stop="tableCreateDlg = true" />
<MdiMenuDown
class="transition-transform !duration-100 text-gray-500"
:class="{ 'transform rotate-180': showTableList }"
/>
</div>
<div class="flex-1">
<div class="transition-height duration-200 overflow-hidden" :class="{ 'h-100': showTableList, 'h-0': !showTableList }">
<div :key="key" ref="menuRef" class="border-none sortable-list">
<div
v-for="table in tables"
:key="table.id"
v-t="['a:table:open']"
:class="{ hidden: !filteredTables?.includes(table) }"
class="!pl-1 py-1 !h-[28px] !my-0 text-sm pointer group"
:data-order="table.order"
:data-id="table.id"
@click="addTableTab(table)"
>
<div class="flex align-center gap-1 h-full" @contextmenu="setMenuContext('table', table)">
<MdiDrag class="transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 nc-drag-icon cursor-move" />
<component :is="icon(table)" class="text-[10px] text-gray-500" />
<span class="text-xs flex-1 ml-2">{{ table.title }}</span>
<a-dropdown :trigger="['click']" @click.stop>
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
<a-menu class="cursor-pointer">
<a-menu-item class="!text-xs" @click="showRenameTableDlg(table)"> Rename </a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</div>
</div>
</div>
</div>
<template #overlay>
<a-menu class="cursor-pointer">
<template v-if="contextMenuTarget.type === 'table'">
<a-menu-item class="!text-xs" @click="showRenameTableDlg(contextMenuTarget.value)">
{{ $t('general.rename') }}
</a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(contextMenuTarget.value)">
{{ $t('general.delete') }}
</a-menu-item>
</template>
<template v-else>
<a-menu-item class="!text-xs" @click="reloadTables">
{{ $t('general.reload') }}
</a-menu-item>
</template>
</a-menu>
</template>
</a-dropdown>
<div class="w-full h-[1px] bg-gray-200" />
<a v-if="isUIAllowed('apiDocs')" v-t="['e:api-docs']" class="nc-treeview-footer-item" :href="apiLink" target="_blank">
<MdiAPIDocIcon class="mr-2" />
<span> {{ $t('title.apiDocs') }}</span>
</a>
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-treeview-footer-item nc-team-settings"
@click="settingsDlg = true"
>
<MdiSettingIcon class="mr-2" />
<span> {{ $t('title.teamAndSettings') }}</span>
</div>
<a-modal v-model:visible="settingsDlg" width="max(90vw, 600px)"> Team and settings </a-modal>
<a-modal v-model:visible="settingsDlg" width="max(90vw, 600px)"> Team and settings</a-modal>
<DlgTableCreate v-model="tableCreateDlg" />
<DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" />
</div>
</template>
@ -37,4 +304,16 @@ const settingsDlg = ref(false)
.nc-treeview-container {
height: calc(100vh - var(--header-height));
}
.nc-treeview-footer-item {
@apply cursor-pointer px-4 py-2 flex align-center hover:bg-gray-200/20 text-xs text-current;
}
:deep(.nc-filter-input input::placeholder) {
@apply !text-xs;
}
:deep(.ant-dropdown-menu-title-content) {
@apply !p-2;
}
</style>

121
packages/nc-gui-v2/components/dlg/TableRename.vue

@ -0,0 +1,121 @@
<script setup lang="ts">
import { watchEffect } from '@vue/runtime-core'
import { Form } from 'ant-design-vue'
import type { TableType } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { useProject, useTableCreate, useTabs } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { validateTableName } from '~/utils/validation'
import { useNuxtApp } from '#app'
interface Props {
modelValue?: boolean
tableMeta: TableType
}
const { modelValue = false, tableMeta } = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'updated'])
const { $e, $api } = useNuxtApp()
const toast = useToast()
const dialogShow = computed({
get() {
return modelValue
},
set(v) {
emit('update:modelValue', v)
},
})
const { updateTab } = useTabs()
const { loadTables } = useProject()
const { project, tables } = useProject()
const prefix = computed(() => project?.value?.prefix || '')
const inputEl = $ref<any>()
let loading = $ref(false)
const useForm = Form.useForm
const formState = reactive({
title: '',
})
const validators = computed(() => {
return {
title: [
validateTableName,
{
validator: (rule: any, value: any, callback: (errMsg?: string) => void) => {
if (/^\s+|\s+$/.test(value)) {
callback('Leading or trailing whitespace not allowed in table name')
}
if (
!(tables?.value || []).every(
(t) => t.id === tableMeta.id || t.table_name.toLowerCase() !== (value || '').toLowerCase(),
)
) {
callback('Duplicate table alias')
}
callback()
},
},
],
}
})
const { resetFields, validate, validateInfos } = useForm(formState, validators)
watchEffect(() => {
if (tableMeta?.title) formState.title = tableMeta?.title
// todo: replace setTimeout and follow better approach
nextTick(() => {
const input = inputEl?.$el
input.setSelectionRange(0, formState.title.length)
input.focus()
})
})
const renameTable = async () => {
loading = true
try {
await $api.dbTable.update(tableMeta?.id as string, {
title: formState.title,
})
dialogShow.value = false
loadTables()
updateTab({ id: tableMeta?.id }, { title: formState.title })
toast.success('Table renamed successfully')
$e('a:table:rename')
dialogShow.value = false
} catch (e) {
toast.error(await extractSdkResponseErrorMsg(e))
}
loading = false
}
</script>
<template>
<a-modal
v-model:visible="dialogShow"
:title="$t('activity.renameTable')"
@keydown.esc="dialogShow = false"
@finish="renameTable"
>
<template #footer>
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" type="primary" :loading="loading" @click="renameTable">{{ $t('general.submit') }}</a-button>
</template>
<div class="pl-10 pr-10 pt-5">
<a-form :model="formState" name="create-new-table-form">
<!-- hint="Enter table name" -->
<div class="mb-2">{{ $t('msg.info.enterTableName') }}</div>
<a-form-item v-bind="validateInfos.title">
<a-input
ref="inputEl"
v-model:value="formState.title"
hide-details
:placeholder="$t('msg.info.enterTableName')"
@keydown.enter="renameTable"
/>
</a-form-item>
</a-form>
</div>
</a-modal>
</template>

475
packages/nc-gui-v2/components/template/Editor.vue

@ -0,0 +1,475 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { Form } from 'ant-design-vue'
import { tableColumns } from './utils'
import { computed, onMounted } from '#imports'
import MdiTableIcon from '~icons/mdi/table'
import MdiStringIcon from '~icons/mdi/alpha-a'
import MdiLongTextIcon from '~icons/mdi/text'
import MdiNumericIcon from '~icons/mdi/numeric'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiKeyStarIcon from '~icons/mdi/key-star'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { fieldRequiredValidator } from '~/utils/validation'
interface Props {
quickImportType: 'csv' | 'excel' | 'json'
projectTemplate: Record<string, any>
importData: any[]
}
interface Option {
label: string
value: string
}
const { quickImportType, projectTemplate, importData } = defineProps<Props>()
const useForm = Form.useForm
const { $api } = useNuxtApp()
const hasSelectColumn = ref<boolean[]>([])
const expansionPanel = ref<number[]>([])
const editableTn = ref<boolean[]>([])
const inputRefs = ref<HTMLInputElement[]>([])
const isImporting = ref(false)
const importingTip = ref('Importing')
const uiTypeOptions = ref<Option[]>(
(Object.keys(UITypes) as (keyof typeof UITypes)[])
.filter(
(uiType) =>
!isVirtualCol(UITypes[uiType]) &&
![UITypes.ForeignKey, UITypes.ID, UITypes.CreateTime, UITypes.LastModifiedTime, UITypes.Barcode, UITypes.Button].includes(
UITypes[uiType],
),
)
.map<Option>((uiType) => ({
value: uiType,
label: uiType,
})),
)
const data = reactive<{ title: string | null; name: string; tables: TableType[] }>({
title: null,
name: 'Project Name',
tables: [],
})
const toast = useToast()
const { addTab } = useTabs()
const { sqlUi, project, loadTables } = useProject()
onMounted(() => {
parseAndLoadTemplate()
})
const validators = computed(() =>
data.tables.reduce<Record<string, [typeof fieldRequiredValidator]>>((acc, table, tableIdx) => {
acc[`tables.${tableIdx}.table_name`] = [fieldRequiredValidator]
hasSelectColumn.value[tableIdx] = false
table.columns?.forEach((column, columnIdx) => {
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [fieldRequiredValidator]
acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator]
if (isSelect(column)) {
hasSelectColumn.value[tableIdx] = true
}
})
return acc
}, {}),
)
const editorTitle = computed(() => `${quickImportType.toUpperCase()} Import: ${data.title}`)
const { validate, validateInfos } = useForm(data, validators)
function filterOption(input: string, option: Option) {
return option.value.toUpperCase().includes(input.toUpperCase())
}
function parseAndLoadTemplate() {
if (projectTemplate) {
parseTemplate(projectTemplate)
expansionPanel.value = Array.from({ length: data.tables.length || 0 }, (_, i) => i)
hasSelectColumn.value = Array.from({ length: data.tables.length || 0 }, () => false)
}
}
function parseTemplate({ tables = [], ...rest }: Props['projectTemplate']) {
const parsedTemplate = {
...rest,
tables: tables.map(({ v = [], columns = [], ...rest }) => ({
...rest,
columns: [
...columns.map((c: any, idx: number) => {
c.key = idx
return c
}),
...v.map((v: any) => ({
column_name: v.title,
ref_table_name: {
...v,
},
})),
],
})),
}
Object.assign(data, parsedTemplate)
}
function isSelect(col: ColumnType) {
return col.uidt === 'MultiSelect' || col.uidt === 'SingleSelect'
}
function deleteTable(tableIdx: number) {
data.tables.splice(tableIdx, 1)
}
function deleteTableColumn(tableIdx: number, columnIdx: number) {
data.tables[tableIdx].columns?.splice(columnIdx, 1)
}
function addNewColumnRow(table: Record<string, any>, uidt?: string) {
table.columns.push({
key: table.columns.length,
column_name: `title${table.columns.length + 1}`,
uidt,
})
nextTick(() => {
const input = inputRefs.value[table.columns.length - 1]
input.focus()
input.select()
})
}
function setEditableTn(tableIdx: number, val: boolean) {
editableTn.value[tableIdx] = val
}
function remapColNames(batchData: any[], columns: ColumnType[]) {
return batchData.map((data) =>
(columns || []).reduce(
(aggObj, col: Record<string, any>) => ({
...aggObj,
[col.column_name]: data[col.ref_column_name || col.column_name],
}),
{},
),
)
}
async function importTemplate() {
// check if form is valid
try {
await validate()
} catch (errorInfo) {
toast.error('Please fill all the required values')
isImporting.value = false
return
}
try {
isImporting.value = true
// tab info to be used to show the tab after successful import
const tab = {
id: '',
title: '',
}
// create tables
for (const table of data.tables) {
// enrich system fields if not provided
// e.g. id, created_at, updated_at
const systemColumns = sqlUi?.value.getNewTableColumns().filter((c: ColumnType) => c.column_name !== 'title')
for (const systemColumn of systemColumns) {
if (!table.columns?.some((c) => c.column_name?.toLowerCase() === systemColumn.column_name.toLowerCase())) {
table.columns?.push(systemColumn)
}
}
// set pk & rqd if ID is provided
if (table.columns) {
for (const column of table.columns) {
if (column.column_name?.toLowerCase() === 'id' && !('pk' in column)) {
column.pk = true
column.rqd = true
break
}
}
}
const tableMeta = await $api.dbTable.create(project?.value?.id as string, {
table_name: table.table_name,
// leave title empty to get a generated one based on table_name
title: '',
columns: table.columns,
})
table.title = tableMeta.title
// open the first table after import
if (tab.id === '' && tab.title === '') {
tab.id = tableMeta.id as string
tab.title = tableMeta.title as string
}
// set primary value
if (tableMeta?.columns?.[0]?.id) {
await $api.dbTableColumn.primaryColumnSet(tableMeta.columns[0].id as string)
}
}
// bulk imsert data
if (importData) {
let total = 0
let progress = 0
const offset = 500
const projectName = project.value.title as string
await Promise.all(
data.tables.map((table: Record<string, any>) =>
(async (tableMeta) => {
const data = importData[tableMeta.ref_table_name]
if (data) {
total += data.length
for (let i = 0; i < data.length; i += offset) {
importingTip.value = `Importing data to ${projectName}: ${progress}/${total} records`
const batchData = remapColNames(data.slice(i, i + offset), tableMeta.columns)
await $api.dbTableRow.bulkCreate('noco', projectName, tableMeta.table_title, batchData)
progress += batchData.length
}
}
})(table),
),
)
}
// reload table list
await loadTables()
addTab({
...tab,
type: 'table',
})
} catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e))
} finally {
// TODO: close dialog when the integration is ready
isImporting.value = false
}
}
</script>
<template>
<a-spin :spinning="isImporting" :tip="importingTip" size="large">
<a-card :title="editorTitle">
<template #extra>
<a-button type="primary" size="large" @click="importTemplate">
{{ $t('activity.import') }}
</a-button>
</template>
<a-form :model="data" name="template-editor-form">
<p v-if="data.tables && quickImportType === 'excel'" class="text-center">
{{ data.tables.length }} sheet{{ data.tables.length > 1 ? 's' : '' }}
available for import
</p>
<a-collapse
v-if="data.tables && data.tables.length"
v-model:activeKey="expansionPanel"
class="template-collapse"
accordion
>
<a-collapse-panel v-for="(table, tableIdx) in data.tables" :key="tableIdx">
<template #header>
<a-form-item v-if="editableTn[tableIdx]" v-bind="validateInfos[`tables.${tableIdx}.table_name`]" no-style>
<a-input
v-model:value="table.table_name"
size="large"
style="max-width: 300px"
hide-details
@click="(e) => e.stopPropagation()"
@blur="setEditableTn(tableIdx, false)"
@keydown.enter="setEditableTn(tableIdx, false)"
/>
</a-form-item>
<span
v-else
class="font-weight-bold text-lg flex items-center gap-2"
@click="$event.stopPropagation() && setEditableTn(tableIdx, true)"
>
<MdiTableIcon class="text-primary" />
{{ table.table_name }}
</span>
</template>
<template #extra>
<a-tooltip bottom>
<template #title>
<!-- TODO: i18n -->
<span>Delete Table</span>
</template>
<MdiDeleteOutlineIcon v-if="data.tables.length > 1" class="text-lg mr-8" @click.stop="deleteTable(tableIdx)" />
</a-tooltip>
</template>
<a-table
v-if="table.columns.length"
class="template-form"
row-class-name="template-form-row"
:data-source="table.columns"
:columns="tableColumns"
:pagination="false"
>
<template #headerCell="{ column }">
<template v-if="column.key === 'column_name'">
<span>
{{ $t('labels.columnName') }}
</span>
</template>
<template v-else-if="column.key === 'uidt'">
<span>
{{ $t('labels.columnType') }}
</span>
</template>
<template v-else-if="column.key === 'dtxp' && hasSelectColumn[tableIdx]">
<span>
<!-- TODO: i18n -->
Options
</span>
</template>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'column_name'">
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.columns.${record.key}.${column.key}`]">
<a-input
:ref="
(el) => {
inputRefs[record.key] = el
}
"
v-model:value="record.column_name"
size="large"
/>
</a-form-item>
</template>
<template v-else-if="column.key === 'uidt'">
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.columns.${record.key}.${column.key}`]">
<a-auto-complete
v-model:value="record.uidt"
size="large"
:options="uiTypeOptions"
:filter-option="filterOption"
style="width: 200px"
/>
</a-form-item>
</template>
<template v-else-if="column.key === 'dtxp'">
<a-form-item v-if="isSelect(record)">
<a-input v-model:value="record.dtxp" size="large" />
</a-form-item>
</template>
<template v-if="column.key === 'action'">
<a-tooltip v-if="record.key === 0">
<template #title>
<!-- TODO: i18n -->
<span>Primary Value</span>
</template>
<span class="mr-3">
<MdiKeyStarIcon class="text-lg" />
</span>
</a-tooltip>
<a-tooltip v-else>
<template #title>
<!-- TODO: i18n -->
<span>Delete Column</span>
</template>
<a-button type="text" @click="deleteTableColumn(tableIdx, record.key)">
<div class="flex items-center">
<MdiDeleteOutlineIcon class="text-lg" />
</div>
</a-button>
</a-tooltip>
</template>
</template>
</a-table>
<div class="text-center mt-5">
<a-tooltip bottom>
<template #title>
<!-- TODO: i18n -->
<span>Add Number Column</span>
</template>
<a-button @click="addNewColumnRow(table, 'Number')">
<div class="flex items-center">
<MdiNumericIcon class="text-lg" />
</div>
</a-button>
</a-tooltip>
<a-tooltip bottom>
<template #title>
<!-- TODO: i18n -->
<span>Add SingleLineText Column</span>
</template>
<a-button @click="addNewColumnRow(table, 'SingleLineText')">
<div class="flex items-center">
<MdiStringIcon class="text-lg" />
</div>
</a-button>
</a-tooltip>
<a-tooltip bottom>
<template #title>
<!-- TODO: i18n -->
<span>Add LongText Column</span>
</template>
<a-button @click="addNewColumnRow(table, 'LongText')">
<div class="flex items-center">
<MdiLongTextIcon class="text-lg" />
</div>
</a-button>
</a-tooltip>
<a-tooltip bottom>
<template #title>
<!-- TODO: i18n -->
<span>Add Other Column</span>
</template>
<a-button @click="addNewColumnRow(table)">
<div class="flex items-center">
<MdiPlusIcon class="text-lg" />
Column
</div>
</a-button>
</a-tooltip>
</div>
</a-collapse-panel>
</a-collapse>
</a-form>
</a-card>
</a-spin>
</template>
<style scoped lang="scss">
.template-collapse {
@apply bg-white;
}
.template-form {
:deep(.ant-table-thead) > tr > th {
@apply bg-white;
}
:deep(.template-form-row) > td {
@apply !pb-0;
}
}
</style>

25
packages/nc-gui-v2/components/template/utils.ts

@ -0,0 +1,25 @@
import type { ColumnGroupType } from 'ant-design-vue/es/table'
export const tableColumns: (Omit<ColumnGroupType<any>, 'children'> & { dataIndex?: string; name: string })[] = [
{
name: 'Column Name',
dataIndex: 'column_name',
key: 'column_name',
width: 250,
},
{
name: 'Column Type',
dataIndex: 'column_type',
key: 'uidt',
width: 250,
},
{
name: 'Select Option',
key: 'dtxp',
},
{
name: 'Action',
key: 'action',
align: 'right',
},
]

21
packages/nc-gui-v2/composables/useMetas.ts

@ -1,4 +1,4 @@
import type { TableType } from 'nocodb-sdk'
import type { TableInfoType, TableType } from 'nocodb-sdk'
import { useNuxtApp, useState } from '#app'
import { useProject } from '#imports'
@ -8,13 +8,13 @@ export default () => {
const metas = useState<{ [idOrTitle: string]: TableType | any }>('metas', () => ({}))
const getMeta = async (tableIdOrTitle: string, force = false) => {
if (!force && metas[tableIdOrTitle as keyof typeof metas]) return metas[tableIdOrTitle as keyof typeof metas]
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => {
if (!force && metas.value[tableIdOrTitle as string]) return metas.value[tableIdOrTitle as string]
const modelId = (tables.value.find((t) => t.title === tableIdOrTitle || t.id === tableIdOrTitle) || {}).id
if (!modelId) {
console.warn(`Table '${tableIdOrTitle}' is not found in the table list`)
return
return null
}
const model = await $api.dbTable.read(modelId)
@ -28,5 +28,16 @@ export default () => {
return model
}
return { getMeta, metas }
const clearAllMeta = () => {
metas.value = {}
}
const removeMeta = (idOrTitle: string) => {
const meta = metas.value[idOrTitle]
if (meta) {
delete metas.value[meta.id]
delete metas.value[meta.title]
}
}
return { getMeta, clearAllMeta, metas, removeMeta }
}

25
packages/nc-gui-v2/composables/useTabs.ts

@ -6,6 +6,13 @@ export interface TabItem {
id?: string
}
function getPredicate(key: Partial<TabItem>) {
return (tab: TabItem) =>
(!('id' in key) || tab.id === key.id) &&
(!('title' in key) || tab.title === key.id) &&
(!('type' in key) || tab.type === key.id)
}
export default () => {
const tabs = useState<TabItem[]>('tabs', () => [])
const activeTab = useState<number>('activeTab', () => 0)
@ -25,9 +32,21 @@ export default () => {
const clearTabs = () => {
tabs.value = []
}
const closeTab = (index: number) => {
tabs.value.splice(index, 1)
const closeTab = (key: number | Partial<TabItem>) => {
if (typeof key === 'number') tabs.value.splice(key, 1)
else {
const index = tabs.value.findIndex(getPredicate(key))
if (index > -1) tabs.value.splice(index, 1)
}
}
const updateTab = (key: number | Partial<TabItem>, newTabItemProps: Partial<TabItem>) => {
const tab = typeof key === 'number' ? tabs.value[key] : tabs.value.find(getPredicate(key))
if (tab) {
Object.assign(tab, newTabItemProps)
}
}
return { tabs, addTab, activeTab, clearTabs, closeTab }
return { tabs, addTab, activeTab, clearTabs, closeTab, updateTab }
}

8
packages/nc-gui-v2/lib/enums.ts

@ -4,7 +4,13 @@ export enum Role {
User = 'user',
}
export type Roles = Record<Role, boolean>
export enum ClientType {
MYSQL = 'mysql2',
MSSQL = 'mssql',
PG = 'pg',
SQLITE = 'sqlite3',
VITESS = 'vitess',
}
export enum Language {
de = 'Deutsch',

40
packages/nc-gui-v2/lib/types.ts

@ -1,5 +1,5 @@
import type { ComputedRef, ToRefs } from 'vue'
import type { Roles } from '~/lib/enums'
import type { Role } from './enums'
export interface User {
id: string
@ -33,40 +33,4 @@ export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State
export type GlobalState = Getters & Actions & ToRefs<ReadonlyState>
export enum ClientType {
MYSQL = 'mysql2',
MSSQL = 'mssql',
PG = 'pg',
SQLITE = 'sqlite3',
VITESS = 'vitess',
}
export interface ProjectCreateForm {
title: string
dataSource: {
client: ClientType
connection:
| {
host: string
database: string
user: string
password: string
port: number | string
ssl?: Record<string, string>
searchPath?: string[]
}
| {
client?: 'sqlite3'
database: string
connection?: {
filename?: string
}
useNullAsDefault?: boolean
}
}
inflection: {
inflectionColumn?: string
inflectionTable?: string
}
sslUse?: any
}
export type Roles = Record<Role, boolean>

1
packages/nc-gui-v2/nuxt.config.ts

@ -18,7 +18,6 @@ export default defineNuxtConfig({
'vuetify/lib/styles/main.sass',
'~/assets/style/fonts.css',
'~/assets/css/global.css',
'~/assets/style/style.css',
'~/assets/style.css',
'~/assets/style-v2.scss',
],

43937
packages/nc-gui-v2/package-lock.json generated

File diff suppressed because it is too large Load Diff

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

@ -20,6 +20,7 @@
"nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2",
"socket.io-client": "^4.5.1",
"sortablejs": "^1.15.0",
"unique-names-generator": "^4.7.1",
"vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5",
@ -32,6 +33,7 @@
"@iconify-json/mdi": "^1.1.25",
"@intlify/vite-plugin-vue-i18n": "^4.0.0",
"@types/papaparse": "^5.3.2",
"@types/sortablejs": "^1.13.0",
"@vitejs/plugin-vue": "^2.3.3",
"@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37",

69
packages/nc-gui-v2/pages/index/index.vue

@ -1,10 +1,10 @@
<script lang="ts" setup>
import {Modal} from 'ant-design-vue'
import type {ProjectType} from 'nocodb-sdk'
import {useToast} from 'vue-toastification'
import {navigateTo} from '#app'
import {computed, onMounted} from '#imports'
import {extractSdkResponseErrorMsg} from '~/utils/errorUtils'
import { Modal } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { navigateTo } from '#app'
import { computed, onMounted } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiDeleteOutline from '~icons/mdi/delete-outline'
import MdiEditOutline from '~icons/mdi/edit-outline'
@ -13,7 +13,7 @@ import MdiMenuDown from '~icons/mdi/menu-down'
import MdiPlus from '~icons/mdi/plus'
import MdiDatabaseOutline from '~icons/mdi/database-outline'
const {$api, $state, $e} = useNuxtApp()
const { $api, $state, $e } = useNuxtApp()
const toast = useToast()
const filterQuery = ref('')
@ -29,7 +29,7 @@ const loadProjects = async () => {
const filteredProjects = computed(() => {
return projects.value.filter(
(project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()),
(project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()),
)
})
@ -68,17 +68,17 @@ $state.sidebarOpen.value = false
<b>{{ $t('title.myProject') }}</b>
<MdiRefresh
v-t="['a:project:refresh']"
class="text-sm text-gray-500 hover:text-primary mt-1 cursor-pointer"
@click="loadProjects"
v-t="['a:project:refresh']"
class="text-sm text-gray-500 hover:text-primary mt-1 cursor-pointer"
@click="loadProjects"
></MdiRefresh>
</h1>
<div class="flex mb-6">
<a-input-search
v-model:value="filterQuery"
class="max-w-[200px] nc-project-page-search"
:placeholder="$t('activity.searchProject')"
v-model:value="filterQuery"
class="max-w-[200px] nc-project-page-search"
:placeholder="$t('activity.searchProject')"
></a-input-search>
<div class="flex-grow"></div>
@ -86,27 +86,27 @@ $state.sidebarOpen.value = false
<a-button class="nc-new-project-menu !shadow">
<div class="flex align-center">
{{ $t('title.newProj') }}
<MdiMenuDown class="menu-icon"/>
<MdiMenuDown class="menu-icon" />
</div>
</a-button>
<template #overlay>
<a-menu>
<div
v-t="['c:project:create:xcdb']"
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2 nc-create-xc-db-project"
@click="navigateTo('/project/create')"
v-t="['c:project:create:xcdb']"
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2 nc-create-xc-db-project"
@click="navigateTo('/project/create')"
>
<MdiPlus class="col-span-2 mr-1 mt-[1px] text-primary text-lg"/>
<MdiPlus class="col-span-2 mr-1 mt-[1px] text-primary text-lg" />
<div class="col-span-10 text-sm xl:text-md">{{ $t('activity.createProject') }}</div>
</div>
<div
v-t="['c:project:create:extdb']"
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2 nc-create-external-db-project"
@click="navigateTo('/project/create-external')"
v-t="['c:project:create:extdb']"
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2 nc-create-external-db-project"
@click="navigateTo('/project/create-external')"
>
<MdiDatabaseOutline class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg"/>
<div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')"/>
<MdiDatabaseOutline class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg" />
<div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')" />
</div>
</a-menu>
</template>
@ -114,12 +114,12 @@ $state.sidebarOpen.value = false
</div>
<div v-if="loading">
<a-skeleton/>
<a-skeleton />
</div>
<a-table
v-else
:custom-row="
v-else
:custom-row="
(record) => ({
onClick: () => {
$e('a:project:open')
@ -127,14 +127,13 @@ $state.sidebarOpen.value = false
},
})
"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
>
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text }">
<div class="capitalize !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap nc-project-row"
:title="text">
<div class="capitalize !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap nc-project-row" :title="text">
{{ text }}
</div>
</template>
@ -144,11 +143,11 @@ $state.sidebarOpen.value = false
<template #default="{ text, record }">
<div class="flex align-center">
<MdiEditOutline
v-t="['c:project:edit:rename']"
class="nc-action-btn"
@click.stop="navigateTo(`/project/${text}`)"
v-t="['c:project:edit:rename']"
class="nc-action-btn"
@click.stop="navigateTo(`/project/${text}`)"
/>
<MdiDeleteOutline class="nc-action-btn" @click.stop="deleteProject(record)"/>
<MdiDeleteOutline class="nc-action-btn" @click.stop="deleteProject(record)" />
</div>
</template>
</a-table-column>

3
packages/nc-gui-v2/pages/nc/[projectId].vue

@ -2,6 +2,7 @@
const route = useRoute()
const { loadProject, loadTables } = useProject(route.params.projectId as string)
const { clearTabs, addTab } = useTabs()
const { $state } = useNuxtApp()
addTab({ type: 'auth', title: 'Team & Auth' })
@ -17,6 +18,8 @@ watch(
}
},
)
$state.sidebarOpen.value = true
</script>
<template>

14
packages/nc-gui-v2/pages/project/index/create-external.vue

@ -310,7 +310,7 @@ onMounted(() => {
<template #title>
<span>{{ $t('tooltip.clientCert') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" size="small" @click="certFileInput.click()" class="shadow">
<a-button :disabled="!sslFilesRequired" size="small" class="shadow" @click="certFileInput.click()">
{{ $t('labels.clientCert') }}
</a-button>
</a-tooltip>
@ -319,7 +319,7 @@ onMounted(() => {
<template #title>
<span>{{ $t('tooltip.clientKey') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" size="small" @click="keyFileInput.click()" class="shadow">
<a-button :disabled="!sslFilesRequired" size="small" class="shadow" @click="keyFileInput.click()">
{{ $t('labels.clientKey') }}
</a-button>
</a-tooltip>
@ -328,7 +328,7 @@ onMounted(() => {
<template #title>
<span>{{ $t('tooltip.clientCA') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" size="small" @click="caFileInput.click()" class="shadow">
<a-button :disabled="!sslFilesRequired" size="small" class="shadow" @click="caFileInput.click()">
{{ $t('labels.serverCA') }}
</a-button>
</a-tooltip>
@ -364,7 +364,9 @@ onMounted(() => {
<a-button type="primary" ghost class="nc-extdb-btn-test-connection" @click="testConnection">
{{ $t('activity.testDbConn') }}
</a-button>
<a-button type="primary" :disabled="!testSuccess" class="nc-extdb-btn-submit !shadow" @click="createProject"> Submit </a-button>
<a-button type="primary" :disabled="!testSuccess" class="nc-extdb-btn-submit !shadow" @click="createProject">
Submit
</a-button>
</div>
</a-form-item>
</a-form>
@ -398,7 +400,7 @@ onMounted(() => {
@apply !min-h-0;
}
:deep(.ant-card-head-title){
@apply !text-3xl
:deep(.ant-card-head-title) {
@apply !text-3xl;
}
</style>

4
packages/nc-gui-v2/pages/signin.vue

@ -111,7 +111,9 @@ const resetError = () => {
</nuxt-link>
</div>
<div class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full">
<div
class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full"
>
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiLogin /> {{ $t('general.signIn') }}</span>
</button>

6
packages/nc-gui-v2/pages/signup.vue

@ -19,7 +19,7 @@ let error = $ref<string | null>(null)
const form = reactive({
email: '',
password: ''
password: '',
})
const formRules = {
@ -102,7 +102,9 @@ const resetError = () => {
/>
</a-form-item>
<div class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full">
<div
class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full"
>
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MaterialSymbolsRocketLaunchOutline /> {{ $t('general.signUp') }}</span>
</button>

118
packages/nc-gui-v2/utils/projectCreateUtils.ts

@ -1,43 +1,75 @@
import { adjectives, animals, starWars, uniqueNamesGenerator } from 'unique-names-generator'
import type { ClientType, ProjectCreateForm } from '~/lib/types'
import { ClientType } from '~/lib/enums'
export interface ProjectCreateForm {
title: string
dataSource: {
client: ClientType
connection:
| {
host: string
database: string
user: string
password: string
port: number | string
ssl?: Record<string, string>
searchPath?: string[]
}
| {
client?: ClientType.SQLITE
database: string
connection?: {
filename?: string
}
useNullAsDefault?: boolean
}
}
inflection: {
inflectionColumn?: string
inflectionTable?: string
}
sslUse?: any
}
const defaultHost = 'localhost'
const testDataBaseNames = {
mysql2: null,
[ClientType.MYSQL]: null,
mysql: null,
pg: 'postgres',
[ClientType.PG]: 'postgres',
oracledb: 'xe',
mssql: undefined,
sqlite3: 'a.sqlite',
[ClientType.MSSQL]: undefined,
[ClientType.SQLITE]: 'a.sqlite',
}
export const getTestDatabaseName = (db: { client: ClientType; connection?: { database?: string } }) => {
if (db.client === 'pg') return db.connection?.database
if (db.client === ClientType.PG) return db.connection?.database
return testDataBaseNames[db.client as keyof typeof testDataBaseNames]
}
export const clientTypes = [
{
text: 'MySql',
value: 'mysql2',
value: ClientType.MYSQL,
},
{
text: 'MSSQL',
value: 'mssql',
value: ClientType.MSSQL,
},
{
text: 'PostgreSQL',
value: 'pg',
value: ClientType.PG,
},
{
text: 'SQLite',
value: 'sqlite3',
value: ClientType.SQLITE,
},
]
const homeDir = ''
const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataSource']['connection']> = {
pg: {
host: 'localhost',
[ClientType.PG]: {
host: defaultHost,
port: '5432',
user: 'postgres',
password: 'password',
@ -49,8 +81,8 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
cert: '',
},
},
mysql2: {
host: 'localhost',
[ClientType.MYSQL]: {
host: defaultHost,
port: '3306',
user: 'root',
password: 'password',
@ -61,8 +93,8 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
cert: '',
},
},
vitess: {
host: 'localhost',
[ClientType.VITESS]: {
host: defaultHost,
port: '15306',
user: 'root',
password: 'password',
@ -73,8 +105,29 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
cert: '',
},
},
[ClientType.MSSQL]: {
host: defaultHost,
port: 1433,
user: 'sa',
password: 'Password123.',
database: '_test',
searchPath: ['dbo'],
ssl: {
ca: '',
key: '',
cert: '',
},
},
[ClientType.SQLITE]: {
client: ClientType.SQLITE,
database: homeDir,
connection: {
filename: homeDir,
},
useNullAsDefault: true,
},
tidb: {
host: 'localhost',
host: defaultHost,
port: '4000',
user: 'root',
password: '',
@ -86,7 +139,7 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
},
},
yugabyte: {
host: 'localhost',
host: defaultHost,
port: '5432',
user: 'postgres',
password: '',
@ -98,7 +151,7 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
},
},
citusdb: {
host: 'localhost',
host: defaultHost,
port: '5432',
user: 'postgres',
password: '',
@ -110,7 +163,7 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
},
},
cockroachdb: {
host: 'localhost',
host: defaultHost,
port: '5432',
user: 'postgres',
password: '',
@ -122,7 +175,7 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
},
},
greenplum: {
host: 'localhost',
host: defaultHost,
port: '5432',
user: 'postgres',
password: '',
@ -133,21 +186,8 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
cert: '',
},
},
mssql: {
host: 'localhost',
port: 1433,
user: 'sa',
password: 'Password123.',
database: '_test',
searchPath: ['dbo'],
ssl: {
ca: '',
key: '',
cert: '',
},
},
oracledb: {
host: 'localhost',
host: defaultHost,
port: '1521',
user: 'system',
password: 'Oracle18',
@ -158,14 +198,6 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
cert: '',
},
},
sqlite3: {
client: 'sqlite3',
database: homeDir,
connection: {
filename: homeDir,
},
useNullAsDefault: true,
},
}
export const getDefaultConnectionConfig = (client: ClientType): ProjectCreateForm['dataSource'] => {

6
packages/nc-gui-v2/utils/validation.ts

@ -78,11 +78,17 @@ export const projectTitleValidator = {
callback()
},
}
export const fieldRequiredValidator = {
required: true,
message: 'Field is required',
}
export const getRequiredValidator = (field = 'Field') => ({
required: true,
message: `${field} is required`,
})
export const importUrlValidator = {
validator: (rule: any, value: any, callback: (errMsg?: string) => void) => {
if (

Loading…
Cancel
Save