Browse Source

Merge pull request #1710 from nocodb/develop

0.90.0 Pre-Release
pull/1711/head
աɨռɢӄաօռɢ 2 years ago committed by GitHub
parent
commit
4261617ead
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      .all-contributorsrc
  2. 504
      .github/workflows/ci-cd.yml
  3. 8
      .github/workflows/dco-check.yml
  4. 26
      .github/workflows/publish-api-docs.yml
  5. 16
      .github/workflows/release-docker.yml
  6. 8
      .github/workflows/release-nightly-dev.yml
  7. 20
      .github/workflows/release-npm.yml
  8. 3
      .gitignore
  9. 5
      .run/Clear metadb.run.xml
  10. 14
      .run/build.run.xml
  11. 12
      .run/dev.run.xml
  12. 14
      .run/watch_run_mysql.run.xml
  13. 222
      README.md
  14. 42
      Run.md
  15. 15116
      package-lock.json
  16. 7
      package.json
  17. 34
      packages/nc-common/.eslintrc.json
  18. 9587
      packages/nc-common/package-lock.json
  19. 2
      packages/nc-common/src/index.ts
  20. 5
      packages/nc-gui/.eslintrc.json
  21. BIN
      packages/nc-gui/assets/img/discourse-icon.png
  22. 918
      packages/nc-gui/components/ProjectTreeView.vue
  23. 1170
      packages/nc-gui/components/ProjectTreeViewOld.vue
  24. 4
      packages/nc-gui/components/apiOverlay.vue
  25. 46
      packages/nc-gui/components/auth/apiTokens.vue
  26. 1
      packages/nc-gui/components/auth/authHooks.vue
  27. 2
      packages/nc-gui/components/auth/roles.vue
  28. 285
      packages/nc-gui/components/auth/shareOrInviteModal.vue
  29. 221
      packages/nc-gui/components/auth/userManagement.vue
  30. 15
      packages/nc-gui/components/authTab.vue
  31. 59
      packages/nc-gui/components/base/shareBase.vue
  32. 558
      packages/nc-gui/components/createOrEditProject.vue
  33. 15
      packages/nc-gui/components/createProjectComingSoon.vue
  34. 5
      packages/nc-gui/components/environment.vue
  35. 46
      packages/nc-gui/components/githubStarBtn.vue
  36. 62
      packages/nc-gui/components/globalAcl.vue
  37. 2
      packages/nc-gui/components/import/dropOrSelectFile.vue
  38. 1
      packages/nc-gui/components/import/dropOrSelectFileModal.vue
  39. 3
      packages/nc-gui/components/import/excelImport.vue
  40. 24
      packages/nc-gui/components/import/templateParsers/ExcelTemplateAdapter.js
  41. 21
      packages/nc-gui/components/importantAnnouncement.vue
  42. 2
      packages/nc-gui/components/loader.vue
  43. 2
      packages/nc-gui/components/monaco/Monaco.vue
  44. 3
      packages/nc-gui/components/monaco/MonacoSingleLineEditor.js
  45. 8
      packages/nc-gui/components/monaco/MonacoSqlEditor.vue
  46. 167
      packages/nc-gui/components/previewAs.vue
  47. 11
      packages/nc-gui/components/project/apiClientSwagger.vue
  48. 2
      packages/nc-gui/components/project/apis.vue
  49. 307
      packages/nc-gui/components/project/appStore.vue
  50. 34
      packages/nc-gui/components/project/appStore/appInstall.vue
  51. 6
      packages/nc-gui/components/project/auditTab.vue
  52. 22
      packages/nc-gui/components/project/auditTab/audit.vue
  53. 33
      packages/nc-gui/components/project/auditTab/db.vue
  54. 6
      packages/nc-gui/components/project/cronJobs.vue
  55. 28
      packages/nc-gui/components/project/dlgs/dlgAddRelation.vue
  56. 2
      packages/nc-gui/components/project/dlgs/dlgTriggerAddEdit.vue
  57. 3
      packages/nc-gui/components/project/functionTab/functionQuery.vue
  58. 4
      packages/nc-gui/components/project/gqlHandlerCodeEditor.vue
  59. 4
      packages/nc-gui/components/project/grpcHandlerCodeEditor.vue
  60. 102
      packages/nc-gui/components/project/projectMetadata/disableOrEnableModels.vue
  61. 4
      packages/nc-gui/components/project/projectMetadata/sync/disableOrEnableRelations.vue
  62. 47
      packages/nc-gui/components/project/projectMetadata/sync/metaDiffSync.vue
  63. 4
      packages/nc-gui/components/project/projectMetadata/uiAcl/toggleRelationsUIAcl.vue
  64. 66
      packages/nc-gui/components/project/projectMetadata/uiAcl/toggleTableUIAcl.vue
  65. 16
      packages/nc-gui/components/project/restHandlerCodeEditor.vue
  66. 13
      packages/nc-gui/components/project/sequence.vue
  67. 65
      packages/nc-gui/components/project/settings/xcMeta.vue
  68. 29
      packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js
  69. 2
      packages/nc-gui/components/project/spreadsheet/apis/grpcApi.js
  70. 6
      packages/nc-gui/components/project/spreadsheet/apis/restApi.js
  71. 499
      packages/nc-gui/components/project/spreadsheet/components/columnFilter.vue
  72. 28
      packages/nc-gui/components/project/spreadsheet/components/columnFilterMenu.vue
  73. 115
      packages/nc-gui/components/project/spreadsheet/components/editColumn.vue
  74. 88
      packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue
  75. 203
      packages/nc-gui/components/project/spreadsheet/components/editColumn/linkedToAnotherOptions.vue
  76. 116
      packages/nc-gui/components/project/spreadsheet/components/editColumn/lookupOptions.vue
  77. 25
      packages/nc-gui/components/project/spreadsheet/components/editColumn/relationOptions.vue
  78. 113
      packages/nc-gui/components/project/spreadsheet/components/editColumn/rollupOptions.vue
  79. 62
      packages/nc-gui/components/project/spreadsheet/components/editVirtualColumn.vue
  80. 7
      packages/nc-gui/components/project/spreadsheet/components/editable.vue
  81. 4
      packages/nc-gui/components/project/spreadsheet/components/editableCell.vue
  82. 9
      packages/nc-gui/components/project/spreadsheet/components/editableCell/booleanCell.vue
  83. 8
      packages/nc-gui/components/project/spreadsheet/components/editableCell/dateTimePickerCell.vue
  84. 113
      packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue
  85. 1
      packages/nc-gui/components/project/spreadsheet/components/editableCell/editableUrlCell.vue
  86. 1
      packages/nc-gui/components/project/spreadsheet/components/editableCell/enumListEditableCell.vue
  87. 2
      packages/nc-gui/components/project/spreadsheet/components/editableCell/jsonEditableCell.vue
  88. 5
      packages/nc-gui/components/project/spreadsheet/components/editableCell/setListEditableCell.vue
  89. 2
      packages/nc-gui/components/project/spreadsheet/components/editableCell/textCell.vue
  90. 1
      packages/nc-gui/components/project/spreadsheet/components/editableCell/timePickerCell.vue
  91. 226
      packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue
  92. 85
      packages/nc-gui/components/project/spreadsheet/components/extras.vue
  93. 162
      packages/nc-gui/components/project/spreadsheet/components/fieldsMenu.vue
  94. 1
      packages/nc-gui/components/project/spreadsheet/components/fieldsMenuItem.vue
  95. 71
      packages/nc-gui/components/project/spreadsheet/components/headerCell.vue
  96. 60
      packages/nc-gui/components/project/spreadsheet/components/importExport/columnMappingModal.vue
  97. 1
      packages/nc-gui/components/project/spreadsheet/components/lockMenu.vue
  98. 132
      packages/nc-gui/components/project/spreadsheet/components/moreActions.vue
  99. 3
      packages/nc-gui/components/project/spreadsheet/components/pagination.vue
  100. 38
      packages/nc-gui/components/project/spreadsheet/components/shareViewMenu.vue
  101. Some files were not shown because too many files have changed in this diff Show More

9
.all-contributorsrc

@ -702,6 +702,15 @@
"contributions": [
"code"
]
},
{
"login": "RobinFrcd",
"name": "Robin Fourcade",
"avatar_url": "https://avatars.githubusercontent.com/u/29704178?v=4",
"profile": "https://github.com/RobinFrcd",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

504
.github/workflows/ci-cd.yml

@ -4,15 +4,14 @@
name: "CI/CD"
on:
push:
branches-ignore:
- master
branches: [master, develop, cypress]
paths:
- "packages/nc-gui/**"
- "scripts/cypress/**"
- "packages/nocodb/**"
- ".github/workflows/ci-cd.yml"
pull_request:
branches: [develop]
branches: [master, develop]
paths:
- "packages/nc-gui/**"
- "scripts/cypress/**"
@ -20,7 +19,7 @@ on:
- ".github/workflows/ci-cd.yml"
jobs:
cypress-pg-restTableOps-run:
cypress-pg-restTableOps-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -50,7 +49,8 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
spec: "./scripts/cypress/integration/test/pg-restTableOps.js"
@ -58,12 +58,13 @@ jobs:
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-pg-restViews-run:
cypress-pg-restViews-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -93,7 +94,8 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
spec: "./scripts/cypress/integration/test/pg-restViews.js"
@ -101,12 +103,13 @@ jobs:
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-pg-restRoles-run:
cypress-pg-restRoles-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -136,7 +139,8 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
spec: "./scripts/cypress/integration/test/pg-restRoles.js"
@ -144,12 +148,13 @@ jobs:
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-pg-restMisc-run:
cypress-pg-restMisc-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -179,7 +184,8 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
spec: "./scripts/cypress/integration/test/pg-restMisc.js"
@ -187,12 +193,13 @@ jobs:
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-restTableOps-run:
cypress-restTableOps-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -222,7 +229,8 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
spec: "./scripts/cypress/integration/test/restTableOps.js"
@ -230,12 +238,13 @@ jobs:
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-restViews-run:
cypress-restViews-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -259,14 +268,14 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
spec: "./scripts/cypress/integration/test/restViews.js"
@ -274,12 +283,13 @@ jobs:
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: restViews-snapshots
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-restRoles-run:
cypress-restRoles-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -303,14 +313,14 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
spec: "./scripts/cypress/integration/test/restRoles.js"
@ -318,12 +328,13 @@ jobs:
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: restRoles-snapshots
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-restMisc-run:
cypress-restMisc-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -347,14 +358,14 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
spec: "./scripts/cypress/integration/test/restMisc.js"
@ -362,183 +373,13 @@ jobs:
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: restMisc-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-restTableOps-run:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:xcdb-api
npm run start:web
spec: "./scripts/cypress/integration/test/xcdb-restTableOps.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
uses: actions/upload-artifact@v2
with:
name: xcdb-restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-restViews-run:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:xcdb-api
npm run start:web
spec: "./scripts/cypress/integration/test/xcdb-restViews.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
uses: actions/upload-artifact@v2
with:
name: xcdb-restViews-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-restRoles-run:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:xcdb-api
npm run start:web
spec: "./scripts/cypress/integration/test/xcdb-restRoles.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
uses: actions/upload-artifact@v2
with:
name: xcdb-restRoles-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-restMisc-run:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:xcdb-api
npm run start:web
spec: "./scripts/cypress/integration/test/xcdb-restMisc.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
uses: actions/upload-artifact@v2
with:
name: xcdb-restMisc-snapshots
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-gqlTableOps-run:
cypress-xcdb-restTableOps-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -562,27 +403,28 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
spec: "./scripts/cypress/integration/test/gqlTableOps.js"
spec: "./scripts/cypress/integration/test/xcdb-restTableOps.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: gqlTableOps-snapshots
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-gqlViews-run:
cypress-xcdb-restViews-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -606,27 +448,28 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
spec: "./scripts/cypress/integration/test/gqlViews.js"
spec: "./scripts/cypress/integration/test/xcdb-restViews.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: gqlViews-snapshots
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-gqlRoles-run:
cypress-xcdb-restRoles-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -650,27 +493,28 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
spec: "./scripts/cypress/integration/test/gqlRoles.js"
spec: "./scripts/cypress/integration/test/xcdb-restRoles.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: gqlRoles-snapshots
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-gqlMisc-run:
cypress-xcdb-restMisc-run-cache:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
@ -694,226 +538,54 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:api
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
spec: "./scripts/cypress/integration/test/gqlMisc.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
uses: actions/upload-artifact@v2
with:
name: gqlMisc-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-gqlTableOps-run:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:xcdb-api
npm run start:web
spec: "./scripts/cypress/integration/test/xcdb-gqlTableOps.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
uses: actions/upload-artifact@v2
with:
name: xcdb-gqlTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-gqlViews-run:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:xcdb-api
npm run start:web
spec: "./scripts/cypress/integration/test/xcdb-gqlViews.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
uses: actions/upload-artifact@v2
with:
name: xcdb-gqlViews-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-gqlRoles-run:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:xcdb-api
npm run start:web
spec: "./scripts/cypress/integration/test/xcdb-gqlRoles.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
uses: actions/upload-artifact@v2
with:
name: xcdb-gqlRoles-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-gqlMisc-run:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
npm run start:xcdb-api
npm run start:web
spec: "./scripts/cypress/integration/test/xcdb-gqlMisc.js"
spec: "./scripts/cypress/integration/test/xcdb-restMisc.js"
wait-on: "http://localhost:8080, http://localhost:3000/_nuxt/runtime.js"
wait-on-timeout: 1200
config-file: scripts/cypress/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: xcdb-gqlMisc-snapshots
name: restTableOps-snapshots
path: scripts/cypress/screenshots
retention-days: 2
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Check for update
run: |
echo "CHANGED=$([[ $(lerna ls --since ${{github.event.before}} | grep nocodb) = nocodb ]] && echo 'OK')" >> $GITHUB_ENV
- name: Test Mysql REST APIs
if: ${{ env.CHANGED == 'OK' }}
run: cd ./packages/nocodb/ && docker-compose run xc-test-mysql
- name: Test Mysql GraphQL APIs
if: ${{ env.CHANGED == 'OK' }}
run: cd ./packages/nocodb/ && docker-compose run xc-test-gql-mysql
# - name: Test MSSQL REST APIs
# run: cd ./packages/nocodb/ && docker-compose run xc-test-mssql
# - name: Test MSSQL GraphQL APIs
# run: cd ./packages/nocodb/ && docker-compose run xc-test-gql-mssql
#
- name: Test PostgreSQL REST APIs
if: ${{ env.CHANGED == 'OK' }}
run: cd ./packages/nocodb/ && docker-compose run xc-test-pg
- name: Test PostgreSQL GraphQL APIs
if: ${{ env.CHANGED == 'OK' }}
run: cd ./packages/nocodb/ && docker-compose run xc-test-gql-pg
# docker:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# with:
# fetch-depth: 0
# - name: Check for update
# run: |
# echo "CHANGED=$([[ $(lerna ls --since ${{github.event.before}} | grep nocodb) = nocodb ]] && echo 'OK')" >> $GITHUB_ENV
# - name: Test Mysql REST APIs
# if: ${{ env.CHANGED == 'OK' }}
# run: cd ./packages/nocodb/ && docker-compose run xc-test-mysql
# - name: Test Mysql GraphQL APIs
# if: ${{ env.CHANGED == 'OK' }}
# run: cd ./packages/nocodb/ && docker-compose run xc-test-gql-mysql
#
# # - name: Test MSSQL REST APIs
# # run: cd ./packages/nocodb/ && docker-compose run xc-test-mssql
# # - name: Test MSSQL GraphQL APIs
# # run: cd ./packages/nocodb/ && docker-compose run xc-test-gql-mssql
# #
# - name: Test PostgreSQL REST APIs
# if: ${{ env.CHANGED == 'OK' }}
# run: cd ./packages/nocodb/ && docker-compose run xc-test-pg
# - name: Test PostgreSQL GraphQL APIs
# if: ${{ env.CHANGED == 'OK' }}
# run: cd ./packages/nocodb/ && docker-compose run xc-test-gql-pg
#
# - name: Test SQLite3 REST APIs
# run: cd ./packages/nocodb/ && docker-compose run xc-test-sqlite

8
.github/workflows/dco-check.yml

@ -5,11 +5,11 @@ name: Check DCO
on:
pull_request:
paths:
- "packages/nc-common/**"
- "packages/nocodb-sdk/**"
- "packages/nc-gui/**"
- "packages/nc-lib-gui/**"
- "packages/nc-plugin/**"
- "packages/nocodb/**"
- "packages/nc-lib-gui/**"
- "packages/nc-plugin/**"
- "packages/nocodb/**"
- "scripts/cypress/**"
jobs:

26
.github/workflows/publish-api-docs.yml

@ -0,0 +1,26 @@
name: "Publish : Api Docs"
on:
push:
branches: [ master ]
paths:
- "scripts/sdk/swagger.json"
jobs:
copy-file:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Pushes generated output
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658
env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with:
source_file: 'scripts/sdk/swagger.json'
destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'src'
user_email: 'oof1lab@gmail.com'
user_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nc'

16
.github/workflows/release-docker.yml

@ -42,7 +42,7 @@ jobs:
working-directory: ./packages/nocodb
strategy:
matrix:
node-version: [12]
node-version: [14]
steps:
- name: Get Docker Repository
id: get-docker-repository
@ -73,6 +73,20 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: upgrade packages for nightly build
if: ${{ github.event.inputs.targetEnv == 'DEV' || inputs.targetEnv == 'DEV' }}
run: |
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} targetVersion=${{ github.event.inputs.tag || inputs.tag }} node scripts/bumpNocodbSdkVersion.js
cd packages/nocodb-sdk
npm install && npm run build
cd ../..
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} node scripts/upgradeNocodbSdk.js
cd packages/nc-gui
npm install
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} targetVersion=${{ github.event.inputs.tag || inputs.tag }} npm run build:copy
cd ../..
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} node scripts/upgradeNcGui.js
- uses: bahmutov/npm-install@v1
with:
working-directory: ${{ env.working-directory }}

8
.github/workflows/release-nightly-dev.yml

@ -12,8 +12,8 @@ on:
- DEV
# - PROD
schedule:
# at the end of every day
- cron: '0 0 * * *'
# every 6 hours
- cron: '0 */6 * * *'
jobs:
# enrich tag for nightly auto release
@ -26,11 +26,11 @@ jobs:
# Get current date
CURRENT_DATE=$(date +"%Y%m%d")
CURRENT_TIME=$(date +"%H%M")
TAG_NAME=${CURRENT_DATE}
TAG_NAME=${CURRENT_DATE}-${CURRENT_TIME}
IS_DAILY='Y'
# Set the tag
if [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then
TAG_NAME=${TAG_NAME}-${CURRENT_TIME}
TAG_NAME=${TAG_NAME}
IS_DAILY='N'
fi
echo "::set-output name=NIGHTLY_BUILD_TAG::${TAG_NAME}"

20
.github/workflows/release-npm.yml

@ -37,21 +37,27 @@ jobs:
working-directory: ./packages/nocodb
strategy:
matrix:
node-version: [12]
node-version: [14]
steps:
- uses: actions/checkout@v2
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v2
- name: Checkout
uses: actions/checkout@v2
- name: NPM Setup and Publish with ${{ matrix.node-version }}
# Setup .npmrc file to publish to npm
uses: actions/setup-node@v2
with:
node-version: '16.x'
node-version: ${{ matrix.node-version }}
registry-url: 'https://registry.npmjs.org'
- run: |
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} targetVersion=${{ github.event.inputs.tag || inputs.tag }} node scripts/bumpNocodbSdkVersion.js
cd packages/nocodb-sdk
npm install && npm run build && npm publish
cd ../..
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} node scripts/upgradeNocodbSdk.js
cd packages/nc-gui
npm install
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} targetVersion=${{ github.event.inputs.tag || inputs.tag }} npm run build:copy:jsdeliver
cd ../..
npm install
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} targetVersion=${{ github.event.inputs.tag || inputs.tag }} node scripts/upgradeNcGui.js && cd packages/nocodb && npm install && npm run obfuscate:build:publish
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} node scripts/upgradeNcGui.js && cd packages/nocodb && npm install && npm run obfuscate:build:publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create Pull Request

3
.gitignore vendored

@ -83,4 +83,5 @@ mongod
# Cypress
#=========
shared.json
/scripts/Cypress/screenshots
/scripts/Cypress/screenshots
/scripts/exp/

5
.run/Clear metadb.run.xml

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Drop metadb" type="NodeJSConfigurationType" path-to-js-file="deleteMetaDb.js" working-dir="$PROJECT_DIR$/packages/nocodb/src/example">
<method v="2" />
</configuration>
</component>

14
.run/build.run.xml

@ -0,0 +1,14 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build Nc Common" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/packages/nocodb-sdk/package.json" />
<command value="run" />
<scripts>
<script value="build" />
</scripts>
<node-interpreter value="project" />
<envs>
<env name="NC_DISABLE_CACHE" value="true" />
</envs>
<method v="2" />
</configuration>
</component>

12
.run/dev.run.xml

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run GUI" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/packages/nc-gui/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

14
.run/watch_run_mysql.run.xml

@ -0,0 +1,14 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run NocoDB Mysql" type="js.build_tools.npm" activateToolWindowBeforeRun="false">
<package-json value="$PROJECT_DIR$/packages/nocodb/package.json" />
<command value="run" />
<scripts>
<script value="watch:run:mysql" />
</scripts>
<node-interpreter value="project" />
<envs>
<env name="NC_DISABLE_CACHE" value="true" />
</envs>
<method v="2" />
</configuration>
</component>

222
README.md

@ -13,7 +13,7 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadshe
<div align="center">
[![Build Status](https://travis-ci.org/dwyl/esta.svg?branch=master)](https://travis-ci.com/github/NocoDB/NocoDB)
[![Node version](https://badgen.net/npm/node/next)](http://nodejs.org/download/)
[![Node version](https://img.shields.io/badge/node-%3E%3D%2014.18.0-brightgreen)](http://nodejs.org/download/)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-green.svg)](https://conventionalcommits.org)
</div>
@ -21,6 +21,7 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadshe
<p align="center">
<a href="http://www.nocodb.com"><b>Website</b></a>
<a href="https://discord.gg/5RgZmkW"><b>Discord</b></a>
<a href="https://community.nocodb.com/"><b>Community</b></a>
<a href="https://twitter.com/nocodb"><b>Twitter</b></a>
<a href="https://www.reddit.com/r/NocoDB/"><b>Reddit</b></a>
<a href="https://docs.nocodb.com/"><b>Documentation</b></a>
@ -51,9 +52,11 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadshe
<img src="https://static.scarf.sh/a.png?x-pxid=c12a77cc-855e-4602-8a0f-614b2d0da56a" />
# Quick try
### 1-Click Deploy
#### Heroku
## 1-Click Deploy to Heroku
Before doing so, make sure you have a Heroku account. By default, an add-on Heroku Postgres will be used as meta database. You can see the connection string defined in `DATABASE_URL` by navigating to Heroku App Settings and selecting Config Vars.
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
@ -61,43 +64,100 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadshe
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br>
### Using Docker
```bash
docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
```
<br/>
- NocoDB needs a database as input : See [Production Setup](https://github.com/nocodb/nocodb/blob/master/README.md#production-setup).
- If this input is absent, we fallback to SQLite. In order too persist sqlite, you can mount `/usr/app/data/`.
## NPX
Example:
You can run below command if you need an interactive configuration.
```
docker run -d -p 8080:8080 --name nocodb -v /local/path:/usr/app/data/ nocodb/nocodb:latest
```
### Using Npm
```
npx create-nocodb-app
```
### Using Git
```
<img src="https://user-images.githubusercontent.com/35857179/163672964-00ef5d62-0434-447d-ac01-3ebb780099b9.png" width="520px"/>
## Node Application
We provide a simple NodeJS Application for getting started.
```bash
git clone https://github.com/nocodb/nocodb-seed
cd nocodb-seed
npm install
npm start
```
### GUI
## Docker
```bash
# for SQLite
docker run -d --name nocodb \
-v /local/path:/usr/app/data/ \
-p 8080:8080 \
nocodb/nocodb:latest
# for MySQL
docker run -d --name nocodb-mysql \
-v /local/path:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
# for PostgreSQL
docker run -d --name nocodb-postgres \
-v /local/path:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="pg://host.docker.internal:5432?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
# for MSSQL
docker run -d --name nocodb-mssql \
-v /local/path:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="mssql://host.docker.internal:1433?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
> To persist data in docker you can mount volume at `/usr/app/data/` since 0.10.6. Otherwise your data will be lost after recreating the container.
> If you plan to input some special characters, you may need to change the character set and collation yourself when creating the database. Please check out the examples for [MySQL Docker](https://github.com/nocodb/nocodb/issues/1340#issuecomment-1049481043).
## Docker Compose
We provide different docker-compose.yml files under [this directory](https://github.com/nocodb/nocodb/tree/master/docker-compose). Here are some examples.
```bash
git clone https://github.com/nocodb/nocodb
# for MySQL
cd nocodb/docker-compose/mysql
# for PostgreSQL
cd nocodb/docker-compose/pg
# for MSSQL
cd nocodb/docker-compose/mssql
docker-compose up -d
```
> To persist data in docker, you can mount volume at `/usr/app/data/` since 0.10.6. Otherwise your data will be lost after recreating the container.
> If you plan to input some special characters, you may need to change the character set and collation yourself when creating the database. Please check out the examples for [MySQL Docker Compose](https://github.com/nocodb/nocodb/issues/1313#issuecomment-1046625974).
# GUI
Access Dashboard using : [http://localhost:8080/dashboard](http://localhost:8080/dashboard)
# Join Our Community
<a href="https://discord.gg/5RgZmkW">
<a href="https://discord.gg/5RgZmkW" target="_blank">
<img src="https://discordapp.com/api/guilds/661905455894888490/widget.png?style=banner3" alt="">
</a>
<a href="https://community.nocodb.com/" target="_blank">
<img src="https://i2.wp.com/www.feverbee.com/wp-content/uploads/2018/07/logo-discourse.png" alt="">
</a>
# Screenshots
@ -128,7 +188,8 @@ Access Dashboard using : [http://localhost:8080/dashboard](http://localhost:8080
![10](https://user-images.githubusercontent.com/5435402/133759250-ebd75ecf-31db-4a17-b2d7-2c43af78a54e.png)
<br>
![8](https://user-images.githubusercontent.com/5435402/133759248-3a7141e0-4b7d-4079-a5f9-cf8611d00bc5.png)
![8](https://user-images.githubusercontent.com/35857179/163675704-54eb644d-3b5e-45e3-aad4-794a0f55c692.png)
<br>
![9](https://user-images.githubusercontent.com/5435402/133759249-8c1a85c2-a55c-48f6-bd58-aa6b4195cce7.png)
@ -136,27 +197,25 @@ Access Dashboard using : [http://localhost:8080/dashboard](http://localhost:8080
# Table of Contents
- [Quick try](#quick-try)
+ [1-Click Deploy](#1-click-deploy)
- [Heroku](#heroku)
+ [Using Docker](#using-docker)
+ [Using Npm](#using-npm)
+ [Using Git](#using-git)
+ [GUI](#gui)
* [1-Click Deploy to Heroku](#1-click-deploy-to-heroku)
* [NPX](#npx)
* [Node Application](#node-application)
* [Docker](#docker)
* [Docker Compose](#docker-compose)
- [GUI](#gui)
- [Join Our Community](#join-our-community)
- [Screenshots](#screenshots)
- [Table of Contents](#table-of-contents)
- [Features](#features)
+ [Rich Spreadsheet Interface](#rich-spreadsheet-interface)
+ [App Store for workflow automations](#app-store-for-workflow-automations)
+ [Programmatic API access via](#programmatic-api-access-via)
+ [App Store for Workflow Automations](#app-store-for-workflow-automations)
+ [Programmatic Access](#programmatic-access)
+ [Sync Schema](#sync-schema)
+ [Audit](#audit)
- [Production Setup](#production-setup)
* [Docker](#docker)
- [Example: MySQL](#example--mysql)
- [Example: PostgreSQL](#example--postgresql)
- [Example: SQL Server](#example--sql-server)
* [Docker Compose](#docker-compose)
* [Environment variables](#environment-variables)
- [Development Setup](#development-setup)
* [Cloning the project](#clone-the-project)
* [Cloning the Project](#cloning-the-project)
* [Running Backend locally](#running-backend-locally)
* [Running Frontend locally](#running-frontend-locally)
* [Running Cypress tests locally](#running-cypress-tests-locally)
@ -166,66 +225,44 @@ Access Dashboard using : [http://localhost:8080/dashboard](http://localhost:8080
- [Contributors](#contributors)
# Features
### Rich Spreadsheet Interface
- ⚡ &nbsp;Search, sort, filter, hide columns with uber ease
- ⚡ &nbsp;Create Views : Grid, Gallery, Kanban, Form
- ⚡ &nbsp;Share Views : public & password protected
- ⚡ &nbsp;Personal & locked Views
- ⚡ &nbsp;Upload images to cells (Works with S3, Minio, GCP, Azure, DigitalOcean, Linode, OVH, BackBlaze)
- ⚡ &nbsp;Roles : Owner, Creator, Editor, Viewer, Commenter, Custom Roles.
- ⚡ &nbsp;Access Control : Fine-grained access control even at database, table & column level.
### App Store for workflow automations
- ⚡ &nbsp;Chat : Microsoft Teams, Slack, Discord, Mattermost
- ⚡ &nbsp;Email : SMTP, SES, Mailchimp
- ⚡ &nbsp;SMS : Twilio
- ⚡ &nbsp;Whatsapp
- ⚡ &nbsp;Any 3rd Party APIs
### Programmatic API access via
- ⚡ &nbsp;REST APIs (Swagger)
- ⚡ &nbsp;GraphQL APIs.
- ⚡ &nbsp;Includes JWT Authentication & Social Auth
- ⚡ &nbsp;API tokens to integrate with Zapier, Integromat.
# Production Setup
NocoDB requires a database to store metadata of spreadsheets views and external databases.
And connection params for this database can be specified in `NC_DB` environment variable.
- ⚡ &nbsp;Basic Operations: Create, Read, Update and Delete on Tables, Columns, and Rows
- ⚡ &nbsp;Fields Operations: Sort, Filter, Hide / Unhide Columns
- ⚡ &nbsp;Multiple Views Types: Grid (By default), Gallery and Form View
- ⚡ &nbsp;View Permissions Types: Collaborative Views, & Locked Views
- ⚡ &nbsp;Share Bases / Views: either Public or Private (with Password Protected)
- ⚡ &nbsp;Variant Cell Types: ID, LinkToAnotherRecord, Lookup, Rollup, SingleLineText, Attachement, Currency, Formula and etc
- ⚡ &nbsp;Access Control with Roles : Fine-grained Access Control at different levels
- ⚡ &nbsp;and more ...
## Docker
### App Store for Workflow Automations
#### Example: MySQL
```
docker run -d -p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
We provide different integrations in three main categories. See <a href="https://docs.nocodb.com/setup-and-usages/app-store" target="_blank">App Store</a> for details.
#### Example: PostgreSQL
```
docker run -d -p 8080:8080 \
-e NC_DB="pg://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
- ⚡ &nbsp;Chat : Slack, Discord, Mattermost, and etc
- ⚡ &nbsp;Email : AWS SES, SMTP, MailerSend, and etc
- ⚡ &nbsp;Storage : AWS S3, Google Cloud Storage, Minio, and etc
#### Example: SQL Server
```
docker run -d -p 8080:8080 \
-e NC_DB="mssql://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
### Programmatic Access
## Docker Compose
```
git clone https://github.com/nocodb/nocodb
cd nocodb
cd docker-compose
cd mysql or pg or mssql
docker-compose up -d
```
We provide the following ways to let users to invoke actions in a programmatic way. You can use a token (either JWT or Social Auth) to sign your requests for authorization to NocoDB.
- ⚡ &nbsp;REST APIs
- ⚡ &nbsp;NocoDB SDK
### Sync Schema
We allow you to sync schema changes if you have made changes outside NocoDB GUI. However, it has to be noted then you will have to bring your own schema migrations for moving from environment to others. See <a href="https://docs.nocodb.com/setup-and-usages/sync-schema/" target="_blank">Sync Schema</a> for details.
### Audit
We are keeping all the user operation logs under one place. See <a href="https://docs.nocodb.com/setup-and-usages/audit" target="_blank">Audit</a> for details.
# Production Setup
By default, SQLite is used for storing meta data. However, you can specify your own database. The connection params for this database can be specified in `NC_DB` environment variable. Moreover, we also provide the below environment variables for configuration.
## Environment variables
@ -262,7 +299,6 @@ Changes made to code automatically restart.
> nocodb/packages/nocodb includes nc-lib-gui which is the built version of nc-gui hosted in npm registry. You can visit localhost:8000/dashboard in browser after starting the backend locally if you just want to modify the backend only.
## Running Cypress tests locally
```shell
@ -402,14 +438,12 @@ Our mission is to provide the most powerful no-code interface for databases whic
<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/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>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->

42
Run.md

@ -0,0 +1,42 @@
# Setup
#### Setting up dev environment
- Clone `nocodb/nocodb` GitHub repo and checkout to `feat/v2` branch
```sh
git clone https://github.com/nocodb/nc
cd nocodb
```
- Navigate to `nocodb-sdk` package folder, install and build the package
```sh
cd packages/nocodb-sdk
npm install
npm run build
```
#### Running backend
```sh
# Navigate to `nocodb` package and install dependencies
cd packages/nocodb
npm install
# requires sqlite3
npm run watch:run
# if you have mysql on localhost (set its password as password)
# npm run watch:run:mysql
```
#### Running frontend
```sh
# Navigate to `nc-gui` package and install dependencies
cd packages/nc-gui
npm install
npm run dev
```

15116
package-lock.json generated

File diff suppressed because it is too large Load Diff

7
package.json

@ -13,8 +13,11 @@
"xlsx": "^0.17.4"
},
"scripts": {
"start:api": "cd ./packages/nocodb; npm install; npm run watch:run",
"start:xcdb-api": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run",
"build:common": "cd ./packages/nocodb-sdk; npm install; npm run build",
"start:api": "cd ./packages/nocodb; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api": "cd ./packages/nocodb; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:api:cache": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api:cache": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:web": "cd ./packages/nc-gui; npm install; npm run dev",
"cypress:run": "cypress run --config-file ./scripts/cypress/cypress.json",
"cypress:open": "cypress open --config-file ./scripts/cypress/cypress.json",

34
packages/nc-common/.eslintrc.json

@ -1,34 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": "./tsconfig.json" },
"env": { "es6": true },
"ignorePatterns": ["node_modules", "build", "coverage"],
"plugins": ["import", "eslint-comments", "functional"],
"extends": [
"eslint:recommended",
"plugin:eslint-comments/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/typescript",
"plugin:functional/lite",
"prettier",
"prettier/@typescript-eslint"
],
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"eslint-comments/disable-enable-pair": [
"error",
{ "allowWholeFile": true }
],
"eslint-comments/no-unused-disable": "error",
"import/order": [
"error",
{ "newlines-between": "always", "alphabetize": { "order": "asc" } }
],
"sort-imports": [
"error",
{ "ignoreDeclarationSort": true, "ignoreCase": true }
]
}
}

9587
packages/nc-common/package-lock.json generated

File diff suppressed because it is too large Load Diff

2
packages/nc-common/src/index.ts

@ -1,2 +0,0 @@
export * from './lib/XcUIBuilder';
export * from './lib/XcNotification';

5
packages/nc-gui/.eslintrc.json

@ -25,7 +25,10 @@
"ignoreI18nBlock": false
}
],
"@intlify/vue-i18n/no-missing-keys": "error"
"@intlify/vue-i18n/no-missing-keys": "error",
"max-len": ["warn", {
"code": 120
}]
},
"parserOptions": {
"parser": "babel-eslint",

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

918
packages/nc-gui/components/ProjectTreeView.vue

File diff suppressed because it is too large Load Diff

1170
packages/nc-gui/components/ProjectTreeViewOld.vue

File diff suppressed because it is too large Load Diff

4
packages/nc-gui/components/apiOverlay.vue

@ -8,8 +8,6 @@
<div>
<v-card dark style="width: 100%; max-height:100%;overflow: auto">
<v-container v-if="databaseCount" fluid style="min-height:200px;">
<!-- <v-row class="text-center">-->
<!-- <v-col cols="12">-->
<v-card class="pa-2 text-center elevation-10" dark>
<h3 class="title mb-3 mt-4">
APIs Generated
@ -19,8 +17,6 @@
<span class="subtitle grey--text text--lighten-1">within {{ timeTaken }} seconds</span>
</p>
</v-card>
<!-- </v-col>-->
<!-- </v-row>-->
<v-row>
<v-col>
<v-card dark class=" elevation-10">

46
packages/nc-gui/components/auth/apiTokens.vue

@ -1,6 +1,6 @@
<template>
<div>
<v-toolbar flat height="38" class="toolbar-border-bottom">
<v-toolbar flat height="38" class="mt-5">
<v-spacer />
<x-btn
@ -27,7 +27,7 @@
color="primary"
small
:disabled="loading"
@click="newTokenDialog = true"
@click="showNewTokenDlg"
>
<v-icon small left>
mdi-plus
@ -55,6 +55,17 @@
</th>
</tr>
</thead>
<tr v-if="!tokens.length">
<td colspan="3">
<div
class="text-center caption grey--text"
>
No tokens available
</div>
</td>
</tr>
<tr v-for="(token,i) in tokens" :key="i">
<td class="caption text-center">
{{ token.description }}
@ -90,15 +101,15 @@
</x-icon>
</td>
</tr>
<tr>
<!-- <tr>
<td colspan="3" class="text-center">
<x-btn tooltip="Generate new api token" outlined x-small color="primary" @click="newTokenDialog = true">
<x-btn tooltip="Generate new api token" outlined x-small color="primary" @click="showNewTokenDlg">
<v-icon>mdi-plus</v-icon>
<!--Add New Token-->
&lt;!&ndash;Add New Token&ndash;&gt;
{{ $t('activity.newToken') }}
</x-btn>
</td>
</tr>
</tr>-->
</v-simple-table>
</v-container>
@ -138,6 +149,8 @@
</template>
<script>
import { copyTextToClipboard } from '~/helpers/xutils'
export default {
name: 'ApiTokens',
data: () => ({
@ -150,32 +163,45 @@ export default {
this.loadApiTokens()
},
methods: {
showNewTokenDlg() {
this.newTokenDialog = true
this.$tele.emit('api-mgmt:token:generate:trigger')
},
copyToken(token) {
this.$clipboard(token)
copyTextToClipboard(token)
this.$toast.info('Copied to clipboard').goAway(1000)
this.$tele.emit('api-mgmt:token:copy')
},
async loadApiTokens() {
this.tokens = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcApiTokenList'])
this.tokens = (await this.$api.apiToken.list(this.$store.state.project.projectId))
},
async generateToken() {
try {
this.newTokenDialog = false
this.tokens = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcApiTokenCreate', this.tokenObj])
await this.$api.apiToken.create(this.$store.state.project.projectId, this.tokenObj)
this.$toast.success('Token generated successfully').goAway(3000)
this.tokenObj = {}
await this.loadApiTokens()
} catch (e) {
console.log(e)
this.$toast.error(e.message).goAway(3000)
}
this.$tele.emit('api-mgmt:token:generate:submit')
},
async deleteToken(item) {
try {
this.tokens = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcApiTokenDelete', { id: item.id }])
await this.$api.apiToken.delete(this.$store.state.project.projectId, item.token)
// this.tokens = //await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcApiTokenDelete', { id: item.id }])
this.$toast.success('Token deleted successfully').goAway(3000)
await this.loadApiTokens()
} catch (e) {
console.log(e)
this.$toast.error(e.message).goAway(3000)
}
this.$tele.emit('api-mgmt:token:delete')
}
}
}

1
packages/nc-gui/components/auth/authHooks.vue

@ -73,6 +73,7 @@ export default {
}, 'xcAuthHookSet', this.data])
this.$toast.success('Auth hook details updated successfully').goAway(3000)
} catch (e) {
console.log(e)
this.$toast.error('Some error occurred').goAway(3000)
}
}

2
packages/nc-gui/components/auth/roles.vue

@ -12,7 +12,7 @@
<!-- href: '#'-->
<!-- },-->
<!-- {-->
<!-- text: nodes.tn + ' (Logic)',-->
<!-- text: nodes.table_name + ' (Logic)',-->
<!-- disabled: true,-->
<!-- href: '#'-->
<!-- }]" divider=">" small>-->

285
packages/nc-gui/components/auth/shareOrInviteModal.vue

@ -0,0 +1,285 @@
<template>
<v-dialog v-model="userEditDialog" :width="invite_token ? 700 :700" @close="invite_token = null">
<v-card v-if="selectedUser" style="min-height: 100%" class="elevation-0">
<v-card-title>
{{ $t('activity.share') }} : {{ $store.getters['project/GtrProjectName'] }}
<div class="nc-header-border" />
</v-card-title>
<v-card-text>
<div>
<v-icon small>
mdi-account-outline
</v-icon>
<template v-if="invite_token">
Copy Invite Token
</template>
<template v-else-if="selectedUser.id">
Edit User
</template>
<template v-else>
<!-- Invite Team -->
{{ $t('activity.inviteTeam') }}
</template>
</div>
<div class="pa-4 nc-invite-container">
<div v-if="invite_token" class="mt-6 align-center">
<v-alert
v-ripple
type="success"
outlined
class="pointer"
@click="clipboard(inviteUrl); $toast.success('Copied invite url to clipboard').goAway(3000)"
>
<template #append>
<v-icon color="green" class="ml-2">
mdi-content-copy
</v-icon>
</template>
<div class="ellipsis d-100">
{{ inviteUrl }}
</div>
</v-alert>
<p class="caption grey--text mt-3">
{{ $t('msg.info.userInviteNoSMTP') }}
<!-- Looks like you have not configured mailer yet! <br>Please copy above -->
<!-- invite -->
<!-- link and send it to -->
{{ invite_token && (invite_token.email || invite_token.emails && invite_token.emails.join(', ')) }}.
</p>
<div class="text-right">
<!--tooltip="Invite more users"-->
<x-btn
:tooltip="$t('tooltip.inviteMore')"
small
outlined
btn.class="grey--text"
@click="clickInviteMore"
>
<v-icon small color="grey" class="mr-1">
mdi-account-multiple-plus-outline
</v-icon>
<!--Invite more-->
{{ $t('activity.inviteMore') }}
</x-btn>
</div>
<!-- todo: show error message if failed-->
</div>
<template v-else>
<v-form ref="form" v-model="valid" @submit.prevent="saveUser">
<v-row class="my-0">
<v-col cols="8" class="py-0">
<!--hint="You can add multiple comma(,) separated emails"-->
<v-text-field
ref="email"
v-model="selectedUser.email"
:disabled="!!selectedUser.id"
dense
validate-on-blur
outlined
:rules="emailRules"
class="caption"
:hint="$t('msg.info.addMultipleUsers')"
label="Email"
@input="edited=true"
>
<template #label>
<span class="caption">
<!-- Email -->
{{ $t('labels.email') }}
</span>
</template>
</v-text-field>
</v-col>
<v-col cols="4" class="py-0">
<!--label="Select User Role"-->
<v-combobox
v-model="selectedRoles"
outlined
:rules="roleRules"
class="role-select caption"
hide-details="auto"
:items="roles"
:label="$t('labels.selectUserRole')"
dense
deletable-chips
@change="edited = true"
>
<template #selection="{item}">
<v-chip small :color="rolesColors[item]">
{{ item }}
</v-chip>
</template>
<template #item="{item}">
<div>
<div>{{ item }}</div>
<div class="mb-2 caption grey--text">
{{ roleDescriptions[item] }}
</div>
</div>
</template>
</v-combobox>
</v-col>
</v-row>
</v-form>
<div class="text-center mt-0">
<x-btn
v-ge="['rows','save']"
:tooltip="$t('tooltip.saveChanges')"
color="primary"
btn.class="nc-invite-or-save-btn"
@click="saveUser"
>
<v-icon small left>
{{ selectedUser.id ? 'save' : 'mdi-send' }}
</v-icon>
{{ selectedUser.id ? $t('general.save') : $t('activity.invite') }}
</x-btn>
</div>
</template>
<!-- </v-card-actions>-->
</div>
</v-card-text>
<v-card-text>
<share-base />
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { isEmail } from '~/helpers'
import { enumColor } from '~/components/project/spreadsheet/helpers/colors'
import ShareBase from '~/components/base/shareBase'
export default {
name: 'ShareOrInviteModal',
components: { ShareBase },
props: {
value: Boolean
},
data: () => ({
roles: ['creator', 'editor', 'commenter', 'viewer'],
selectedUser: {},
invite_token: null,
valid: null,
emailRules: [
v => !!v || 'E-mail is required',
(v) => {
const invalidEmails = (v || '').split(/\s*,\s*/).filter(e => !isEmail(e))
return !invalidEmails.length || `"${invalidEmails.join(', ')}" - invalid email`
}
],
roleRules: [
v => !!v || 'User Role is required',
v => ['creator', 'editor', 'commenter', 'viewer'].includes(v) || 'invalid user role'
],
userList: [],
roleDescriptions: {},
deleteUserType: ''
}),
computed: {
userEditDialog: {
get() {
return this.value
},
set(v) {
this.$emit('input', v)
}
},
inviteUrl() {
return this.invite_token ? `${location.origin}${location.pathname}#/user/authentication/signup/${this.invite_token.invite_token}` : null
},
rolesColors() {
const colors = this.$store.state.windows.darkTheme ? enumColor.dark : enumColor.light
return this.roles.reduce((o, r, i) => {
o[r] = colors[i % colors.length]
return o
}, {})
},
selectedRoles: {
get() {
return (this.selectedUser && this.selectedUser.roles ? this.selectedUser.roles.split(',') : []).sort((a, b) => this.roleNames.indexOf(a) - this.roleNames.indexOf(a))[0]
},
set(roles) {
if (this.selectedUser) {
this.selectedUser.roles = roles // .filter(Boolean).join(',')
}
}
}
},
methods: {
async saveUser() {
this.validate = true
await this.$nextTick()
if (this.loading || !this.$refs.form.validate() || !this.selectedUser) {
return
}
this.$tele.emit(`user-mgmt:add:${this.selectedUser.roles}`)
if (!this.edited) {
this.userEditDialog = false
}
try {
let data
if (this.selectedUser.id) {
await this.$api.auth.projectUserUpdate(this.$route.params.project_id, this.selectedUser.id, {
roles: this.selectedUser.roles,
email: this.selectedUser.email,
project_id: this.$route.params.project_id,
projectName: this.$store.getters['project/GtrProjectName']
})
} else {
data = (await this.$api.auth.projectUserAdd(this.$route.params.project_id, {
...this.selectedUser,
project_id: this.$route.params.project_id,
projectName: this.$store.getters['project/GtrProjectName']
}))
}
this.$toast.success('Successfully updated the user details').goAway(3000)
this.$emit('saved')
if (data && data.invite_token) {
this.invite_token = data
// todo: bring anim
// this.simpleAnim()
return
}
} catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
this.userEditDialog = false
},
clickInviteMore() {
this.$tele.emit('user-mgmt:invite-more')
this.invite_token = null
this.selectedUser = { roles: 'editor' }
},
clipboard(str) {
const el = document.createElement('textarea')
el.addEventListener('focusin', e => e.stopPropagation())
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
this.$tele.emit('user-mgmt:copy-url')
}
}
}
</script>
<style scoped>
</style>

221
packages/nc-gui/components/auth/userManagement.vue

@ -1,19 +1,19 @@
<template>
<div class="h-100">
<v-toolbar flat height="38" class="toolbar-border-bottom">
<v-toolbar flat height="38" class="mt-5">
<v-text-field
v-model="query"
style="max-width: 300px"
dense
solo
flat
solo
class="search-field caption"
hide-details
placeholder="Filter by email"
@keypress.enter="loadUsers"
>
<template #prepend-inner>
<v-icon class="mt-1" small>
<v-icon small class="mt-1">
search
</v-icon>
</template>
@ -28,7 +28,7 @@
color="primary"
small
:disabled="loading"
@click="loadUsers"
@click="clickReload"
@click.prevent
>
<v-icon small left>
@ -40,6 +40,7 @@
<!-- tooltip="Add new role" -->
<x-btn
v-if="_isUIAllowed('newUser')"
class="nc-new-user"
v-ge="['roles','add new']"
outlined
:tooltip="$t('tooltip.addRole')"
@ -56,26 +57,13 @@
</x-btn>
</v-toolbar>
<v-card style="height:calc(100% - 38px)">
<v-card style="height:calc(100% - 38px)" class="elevation-0">
<v-container style="height: 100%" fluid>
<!-- <div class="d-flex d-100 justify-center">-->
<v-row style="height:100%">
<v-col cols="12" class="h-100">
<v-card class="h-100 elevation-0">
<v-row style="height:100%">
<v-col offset="2" :cols="8" class="h-100" style="overflow-y: auto">
<!-- <v-card class="h-100 px-4 py-2">-->
<!-- <v-row>
<v-col>
</v-col>
<v-col class="flex-shrink-1 flex-grow-0"><h4 class="text-center text-capitalize mt-2 d-100"
style="min-width:100px">User List</h4></v-col>
<v-col></v-col>
</v-row>-->
<v-data-table
v-if="users"
dense
@ -116,12 +104,6 @@
<template #item="{item}">
<tr @click="selectedUser = item">
<!-- <td>
<v-radio-group dense hide-details v-model="selectedUserIndex" class="mt-n2">
<v-radio :value="index"
></v-radio>
</v-radio-group>
</td>-->
<td>{{ item.email }}</td>
<td>
<!-- {{ item.roles }}-->
@ -133,23 +115,6 @@
>
{{ getRole(item.roles) }}
</v-chip>
<!-- <v-edit-dialog-->
<!-- >-->
<!-- <div-->
<!-- :title="item.roles"-->
<!-- style="width:180px;overflow:hidden;white-space: nowrap;text-overflow:ellipsis"> {{-->
<!-- item.roles-->
<!-- }}-->
<!-- </div>-->
<!-- <template v-slot:input>-->
<!-- <v-text-field-->
<!-- v-model="item.roles"-->
<!-- label="Edit"-->
<!-- single-line-->
<!-- ></v-text-field>-->
<!-- </template>-->
<!-- </v-edit-dialog>-->
</td>
<td>
<!-- tooltip="Edit User" -->
@ -168,28 +133,18 @@
tooltip="Add user to project"
color="primary"
small
@click="inviteUser(item.email)"
@click="inviteUser(item)"
>
mdi-plus
</x-icon>
<!-- <x-icon
tooltip="Remove user from NocoDB"
class="ml-2"
color="error"
small
@click.prevent.stop="deleteId = item.id; deleteItem = item.id;showConfirmDlg = true;deleteUserType='DELETE_FROM_NOCODB'"
>
mdi-delete-forever-outline
</x-icon> -->
</span>
<!-- tooltip="Remove user from project" -->
<x-icon
v-else
:tooltip="$t('activity.deleteUser')"
class="ml-2"
color="error"
small
@click.prevent.stop="deleteId = item.id; deleteItem = item.id;showConfirmDlg = true;deleteUserType='DELETE_FROM_PROJECT'"
@click.prevent.stop="clickDeleteUser(item.id)"
>
mdi-delete-outline
</x-icon>
@ -201,7 +156,7 @@
icon-class="mt-n1"
color="primary"
small
@click.prevent.stop="rensendInvite(item.id)"
@click.prevent.stop="resendInvite(item.id)"
>
mdi-email-send-outline
</x-icon>
@ -222,7 +177,7 @@
</template>
</v-data-table>
<!-- tooltip="Add new user" -->
<div class="mt-10 text-center">
<!-- <div class="mt-10 text-center">
<x-btn
v-if="_isUIAllowed('newUser')"
v-ge="['roles','add new']"
@ -236,10 +191,10 @@
<v-icon small left>
mdi-plus
</v-icon>
<!-- New User -->
&lt;!&ndash; New User &ndash;&gt;
{{ $t('activity.newUser') }}
</x-btn>
</div>
</div>-->
<feedback-form class="mx-auto mt-6" />
</v-col>
@ -324,21 +279,9 @@
<!-- todo: move to a separate component-->
<v-dialog v-model="userEditDialog" :width="invite_token ? 700 :700" @close="invite_token = null">
<v-card v-if="selectedUser" style="min-height: 100%">
<v-card v-if="selectedUser" style="min-height: 100%" class="elevation-0">
<v-card-title>
{{ $t('activity.share') }} : {{ $store.getters['project/GtrProjectName'] }}
<!--
<h4 class="text-center text-capitalize mt-2 d-100 display-1">
<template v-if="invite_token">
Copy Invite Token
</template>
<template v-else-if="selectedUser.id">
Edit User
</template>
<template v-else>
Invite
</template>
</h4>-->
<div class="nc-header-border" />
</v-card-title>
@ -394,7 +337,7 @@
small
outlined
btn.class="grey--text"
@click="invite_token = null, selectedUser = {}"
@click="clickInviteMore"
>
<v-icon small color="grey" class="mr-1">
mdi-account-multiple-plus-outline
@ -418,7 +361,7 @@
dense
validate-on-blur
outlined
:rules="validate && emailRules"
:rules="emailRules"
class="caption"
:hint="$t('msg.info.addMultipleUsers')"
label="Email"
@ -437,6 +380,7 @@
<v-combobox
v-model="selectedRoles"
outlined
:rules="roleRules"
class="role-select caption"
hide-details="auto"
:items="roles"
@ -462,10 +406,6 @@
</v-col>
</v-row>
</v-form>
<!-- </v-card-text>
<v-card-actions class="justify-center">-->
<!-- tooltip="Save Changes" -->
<div class="text-center mt-0">
<x-btn
v-ge="['rows','save']"
@ -529,6 +469,10 @@ export default {
return !invalidEmails.length || `"${invalidEmails.join(', ')}" - invalid email`
}
],
roleRules: [
v => !!v || 'User Role is required',
v => ['creator', 'editor', 'commenter', 'viewer'].includes(v) || 'invalid user role'
],
userList: [],
roleDescriptions: {},
deleteUserType: '' // [DELETE_FROM_PROJECT, DELETE_FROM_NOCODB]
@ -599,6 +543,22 @@ export default {
this.$eventBus.$off('show-add-user', this.addUser)
},
methods: {
clickReload() {
this.loadUsers()
this.$tele.emit('user-mgmt:reload')
},
clickDeleteUser(id) {
this.$tele.emit('user-mgmt:delete:trigger')
this.deleteId = id
this.deleteItem = id
this.showConfirmDlg = true
this.deleteUserType = 'DELETE_FROM_PROJECT'
},
clickInviteMore() {
this.$tele.emit('user-mgmt:invite-more')
this.invite_token = null
this.selectedUser = { roles: 'editor' }
},
getRole(roles) {
return (roles ? roles.split(',') : []).sort((a, b) => this.roleNames.indexOf(a) - this.roleNames.indexOf(a))[0]
},
@ -650,8 +610,10 @@ export default {
el.select()
document.execCommand('copy')
document.body.removeChild(el)
this.$tele.emit('user-mgmt:copy-url')
},
async rensendInvite(id) {
async resendInvite(id) {
try {
await this.$axios.post('/admin/resendInvite/' + id, {
projectName: this.$store.getters['project/GtrProjectName']
@ -668,23 +630,34 @@ export default {
} catch (e) {
this.$toast.error(e.response.data.msg).goAway(3000)
}
this.$tele.emit('user-mgmt:resend-invite')
},
async loadUsers() {
try {
const { page = 1, itemsPerPage = 20 } = this.options
const data = (await this.$axios.get('/admin', {
headers: {
'xc-auth': this.$store.state.users.token
},
params: {
// const data = (await this.$axios.get('/admin', {
// headers: {
// 'xc-auth': this.$store.state.users.token
// },
// params: {
// limit: itemsPerPage,
// offset: (page - 1) * itemsPerPage,
// query: this.query,
// project_id: this.$route.params.project_id
// }
// })).data
const userData = (await this.$api.auth.projectUserList(this.$store.state.project.projectId, {
query: {
limit: itemsPerPage,
offset: (page - 1) * itemsPerPage,
query: this.query,
project_id: this.$route.params.project_id
query: this.query
}
})).data
this.count = data.count
this.users = data.list
}))
this.count = userData.users.pageInfo.totalRows
this.users = userData.users.list
if (!this.selectedUser && this.users && this.users[0]) {
this.selectedUserIndex = 0
}
@ -694,33 +667,27 @@ export default {
},
async loadRoles() {
try {
this.roles = (await this.$axios.get('/admin/roles', {
headers: {
'xc-auth': this.$store.state.users.token
},
params: {
project_id: this.$route.params.project_id
}
})).data.map((role) => {
this.roleDescriptions[role.title] = role.description
return role.title
}).filter(role => role !== 'guest')
this.roles = ['creator', 'editor', 'commenter', 'viewer']
// todo:
// (await this.$axios.get('/admin/roles', {
// headers: {
// 'xc-auth': this.$store.state.users.token
// },
// params: {
// project_id: this.$route.params.project_id
// }
// })).data.map((role) => {
// this.roleDescriptions[role.title] = role.description
// return role.title
// }).filter(role => role !== 'guest')
} catch (e) {
console.log(e)
}
},
async deleteUser(id, type) {
try {
await this.$axios.delete('/admin/' + id, {
params: {
project_id: this.$route.params.project_id,
email: this.deleteItem.email,
type
},
headers: {
'xc-auth': this.$store.state.users.token
}
})
await this.$api.auth.projectUserRemove(this.$route.params.project_id, id)
this.$toast.success(`Successfully removed the user from ${type === 'DELETE_FROM_PROJECT' ? 'project' : 'NocoDB'}`).goAway(3000)
await this.loadUsers()
} catch (e) {
@ -734,6 +701,8 @@ export default {
}
await this.deleteUser(this.deleteId, this.deleteUserType)
this.showConfirmDlg = false
this.$tele.emit('user-mgmt:delete:submit')
},
addUser() {
this.invite_token = null
@ -741,23 +710,19 @@ export default {
roles: 'editor'
}
this.userEditDialog = true
this.$tele.emit('user-mgmt:add-user:trigger')
},
async inviteUser(email) {
async inviteUser(item) {
try {
await this.$axios.post('/admin', {
email,
project_id: this.$route.params.project_id,
projectName: this.$store.getters['project/GtrProjectName']
}, {
headers: {
'xc-auth': this.$store.state.users.token
}
})
await this.$api.auth.projectUserAdd(this.$route.params.project_id, item)
this.$toast.success('Successfully added user to project').goAway(3000)
await this.loadUsers()
} catch (e) {
this.$toast.error(e.response.data.msg).goAway(3000)
}
this.$tele.emit('user-mgmt:invite-user')
},
async saveUser() {
this.validate = true
@ -765,6 +730,7 @@ export default {
if (this.loading || !this.$refs.form.validate() || !this.selectedUser) {
return
}
this.$tele.emit(`user-mgmt:add:${this.selectedUser.roles}`)
if (!this.edited) {
this.userEditDialog = false
@ -773,31 +739,23 @@ export default {
try {
let data
if (this.selectedUser.id) {
await this.$axios.put('/admin/' + this.selectedUser.id, {
await this.$api.auth.projectUserUpdate(this.$route.params.project_id, this.selectedUser.id, {
roles: this.selectedUser.roles,
email: this.selectedUser.email,
project_id: this.$route.params.project_id,
projectName: this.$store.getters['project/GtrProjectName']
}, {
headers: {
'xc-auth': this.$store.state.users.token
}
})
} else {
data = await this.$axios.post('/admin', {
data = (await this.$api.auth.projectUserAdd(this.$route.params.project_id, {
...this.selectedUser,
project_id: this.$route.params.project_id,
projectName: this.$store.getters['project/GtrProjectName']
}, {
headers: {
'xc-auth': this.$store.state.users.token
}
})
}))
}
this.$toast.success('Successfully updated the user details').goAway(3000)
await this.loadUsers()
if (data && data.data && data.data.invite_token) {
this.invite_token = data.data
if (data && data.invite_token) {
this.invite_token = data
this.simpleAnim()
return
}
@ -805,7 +763,6 @@ export default {
this.$toast.error(e.response.data.msg).goAway(3000)
}
this.userEditDialog = false
await this.loadUsers()
}
}

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

@ -1,7 +1,7 @@
<template>
<div class="h-100 nc-auth-tab">
<div class="h-100" style="width: 100%">
<v-tabs height="30" color="x-active">
<v-tabs height="40" color="x-active">
<v-tab>
<span class="caption text-capitalize">
<!-- Users Management -->
@ -23,19 +23,6 @@
<api-tokens :nodes="nodes" />
</v-tab-item>
</template>
<v-tab>
<span class="caption text-capitalize">
<!-- Roles Management -->
{{ $t('title.rolesMgmt') }}
</span>
</v-tab>
<v-tab-item>
<roles :nodes="nodes" />
</v-tab-item>
<!-- <v-tab><span class="caption text-capitalize">Auth Management</span></v-tab>
<v-tab-item>
<auth-hooks :nodes="nodes"></auth-hooks>
</v-tab-item>-->
</v-tabs>
</div>
</div>

59
packages/nc-gui/components/base/shareBase.vue

@ -8,14 +8,14 @@
{{ $t('activity.shareBase.link') }}
</span>
<div class="nc-container">
<v-chip v-if="base.enabled" :color="colors[4]" style="" class="rounded pl-1 pr-0 d-100 nc-url-chip pr-3">
<v-chip v-if="base.uuid" :color="colors[4]" style="" class="rounded pl-1 pr-0 d-100 nc-url-chip pr-3">
<div class="nc-url-wrapper d-flex mx-1 align-center d-100">
<span class="nc-url flex-grow-1 caption ">{{ url }}</span>
<v-spacer />
<v-divider vertical />
<!-- tooltip="reload" -->
<x-icon
<x-icon
:tooltip="$t('general.reload')"
@click="recreate"
>
@ -23,7 +23,7 @@
</x-icon>
<!-- tooltip="copy URL" -->
<x-icon
<x-icon
:tooltip="$t('activity.copyUrl')"
@click="copyUrl"
>
@ -31,7 +31,7 @@
</x-icon>
<!-- tooltip="open new tab" -->
<x-icon
<x-icon
:tooltip="$t('activity.openTab')"
@click="navigateToSharedBase"
>
@ -39,7 +39,7 @@
</x-icon>
<!-- tooltip="copy embeddable HTML code" -->
<x-icon
<x-icon
:tooltip="$t('activity.iFrame')"
@click="generateEmbeddableIframe"
>
@ -54,7 +54,7 @@
<template #activator="{on}">
<div class="my-2" v-on="on">
<div class="font-weight-bold nc-disable-shared-base">
<span v-if="base.enabled">
<span v-if="base.uuid">
<!-- Anyone with the link -->
{{ $t('activity.shareBase.enable') }}
</span>
@ -69,7 +69,7 @@
</div>
</template>
<v-list dense>
<v-list-item dense @click="createSharedBase('viewer')">
<v-list-item v-if="!base.uuid" dense @click="createSharedBase('viewer')">
<v-list-item-title>
<v-icon small class="mr-1">
mdi-link-variant
@ -80,14 +80,14 @@
</span>
</v-list-item-title>
</v-list-item>
<v-list-item dense @click="disableSharedBase">
<v-list-item v-if="base.uuid" dense @click="disableSharedBase">
<v-list-item-title>
<v-icon small class="mr-1">
mdi-link-variant-off
</v-icon>
<span class="caption">
<!-- Disable shared base -->
{{ $t('activity.shareBase.link') }}
{{ $t('activity.shareBase.disable') }}
</span>
</v-list-item-title>
</v-list-item>
@ -112,9 +112,12 @@
</div>
<v-spacer />
<div class="d-flex justify-center" style="width:120px">
<v-menu v-if="base.enabled" offset-y>
<v-menu v-if="base.uuid" offset-y>
<template #activator="{on}">
<div class="text-capitalize my-2 font-weight-bold backgroundColorDefault py-2 px-4 rounded nc-shared-base-role" v-on="on">
<div
class="text-capitalize my-2 font-weight-bold backgroundColorDefault py-2 px-4 rounded nc-shared-base-role"
v-on="on"
>
{{ base.roles || 'Viewer' }}
<v-icon small>
@ -158,7 +161,7 @@ export default {
}),
computed: {
url() {
return this.base && this.base.shared_base_id ? `${this.dashboardUrl}#/nc/base/${this.base.shared_base_id}` : null
return this.base && this.base.uuid ? `${this.dashboardUrl}#/nc/base/${this.base.uuid}` : null
}
},
mounted() {
@ -167,8 +170,10 @@ export default {
methods: {
async loadSharedBase() {
try {
const sharedBase = await this.$store.dispatch('sqlMgr/ActSqlOp', [
{ dbAlias: 'db' }, 'getSharedBaseLink'])
// const sharedBase = await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { dbAlias: 'db' }, 'getSharedBaseLink'])
const sharedBase = (await this.$api.project.sharedBaseGet(this.$store.state.project.projectId))
this.base = sharedBase || {}
} catch (e) {
console.log(e)
@ -176,35 +181,46 @@ export default {
},
async createSharedBase(roles = 'viewer') {
try {
const sharedBase = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: 'db' }, 'createSharedBaseLink', { roles }])
// const sharedBase = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: 'db' }, 'createSharedBaseLink', { roles }])
const sharedBase = (await this.$api.project.sharedBaseUpdate(this.$store.state.project.projectId, { roles }))
this.base = sharedBase || {}
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
this.$tele.emit(`shared-base:enable:${roles}`)
},
async disableSharedBase() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: 'db' }, 'disableSharedBaseLink'])
await this.$api.project.sharedBaseDisable(this.$store.state.project.projectId)
this.base = {}
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
this.$tele.emit('shared-base:disable')
},
async recreate() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: 'db' }, 'disableSharedBaseLink'])
const sharedBase = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: 'db' }, 'createSharedBaseLink'])
const sharedBase = (await this.$api.project.sharedBaseCreate(this.$store.state.project.projectId, { roles: this.base.roles || 'viewer' }))
this.base = sharedBase || {}
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
this.$tele.emit('shared-base:recreate')
},
copyUrl() {
copyTextToClipboard(this.url)
this.$toast.success('Copied shareable base url to clipboard!').goAway(3000)
this.$tele.emit('shared-base:copy-url')
},
navigateToSharedBase() {
window.open(this.url, '_blank')
this.$tele.emit('shared-base:open-url')
},
generateEmbeddableIframe() {
copyTextToClipboard(`<iframe
@ -215,6 +231,8 @@ width="100%"
height="700"
style="background: transparent; border: 1px solid #ddd"></iframe>`)
this.$toast.success('Copied embeddable html code!').goAway(3000)
this.$tele.emit('shared-base:copy-embed-frame')
}
}
@ -239,5 +257,8 @@ style="background: transparent; border: 1px solid #ddd"></iframe>`)
background: var(--v-backgroundColor-base);
padding: 20px 20px;
}
/deep/ .nc-url-chip .v-chip__content{width: 100%}
/deep/ .nc-url-chip .v-chip__content {
width: 100%
}
</style>

558
packages/nc-gui/components/createOrEditProject.vue

@ -74,81 +74,6 @@
<div ref="panelContainer" style="">
<api-overlay v-show="projectReloading" :project-created="projectCreated" />
<!-- <v-overlay absolute color="grey" opacity="0.4"-->
<!-- class="create-project-overlay">-->
<!--<div>
<v-card dark style="width: 100%; max-height:100%;overflow: auto">
<v-container fluid style="min-height:200px;">
&lt;!&ndash; <v-row class="text-center">&ndash;&gt;
&lt;!&ndash; <v-col cols="12">&ndash;&gt;
<v-card class="pa-2 text-center elevation-10" dark>
<h3 class="title mb-3 mt-4">APIs Generated</h3>
<p><span class="display-2 font-weight-bold">1,200,000</span><br>
<span class="subtitle grey&#45;&#45;text text&#45;&#45;lighten-1">within 60 seconds</span></p>
</v-card>
&lt;!&ndash; </v-col>&ndash;&gt;
&lt;!&ndash; </v-row>&ndash;&gt;
<v-row>
<v-col>
<v-card dark class=" elevation-10">
<v-card-text class="pb-0 font-weight-bold"># Databases</v-card-text>
<v-card-text class="title white&#45;&#45;text">
<v-icon class="mt-n2 mr-1" color="info">mdi-database-sync</v-icon>
180/190
</v-card-text>
</v-card>
</v-col>
<v-col>
<v-card dark class=" elevation-10">
<v-card-text class="pb-0 font-weight-bold"># Tables</v-card-text>
<v-card-text class="title white&#45;&#45;text">
<v-icon class="mr-1 mt-n1 " color="info">mdi-table-large</v-icon>
50000
</v-card-text>
</v-card>
</v-col>
<v-col>
<v-card dark class=" elevation-10">
<v-card-text class="pb-0 font-weight-bold">Time Saved</v-card-text>
<v-card-text class="title white&#45;&#45;text">
<v-icon class="mr-1 mt-n1" color="secondary">mdi-clock-fast</v-icon>
10hrs
</v-card-text>
</v-card>
</v-col>
<v-col>
<v-card dark class=" elevation-10">
<v-card-text class="pb-0 font-weight-bold">CostSaved</v-card-text>
<v-card-text class="title white&#45;&#45;text">
<v-icon color="success" class="mt-n2 mr-1">mdi-currency-usd-circle</v-icon>
100$
</v-card-text>
</v-card>
</v-col>
</v-row>
&lt;!&ndash; <v-divider class="my-3 "></v-divider>&ndash;&gt;
<v-card dark class=" elevation-10">
<p class="title text-center pt-2">List of Database</p>
<v-simple-table style="width: 100%">
<tr v-for="i in 40">
<td v-for="j in 5" class="py-2 px-3">
<div class="d-flex">
<v-icon color="green" x-small class="mr-2">mdi-moon-full</v-icon>
<span class="caption">Database {{ i }} {{ j }}</span>
<v-spacer></v-spacer>
</div>
</td>
</tr>
</v-simple-table>
</v-card>
</v-container>
</v-card>
</div>-->
<!-- </v-overlay>-->
<v-container fluid>
<v-row>
<v-col cols="12" class="mb-0 pb-0">
@ -162,53 +87,8 @@
:height="20"
:label="$t('placeholder.projName')"
autofocus
>
<!-- <v-icon color="info" class="blink_me mt-n1" slot="prepend">-->
<!-- mdi-lightbulb-on-->
<!-- </v-icon>-->
</v-text-field>
<!-- Access Project via -->
<label class="caption"> {{ $t('msg.info.apiOptions') }}</label>
<v-radio-group
v-model="project.projectType"
row
hide-details
dense
class="mb-0 mt-0"
>
<v-radio
v-for="(type, i) in projectTypes"
:key="type.value"
:color="type.iconColor"
:value="type.value"
>
<template #label>
<v-chip :color="i ? colors[3] : colors[7]">
<v-icon small class="mr-1">
{{ type.icon }}
</v-icon>
<span class="caption">{{ type.text }}</span>
</v-chip>
</template>
</v-radio>
</v-radio-group>
/>
</div>
<!-- <v-select
v-model="project.projectType" hint="Access via API type" persistent-hint dense
:items="projectTypes">
<template v-slot:prepend>
<img v-if="typeIcon.type === 'img'" :src="typeIcon.icon" style="width: 32px">
<v-icon v-else :color="typeIcon.iconColor">{{ typeIcon.icon }}</v-icon>
</template>
<template v-slot:item="{item}">
<span class="caption d-flex align-center">
<img v-if="item.type === 'img'" :src="item.icon" style="width: 30px">
<v-icon v-else :color="item.iconColor">{{ item.icon }}</v-icon> &nbsp; {{ item.text }}</span>
</template>
</v-select>-->
</v-col>
<v-col
@ -217,10 +97,6 @@
offset="1"
:class="{ 'mt-0 pt-0': !edit, 'mt-3 pt-3': edit }"
>
<!-- <h2 :class="{'text-center mb-2':!edit,'text-center mb-2 grey&#45;&#45;text':edit}">
{{ project.title && project.title.toUpperCase() }}'s
Environments</h2> -->
<p
:class="{
'text-center mb-2 mt-3': !edit,
@ -244,8 +120,6 @@
>
<v-expansion-panel-header disable-icon-rotate>
<p class="pa-0 ma-0">
<!-- <v-icon>mdi-test-tube</v-icon> &nbsp;-->
<!-- <span class="title">&nbsp;<b>'{{ envKey }}'</b> environment : </span>-->
<v-tooltip v-for="(db, tabIndex) in envData.db" :key="tabIndex" bottom>
<template #activator="{ on }">
<v-icon
@ -321,21 +195,6 @@
db.connection.database
}}</span>
</v-tab>
<!-- <v-tooltip bottom>
<template v-slot:activator="{ on }">
<x-btn tooltip="Add New Database to Environment" text small class="ma-2" v-on="on"
@click.prevent.stop="addNewDB(envKey,panelIndex)"
v-ge="['project','env-db-add']"
>
<v-hover v-slot:default="{ hover }">
<v-icon :color="hover ? 'primary' : 'grey'">mdi-database-plus
</v-icon>
</v-hover>
</x-btn>
</template>
<span>Add new database to '{{ envKey }}' environment</span>
</v-tooltip>
-->
<v-tabs-items v-model="databases[panelIndex]">
<v-tab-item
v-for="(db, dbIndex) in project.envs[envKey].db"
@ -422,14 +281,6 @@
>
{{ data.item }}
</v-chip>
<!-- <div class="d-flex flex-column mx-auto "-->
<!-- style="width:100%;border-bottom: 1px solid #ddd">-->
<!-- <img class="mx-auto py-3" width="80" :src="dbIcons[data.item]"/>-->
<!-- &lt;!&ndash; {{ data.item }}&ndash;&gt;-->
<!-- <p v-if="!databaseNames[data.item]" class="text-center grey&#45;&#45;text">-->
<!-- Coming soon</p>-->
<!-- </div>-->
</template>
</v-select>
</v-col>
@ -523,6 +374,7 @@
:label="$t('labels.dbCreateIfNotExists')"
/>
</v-col>
<!-- todo : ssl & inflection -->
<v-col v-if="db.client !== 'sqlite3'" class="">
<v-expansion-panels>
<v-expansion-panel style="border: 1px solid wheat">
@ -649,7 +501,7 @@
<v-col>
<!-- Inflection - Table name -->
<v-select
v-model="db.meta.inflection.tn"
v-model="db.meta.inflection.table_name"
:disabled="edit"
class="caption"
:label="
@ -667,7 +519,7 @@
<v-col>
<!-- Inflection - Column name -->
<v-select
v-model="db.meta.inflection.cn"
v-model="db.meta.inflection.column_name"
:disabled="edit"
class="caption"
:label="
@ -775,158 +627,8 @@
</v-col>
</v-expansion-panel-content>
</v-expansion-panel>
<!-- <v-expansion-panel>
<v-expansion-panel-header disable-icon-rotate>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<x-btn tooltip="Add New Environment to Project" color="grey" block v-on="on" outlined
v-ge="['project','env-add']"
@click.stop="addNewEnvironment">
<v-icon>mdi-plus</v-icon>
Add Another Environment
</x-btn>
</template>
<span>Add new environment to {{ project.title }} project</span>
</v-tooltip>
<template v-slot:actions>
<i></i>
</template>
</v-expansion-panel-header>
</v-expansion-panel>-->
</v-expansion-panels>
</v-col>
<!-- <v-col cols="10" offset="1" v-show="isTitle"
:class="{'mt-0 pt-0':!edit,'mt-3 pt-3':edit}">
&lt;!&ndash; <h2 :class="{'text-center mb-2':!edit,'text-center mb-2 grey&#45;&#45;text':edit}">&ndash;&gt;
&lt;!&ndash; Advanced Configuration</h2>&ndash;&gt;
<v-expansion-panels focusable accordion="" class="elevation-20"
style="border: 1px solid grey">
<v-expansion-panel
@change="onAdvancePanelToggle"
>
<v-expansion-panel-header disable-icon-rotate>
<p class="pa-0 ma-0">
<v-icon class="mt-n2 " color="grey darken-1">mdi-cog</v-icon> &nbsp;
<span class="grey&#45;&#45;text text&#45;&#45;darken-1">Advance Configuration</span>
</p>
</v-expansion-panel-header>
<v-expansion-panel-content eager>
<v-card class="mt-3">
<v-card-title>
<v-icon class="mr-2">mdi-shield-account-outline</v-icon>
Authentication Configuration
</v-card-title>
<v-card-text>
<v-container class="justify-center">
<v-row>
<v-col cols="12" class="py-0">
<v-select
v-model="auth.authType" hint="Choose Authentication type"
persistent-hint dense
:items="authTypes">
<template v-slot:item="{item}">
<span class="caption">
{{ item.text }}</span>
</template>
</v-select>
</v-col>
<v-col cols="12" class="py-0" v-if="auth.authType && auth.authType !== 'none'">
<v-text-field
v-if="auth.authType !== 'middleware'"
v-model="auth.authSecret"
label="Enter Auth Secret (Randomly generated)"
:type="showSecret ? 'text' : 'password'"
>
<template v-slot:append>
<v-icon small
@click="showSecret = !showSecret"
>{{
showSecret ? 'visibility_off' :
'visibility'
}}
</v-icon>
</template>
</v-text-field>
<v-text-field
v-else
v-model="auth.webhook"
label="Webhook url"
>
</v-text-field>
</v-col>
</v-row>
</v-container>
</v-card-text>
</v-card>
&lt;!&ndash; </v-expansion-panel-content>&ndash;&gt;
&lt;!&ndash; </v-expansion-panel>&ndash;&gt;
&lt;!&ndash; </v-expansion-panels>&ndash;&gt;
&lt;!&ndash; </v-col>&ndash;&gt;
&lt;!&ndash; <v-col cols="10" offset="1" v-show="project.title.trim().length"&ndash;&gt;
&lt;!&ndash; :class="{'mt-0 pt-0':!edit,'mt-3 pt-3':edit}">&ndash;&gt;
&lt;!&ndash; <v-expansion-panels v-model="smtpPanel" focusable accordion="" class="elevation-20"&ndash;&gt;
&lt;!&ndash; style="border: 1px solid grey">&ndash;&gt;
&lt;!&ndash; <v-expansion-panel>&ndash;&gt;
&lt;!&ndash; <v-expansion-panel-header disable-icon-rotate>&ndash;&gt;
&lt;!&ndash; <p class="pa-0 ma-0">&ndash;&gt;
&lt;!&ndash; <v-icon class="mt-n2 " color="grey darken-1">mdi-email-edit</v-icon> &nbsp;&ndash;&gt;
&lt;!&ndash; <span class="grey&#45;&#45;text text&#45;&#45;darken-1">SMTP Configuration</span>&ndash;&gt;
&lt;!&ndash; </p>&ndash;&gt;
&lt;!&ndash; </v-expansion-panel-header>&ndash;&gt;
&lt;!&ndash; <v-expansion-panel-content :eager="false">&ndash;&gt;
<v-card class="mt-3">
<v-card-title>
<v-icon class="mr-2">mdi-email-edit</v-icon>
SMTP Configuration
</v-card-title>
<v-card-text>
<v-text-field
v-model="smtpConfiguration.from"
label="From address"
placeholder="Company<noreply@company.com>"
>
</v-text-field>
<label>Mailer Config Options<span class="caption"> (For connection example visit: <a
href="https://nodemailer.com/smtp/#examples">https://nodemailer.com/smtp/#examples</a>)</span></label>
<monaco-json-editor
ref="monacoEditor"
v-model="smtpConfiguration.options"
style="height: 300px; width:100% "></monaco-json-editor>
</v-card-text>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
<v-col cols="10" offset="1" v-show="isTitle"
:class="{'mt-0 pt-0':!edit,'mt-3 pt-3':edit}">
<create-project-coming-soon></create-project-coming-soon>
</v-col>-->
</v-row>
</v-container>
</div>
@ -941,8 +643,8 @@
:type="dialog.type"
/>
<!-- heading="Connection was successful" -->
<!-- ok-label="Ok & Save Project" -->
<!-- heading="Connection was successful" -->
<!-- ok-label="Ok & Save Project" -->
<dlg-ok-new
v-model="testSuccess"
:heading="$t('msg.info.dbConnected')"
@ -999,7 +701,12 @@ import colors from '@/mixins/colors'
import DlgOkNew from '@/components/utils/dlgOkNew'
import readFile from '@/helpers/fileReader'
const { uniqueNamesGenerator, starWars, adjectives, animals } = require('unique-names-generator')
const {
uniqueNamesGenerator,
starWars,
adjectives,
animals
} = require('unique-names-generator')
const homeDir = ''
@ -1052,14 +759,36 @@ export default {
projectReloading: false,
enableDbEdit: 0,
authTypes: [
{ text: 'JWT', value: 'jwt' },
{ text: 'Master Key', value: 'masterKey' },
{ text: 'Middleware', value: 'middleware' },
{ text: 'Disabled', value: 'none' }
{
text: 'JWT',
value: 'jwt'
},
{
text: 'Master Key',
value: 'masterKey'
},
{
text: 'Middleware',
value: 'middleware'
},
{
text: 'Disabled',
value: 'none'
}
],
projectTypes: [
{ text: 'REST APIs', value: 'rest', icon: 'mdi-code-json', iconColor: 'green' },
{ text: 'GRAPHQL APIs', value: 'graphql', icon: 'mdi-graphql', iconColor: 'pink' }
{
text: 'REST APIs',
value: 'rest',
icon: 'mdi-code-json',
iconColor: 'green'
},
{
text: 'GRAPHQL APIs',
value: 'graphql',
icon: 'mdi-graphql',
iconColor: 'pink'
}
],
showPass: {},
@ -1115,8 +844,8 @@ export default {
graphqlDepthLimit: 10
},
inflection: {
tn: ['camelize'],
cn: ['camelize']
table_name: 'camelize',
column_name: 'camelize'
}
},
ui: {
@ -1374,7 +1103,10 @@ export default {
if (this.project.projectType) {
return this.projectTypes.find(({ value }) => value === this.project.projectType)
} else {
return { icon: 'mdi-server', iconColor: 'primary' }
return {
icon: 'mdi-server',
iconColor: 'primary'
}
}
},
databaseNamesReverse() {
@ -1415,19 +1147,6 @@ export default {
},
selectFile(db, obj, key, index) {
this.$refs[key][index].click()
// console.log(obj, key);
// const file = dialog.showOpenDialog({
// properties: ["openFile"]
// });
// console.log(typeof file, file, typeof file[0]);
// if (file && file[0]) {
// let fileName = path.basename(file[0]);
// db.ui[obj][key] = fileName;
// Vue.set(db.ui[obj], key, fileName)
// //db.connection[obj][key] = file[0].toString();
// Vue.set(db.connection[obj], key, file[0].toString())
// }
},
onPanelToggle(panelIndex, envKey) {
this.$nextTick(() => {
@ -1455,20 +1174,15 @@ export default {
Vue.set(this.databases, panelIndex, tabIndex)
},
getProjectJson() {
console.log('Project json before creating', this.project)
/**
* remove UI keys within project
*/
const xcConfig = JSON.parse(JSON.stringify(this.project))
console.log(JSON.stringify(this.project))
console.log('Project json after parsing', xcConfig)
delete xcConfig.ui
for (const env in xcConfig.envs) {
for (let i = 0; i < xcConfig.envs[env].db.length; ++i) {
xcConfig.envs[env].db[i].meta.api.type = this.project.projectType
console.log('getProjectJson:', env, i, xcConfig.envs[env].db[i])
if (
xcConfig.envs[env].db[i].client === 'mysql' ||
xcConfig.envs[env].db[i].client === 'mysql2'
@ -1495,15 +1209,15 @@ export default {
const inflectionObj = xcConfig.envs[env].db[i].meta.inflection
if (inflectionObj) {
if (Array.isArray(inflectionObj.tn)) {
inflectionObj.tn = inflectionObj.tn.join(',')
if (Array.isArray(inflectionObj.table_name)) {
inflectionObj.table_name = inflectionObj.table_name.join(',')
}
if (Array.isArray(inflectionObj.cn)) {
inflectionObj.cn = inflectionObj.cn.join(',')
if (Array.isArray(inflectionObj.column_name)) {
inflectionObj.column_name = inflectionObj.column_name.join(',')
}
inflectionObj.tn = inflectionObj.tn || 'none'
inflectionObj.cn = inflectionObj.cn || 'none'
inflectionObj.table_name = inflectionObj.table_name || 'none'
inflectionObj.column_name = inflectionObj.column_name || 'none'
}
if (this.allSchemas) {
@ -1563,14 +1277,10 @@ export default {
}
}
console.log('Project json : after', xcConfig)
return xcConfig
},
constructProjectJsonFromProject(project) {
// const {projectJson: envs, ...rest} = project;
// let p = {...rest, ...envs};
const p = project // JSON.parse(JSON.stringify(project.projectJson));
p.ui = {
@ -1651,50 +1361,49 @@ export default {
this.projectReloading = true
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [
{
query: {
skipProjectHasDb: 1
}
},
this.edit ? 'projectUpdateByWeb' : 'projectCreateByWeb',
{
project: {
title: projectJson.title,
folder: 'config.xc.json',
type: 'pg'
},
projectJson
const con = projectJson.envs._noco.db[0]
const inflection = (con.meta && con.meta.inflection) || {}
try {
const result = (await this.$api.project.create({
title: projectJson.title,
bases: [{
type: con.client,
config: con,
inflection_column: inflection.column_name,
inflection_table: inflection.table_name
}],
external: true
}))
clearInterval(interv)
toast.goAway(100)
await this.$store.dispatch('project/ActLoadProjectInfo')
this.projectReloading = false
if (!this.edit && !this.allSchemas) {
this.$router.push({
path: `/nc/${result.id}`,
query: {
new: 1
}
})
}
])
clearInterval(interv)
toast.goAway(100)
console.log('project created redirect to project page', projectJson, result)
await this.$store.dispatch('project/ActLoadProjectInfo')
this.projectReloading = false
if (!this.edit && !this.allSchemas) {
this.$router.push({
path: `/nc/${result.id}`,
query: {
new: 1
}
})
this.projectCreated = true
} catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
toast.goAway(0)
}
this.projectCreated = true
this.projectReloading = false
this.$tele.emit('project:create:extdb:submit')
},
mtdDialogGetEnvNameSubmit(envName, cookie) {
console.log(envName)
this.dialogGetEnvName.dialogShow = false
if (envName in this.project.envs) {
console.log('Environment exists')
} else {
Vue.set(this.project.envs, envName, {
db: [
@ -1711,8 +1420,8 @@ export default {
tn: 'nc_evolutions',
dbAlias: 'db',
inflection: {
tn: 'camelize',
cn: 'camelize'
table_name: 'camelize',
column_name: 'camelize'
},
api: {
type: ''
@ -1734,7 +1443,6 @@ export default {
}
},
mtdDialogGetEnvNameCancel() {
console.log('mtdDialogGetTableNameCancel cancelled')
this.dialogGetEnvName.dialogShow = false
},
@ -1761,8 +1469,8 @@ export default {
tn: 'nc_evolutions',
dbAlias,
inflection: {
tn: 'camelize',
cn: 'camelize'
table_name: 'camelize',
column_name: 'camelize'
},
api: {
type: ''
@ -1786,24 +1494,8 @@ export default {
this.dialog.show = false
},
selectDir(ev) {
// console.log(ev)
// const file = dialog.showOpenDialog({
// properties: ['openDirectory']
// })
// if (file && file[0]) {
// this.baseFolder = file[0]
// this.project.folder = file[0]
// this.userSelectedDir = true
// }
},
selectSqliteFile(db) {
// console.log(ev)
// const file = dialog.showOpenDialog({
// properties: ["openFile"]
// });
// if (file && file[0]) {
// db.connection.connection.filename = file[0];
// }
},
getDbStatusColor(db) {
@ -1838,7 +1530,6 @@ export default {
}
},
async newTestConnection(db, env, panelIndex) {
console.log(this.project.envs[env][0])
if (
db.connection.host === 'localhost' &&
!this.edit &&
@ -1869,7 +1560,6 @@ export default {
'testConnection',
c1
])
console.log('test connection result', result)
if (result.code === 0) {
db.ui.setup = 1
@ -1884,10 +1574,11 @@ export default {
if (e === env) {
// ignore
} else {
console.log(this.project.envs[e])
const c2 = {
connection: { ...this.project.envs[e].db[0].connection, database: undefined },
connection: {
...this.project.envs[e].db[0].connection,
database: undefined
},
client: this.project.envs[e].db[0].client
}
@ -1934,46 +1625,31 @@ export default {
}
let sendAdvancedConfig = false
const sslOptions = Object.values(connection.ssl).filter(el => !!el)
console.log('sslOptions:', sslOptions)
if (sslOptions[0]) {
sendAdvancedConfig = true
} else {
console.log('no ssl options')
}
return sendAdvancedConfig
},
handleSSL(db, creating = true) {
console.log('handleSSL', db)
const sendAdvancedConfig = this.sendAdvancedConfig(db.connection)
if (!sendAdvancedConfig) {
// args.ssl = undefined;
db.connection.ssl = undefined
}
if (db.connection.ssl) {
// db.connection.ssl.caFilePath = db.connection.ssl.ca;
// db.connection.ssl.keyFilePath = db.connection.ssl.key;
// db.connection.ssl.certFilePath = db.connection.ssl.cert;
// if(creating) {
// delete db.connection.ssl.ca;
// delete db.connection.ssl.key;
// delete db.connection.ssl.cert;
// }
}
},
getDatabaseForTestConnection(dbType) {
},
async testConnection(db, env, panelIndex) {
this.$tele.emit('project:create:extdb:test-connection')
this.$store.commit('notification/MutToggleProgressBar', true)
try {
if (!(await this.newTestConnection(db, env, panelIndex))) {
// this.activeDbNode.testConnectionStatus = false;
//
this.handleSSL(db)
console.log('testconnection params', db)
if (db.client === 'sqlite3') {
db.ui.setup = 1
} else {
@ -1985,18 +1661,8 @@ export default {
client: db.client
}
// const result = await this.sqlMgr.testConnection(c1);
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [
{
query: {
skipProjectHasDb: 1
}
},
'testConnection',
c1
])
const result = (await this.$api.utils.testConnection(c1))
console.log('test connection result', result)
if (result.code === 0) {
db.ui.setup = 1
// this.dialog.heading = "Connection was successful"
@ -2010,8 +1676,6 @@ export default {
this.dialog.type = 'error'
this.dialog.show = true
}
console.log('testconnection params : after', db)
}
}
} catch (e) {
@ -2038,7 +1702,10 @@ export default {
const db = this.project.envs[env].db[index]
Vue.set(db, 'client', this.databaseNames[client])
if (client !== 'Sqlite') {
const { ssl, ...connectionDet } = this.sampleConnectionData[client]
const {
ssl,
...connectionDet
} = this.sampleConnectionData[client]
Vue.set(db, 'connection', {
...connectionDet,
@ -2064,11 +1731,8 @@ export default {
database: [this.project.folder, `${this.project.title}_${env}_${index + 1}`].join(
'/'
),
// database: path.join(this.project.folder, `${this.project.title}_${env}_${index + 1}`),
useNullAsDefault: true
})
// Vue.set(db.connection, 'connection', {filename: `${this.project.folder}/${this.project.title}_${env}_${index + 1}`})
// Vue.set(db.connection, 'database', `${this.project.folder}/${this.project.title}_${env}_${index + 1}`)
}
}
}
@ -2082,8 +1746,6 @@ export default {
db.ui.setup = status
},
removeDBFromEnv(db, env, panelIndex, dbIndex) {
console.log(db, env, panelIndex, dbIndex)
for (const env in this.project.envs) {
if (this.project.envs[env].db.length > dbIndex) {
this.project.envs[env].db.splice(dbIndex, 1)
@ -2095,7 +1757,10 @@ export default {
Vue.set(this.project, 'envs', { ...this.project.envs })
}
},
fetch({ store, params }) {
fetch({
store,
params
}) {
},
beforeCreated() {
},
@ -2115,12 +1780,6 @@ export default {
if (db.client !== 'sqlite3') {
Vue.set(db.connection, 'database', `${this.project.title}_${env}_${index + 1}`)
} else {
// Vue.set(db.connection, 'connection', {
// filename: path.join(
// this.project.folder,
// `${this.project.title}_${env}_${index + 1}`
// )
// })
Vue.set(db.connection, 'database', `${this.project.title}_${env}_${index + 1}`)
}
}
@ -2168,12 +1827,9 @@ export default {
this.compErrorMessages[Math.floor(Math.random() * this.compErrorMessages.length)]
if (this.edit) {
// this.edit = true;
// await this.$store.dispatch('sqlMgr/instantiateSqlMgr');
try {
let data = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcProjectGetConfig'])
data = JSON.parse(data.config)
console.log('created:', data)
this.constructProjectJsonFromProject(data)
this.$set(this.project, 'folder', data.folder)
} catch (e) {
@ -2215,17 +1871,9 @@ export default {
...this.sampleConnectionData[dbsAvailable[0]],
ssl: { ...this.sampleConnectionData[dbsAvailable[0]].ssl }
}
// db.ui.setup = await PortScanner.isAlive(db.connection.host, parseInt(db.connection.port)) ? 0 : -1;
}
}
}
// for (let env in this.project.envs) {
// for (let db of this.project.envs[env]) {
// db.ui.setup = await PortScanner.isAlive(db.connection.host, parseInt(db.connection.port)) ? 0 : -1;
// console.log('testing port', env, db.connection.database, db.ui.setup);
// }
// }
}
},
beforeMount() {

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

@ -31,21 +31,6 @@
<v-card-text class="align-self-end" v-text="item.description" />
</v-card>
</v-col>
<!-- <v-col cols="3">-->
<!-- <v-card>-->
<!-- <v-img-->
<!-- class="white&#45;&#45;text align-end"-->
<!-- gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)"-->
<!-- height="200px"-->
<!-- >-->
<!-- <v-card-title>Push Notification</v-card-title>-->
<!-- </v-img>-->
<!-- <v-card-text>-->
<!-- Google Firebase based cloud messaging service-->
<!-- </v-card-text>-->
<!-- </v-card>-->
<!-- </v-col>-->
</v-row>
</v-expansion-panel-content>
</v-expansion-panel>

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

@ -168,10 +168,6 @@
/* eslint-disable */
import draggable from 'vuedraggable'
//
// const {promisify, fs, path, config, Handlebars} = require("electron").remote.require(
// "./libs"
// );
export default {
name: 'Environment',
directives: {},
@ -293,7 +289,6 @@ export default {
'utf-8')
},
async saveEnvironment (env) {
console.log(env, this.envValues[env])
try {
let projectJsonPath, freshProjectObj

46
packages/nc-gui/components/githubStarBtn.vue

@ -0,0 +1,46 @@
<template>
<div>
<gh-btns-star
icon="mark-github"
slug="nocodb/nocodb"
show-count
:class="{'dark' : isDark}"
>
{{ ghStarText }}
</gh-btns-star>
</div>
</template>
<script>
export default {
name: 'GithubStarBtn',
data: () => ({ ghStarText: 'Star' }),
mounted() {
setInterval(() => this.ghStarText = this.ghStarText === 'Star' ? 'Fork' : 'Star', 60000)
}
}
</script>
<style scoped>
/deep/ .gh-button-container{
background: #fff2;
border-radius: 4px;
margin:0
}
/deep/ .gh-button-container:not(.dark) > a {
background: transparent !important;
color: #cdcdcd !important;
}
/deep/ .gh-button-container > a:first-child{
border-left-color: transparent;
border-top-color: transparent;
border-bottom-color: transparent;
}
/deep/ .gh-button-container > a:last-child{
border-color: transparent;
}
/deep/ .gh-button, /deep/ .social-count{
padding:1px 5px;
}
</style>

62
packages/nc-gui/components/globalAcl.vue

@ -225,64 +225,6 @@
</template>
</tbody>
</v-simple-table>
<!-- <v-data-table-->
<!-- :headers="headers"-->
<!-- :items="acls"-->
<!-- item-key="name"-->
<!-- show-expand-->
<!-- class="elevation-1"-->
<!-- >-->
<!-- <template v-slot:expanded-item="{ headers, item }">-->
<!-- <td :colspan="headers.length">More info about {{ item }}</td>-->
<!-- </template>-->
<!-- &lt;!&ndash; <template v-slot:item="{ headers, item }">&ndash;&gt;-->
<!-- &lt;!&ndash; <tr>&ndash;&gt;-->
<!-- &lt;!&ndash; <td>test</td>&ndash;&gt;-->
<!-- &lt;!&ndash; </tr>&ndash;&gt;-->
<!-- &lt;!&ndash; </template>&ndash;&gt;-->
<!-- &lt;!&ndash; <template v-slot:header="{ headers, item }">&ndash;&gt;-->
<!-- &lt;!&ndash; <tr>&ndash;&gt;-->
<!-- &lt;!&ndash; <th>test</th>&ndash;&gt;-->
<!-- &lt;!&ndash; </tr>&ndash;&gt;-->
<!-- &lt;!&ndash; </template>&ndash;&gt;-->
<!-- </v-data-table>-->
<!-- <v-virtual-scroll-->
<!-- :items="data"-->
<!-- :item-height="50"-->
<!-- height="100%"-->
<!-- >-->
<!-- <template v-slot="{ item }">-->
<!-- <acl-ts-file-db-child-->
<!-- v-if="data"-->
<!-- :nodes="nodes" :policies="data"></acl-ts-file-db-child>-->
<!-- </template>-->
<!-- </v-virtual-scroll>-->
<!-- <v-tabs-->
<!-- v-model="aclTabs"-->
<!-- >-->
<!-- <template v-for="table in tables">-->
<!-- <v-tab :key="table">{{ table }}</v-tab>-->
<!-- &lt;!&ndash; <v-tab-item :key="table">&ndash;&gt;-->
<!-- &lt;!&ndash; <acl-ts-file-db-child&ndash;&gt;-->
<!-- &lt;!&ndash; v-if="i === aclTabs"&ndash;&gt;-->
<!-- &lt;!&ndash; key="acl"&ndash;&gt;-->
<!-- &lt;!&ndash; :nodes="nodes" :policies="item"></acl-ts-file-db-child>&ndash;&gt;-->
<!-- &lt;!&ndash; </v-tab-item>&ndash;&gt;-->
<!-- </template>-->
<!-- </v-tabs>-->
<!-- <acl-ts-file-db-child-->
<!-- key="acl"-->
<!-- v-if="groupedData"-->
<!-- :nodes="nodes" :policies="acls[aclTabs]"></acl-ts-file-db-child>-->
</div>
</template>
@ -334,8 +276,8 @@ export default {
const groupedData = {}
for (const item of data) {
groupedData[item.tn] = groupedData[item.tn] || []
groupedData[item.tn].push(item)
groupedData[item.table_name] = groupedData[item.table_name] || []
groupedData[item.table_name].push(item)
}
this.groupedData = groupedData

2
packages/nc-gui/components/import/dropOrSelectFile.vue

@ -85,8 +85,6 @@ export default {
}
},
dragOverHandler(ev) {
console.log('File(s) in drop zone')
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault()
}

1
packages/nc-gui/components/import/dropOrSelectFileModal.vue

@ -98,7 +98,6 @@ export default {
}
},
dragOverHandler(ev) {
console.log('File(s) in drop zone')
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault()

3
packages/nc-gui/components/import/excelImport.vue

@ -282,7 +282,6 @@ export default {
dropHandler(ev) {
this.dragOver = false
console.log('File(s) dropped')
let file
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
@ -303,8 +302,6 @@ export default {
this._file(file)
},
dragOverHandler(ev) {
console.log('File(s) in drop zone')
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault()
},

24
packages/nc-gui/components/import/templateParsers/ExcelTemplateAdapter.js

@ -49,23 +49,23 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
const originalRows = XLSX.utils.sheet_to_json(ws, { header: 1, blankrows: false, cellDates: true, defval: null })
// fix precision bug & timezone offset issues introduced by xlsx
const basedate = new Date(1899, 11, 30, 0, 0, 0);
const basedate = new Date(1899, 11, 30, 0, 0, 0)
// number of milliseconds since base date
const dnthresh = basedate.getTime() + (new Date().getTimezoneOffset() - basedate.getTimezoneOffset()) * 60000;
const dnthresh = basedate.getTime() + (new Date().getTimezoneOffset() - basedate.getTimezoneOffset()) * 60000
// number of milliseconds in a day
const day_ms = 24 * 60 * 60 * 1000;
const day_ms = 24 * 60 * 60 * 1000
// handle date1904 property
const fixImportedDate = (date) => {
const parsed = XLSX.SSF.parse_date_code((date.getTime() - dnthresh) / day_ms, {
date1904: this.wb.Workbook.WBProps.date1904
const parsed = XLSX.SSF.parse_date_code((date.getTime() - dnthresh) / day_ms, {
date1904: this.wb.Workbook.WBProps.date1904
})
return new Date(parsed.y, parsed.m, parsed.d, parsed.H, parsed.M, parsed.S)
}
// fix imported date
const rows = originalRows.map((r) => r.map((v) => {
return v instanceof Date ? fixImportedDate(v): v
const rows = originalRows.map(r => r.map((v) => {
return v instanceof Date ? fixImportedDate(v) : v
}))
const columnNameRowExist = +rows[0].every(v => v === null || typeof v === 'string')
// const colLen = Math.max()
@ -166,7 +166,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
const rowData = {}
for (let i = 0; i < table.columns.length; i++) {
if (table.columns[i].uidt === UITypes.Checkbox) {
rowData[table.columns[i].cn] = getCheckboxValue(row[i])
rowData[table.columns[i].column_name] = getCheckboxValue(row[i])
} else if (table.columns[i].uidt === UITypes.Currency) {
const cellId = XLSX.utils.encode_cell({
c: range.s.c + i,
@ -174,12 +174,12 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
})
const cellObj = ws[cellId]
rowData[table.columns[i].cn] = (cellObj && cellObj.w && cellObj.w.replace(/[^\d.]+/g, '')) || row[i]
rowData[table.columns[i].column_name] = (cellObj && cellObj.w && cellObj.w.replace(/[^\d.]+/g, '')) || row[i]
} else if (table.columns[i].uidt === UITypes.SingleSelect || table.columns[i].uidt === UITypes.MultiSelect) {
rowData[table.columns[i].cn] = (row[i] || '').toString().trim() || null
rowData[table.columns[i].column_name] = (row[i] || '').toString().trim() || null
} else {
// toto: do parsing if necessary based on type
rowData[table.columns[i].cn] = row[i]
rowData[table.columns[i].column_name] = row[i]
}
}
this.data[tn].push(rowData)

21
packages/nc-gui/components/importantAnnouncement.vue

@ -3,7 +3,7 @@
<template #activator="{on}">
<transition name="announcement">
<v-btn
v-if="!loading"
v-show="announcementAlert"
text
small
class="mb-0 mr-2 py-0 "
@ -29,22 +29,21 @@
mdi-script-text-outline
</v-icon>
<span class="caption">
API Changes in v0.90.0
v0.90.0 API Changes
</span>
</v-list-item>
<!-- <v-list-item dense href="#" target="_blank">
<v-list-item dense href="https://github.com/nocodb/nocodb/releases/tag/0.90.0" target="_blank">
<v-icon small class="mr-2">
mdi-rocket-launch-outline
mdi-script-text-outline
</v-icon>
<span class="caption">
Migration Guide
v0.90.0 Release Note
</span>
</v-list-item> -->
</v-list-item>
<v-list-item @click="announcementAlert = false">
<v-icon small class="mr-2">
mdi-close
</v-icon>
<span class="caption">
<!--Hide menu-->
{{ $t('general.hideMenu') }}
@ -61,6 +60,14 @@ export default {
loading: true
}),
computed: {
announcementAlert: {
get() {
return !this.loading && !this.$store.state.app.hiddenAnnouncement
},
set(val) {
return this.$store.commit('app/MutHiddenAnnouncement', val ? null : true)
}
},
},
mounted() {
setTimeout(() => {

2
packages/nc-gui/components/loader.vue

@ -21,8 +21,6 @@
export default {
name: 'Loader',
props: {
// message: String,
// progress: Number
},
computed: {
message() {

2
packages/nc-gui/components/monaco/Monaco.vue

@ -47,7 +47,6 @@ export default {
watch: {
codeLocal(newValue) {
// INFO: for updating value of prop `code` in parent comp
// console.log("update:code Event Emitted", newValue);
this.$emit('update:code', newValue)
},
code(newValue) {
@ -65,7 +64,6 @@ export default {
const editor = this.$refs.editor.getMonaco()
const range = editor.getSelection()
const selectedText = editor.getModel().getValueInRange(range)
// console.log('getValue', editor.getModel())
this.selection = selectedText
this.selectionRange = range
},

3
packages/nc-gui/components/monaco/MonacoSingleLineEditor.js

@ -245,9 +245,6 @@ export default {
...(this.$store.getters['project/GtrProjectJson'].envs ?
Object.keys(this.$store.getters['project/GtrProjectJson'].envs[this.env].api) : [])];
console.log('============', this.envValues)
this.tokRef = monaco.languages.setMonarchTokensProvider('mySpecialLanguage', {
tokenizer: {
root: [

8
packages/nc-gui/components/monaco/MonacoSqlEditor.vue

@ -189,23 +189,15 @@ export default {
this.codeLocal = newValue
}
},
beforeCreate() {
// console.log(MonacoEditor)
},
created() {
//
},
methods: {
selectionFn() {
const editor = this.$refs.editor.getMonaco()
const range = editor.getSelection()
const selectedText = editor.getModel().getValueInRange(range)
// console.log('getValue', editor.getModel())
this.selection = selectedText
this.selectionRange = range
},
pretify() {
// console.log("this.code", this.code);
const editor = this.$refs.editor.getMonaco()
if (this.selection && this.selectionRange) {

167
packages/nc-gui/components/previewAs.vue

@ -0,0 +1,167 @@
<template>
<div>
<v-menu offset-y>
<template #activator="{on}">
<v-btn
v-show="isDashboard && _isUIAllowed('previewAs')"
small
light
color="#fff3"
class="white--text nc-btn-preview"
v-on="on"
>
<v-icon small class="mr-1">
mdi-play-circle
</v-icon>
Preview
<v-icon small>
mdi-menu-down
</v-icon>
</v-btn>
</template>
<v-list dense>
<template v-for="(role) in rolesList">
<v-list-item
:key="role.title"
:class="`pointer nc-preview-${role.title}`"
@click="setPreviewUser(role.title)"
>
<v-list-item-title>
<v-icon
small
class="mr-1"
:color="role.title === previewAs ? 'x-active' : ''"
>
{{ roleIcon[role.title] }}
</v-icon>
<span
class="caption text-capitalize"
:class="{ 'x-active--text': role.title === previewAs }"
>{{ role.title }}</span>
</v-list-item-title>
</v-list-item>
</template>
<template v-if="previewAs">
<!-- <v-divider></v-divider>-->
<v-list-item @click="setPreviewUser(null)">
<v-icon small class="mr-1">
mdi-close
</v-icon>
<!-- Reset Preview -->
<span class="caption nc-preview-reset">{{ $t('activity.resetReview') }}</span>
</v-list-item>
</template>
</v-list>
</v-menu>
<v-menu
:position-x="position.x"
:position-y="position.y"
:value="previewAs"
activator=""
:close-on-click="false"
:close-on-content-click="false"
>
<div class="floating-reset-btn white py-1 pr-3 caption primary lighten-2 white--text font-weight-bold d-flex align-center nc-floating-preview-btn" style="overflow-y: hidden">
<v-icon style="cursor: move" color="white" @mousedown="mouseDown">
mdi-drag
</v-icon>
<v-divider vertical class="mr-2" />
<div class="d-inline pointer d-flex align-center">
<span>Preview as :</span>
<v-radio-group
:value="previewAs"
dense
row
class="mt-0 pt-0"
hide-details
@change="setPreviewUser($event)"
>
<v-radio
v-for="(role) in rolesList"
:key="role.title"
:value="role.title"
color="white"
dark
:class="`ml-1 nc-floating-preview-${role.title}`"
>
<template #label>
<span class="white--text caption text-capitalize">{{ role.title }}</span>
</template>
</v-radio>
</v-radio-group>
<v-divider vertical class="mr-2" />
<span class="pointer" @click="setPreviewUser(null)"> <v-icon small color="white">mdi-exit-to-app</v-icon> Exit</span>
</div>
</div>
</v-menu>
</div>
</template>
<script>
export default {
name: 'PreviewAs',
data: () => ({
roleIcon: {
owner: 'mdi-account-star',
creator: 'mdi-account-hard-hat',
editor: 'mdi-account-edit',
viewer: 'mdi-eye-outline',
commenter: 'mdi-comment-account-outline'
},
rolesList: [{ title: 'editor' }, { title: 'commenter' }, { title: 'viewer' }],
position: {
x: 9999, y: 9999
}
}),
computed: {
previewAs: {
get() {
return this.$store.state.users.previewAs
},
set(previewAs) {
this.$store.commit('users/MutPreviewAs', previewAs)
}
}
},
mounted() {
this.position = {
y: window.innerHeight - 100,
x: window.innerWidth / 2 - 250
}
window.addEventListener('mouseup', this.mouseUp, false)
},
beforeDestroy() {
window.removeEventListener('mousemove', this.divMove, true)
window.removeEventListener('mouseup', this.mouseUp, false)
},
methods: {
setPreviewUser(previewAs) {
this.$tele.emit(`preview-as:${previewAs}`)
if (!process.env.EE) {
this.$toast.info('Available in Enterprise edition').goAway(3000)
} else {
this.previewAs = previewAs
window.location.reload()
}
},
mouseUp() {
window.removeEventListener('mousemove', this.divMove, true)
},
mouseDown(e) {
window.addEventListener('mousemove', this.divMove, true)
},
divMove(e) {
this.position = { y: e.clientY - 10, x: e.clientX - 18 }
}
}
}
</script>
<style scoped>
</style>

11
packages/nc-gui/components/project/apiClientSwagger.vue

@ -782,7 +782,6 @@ export default {
await this.loadFileCollection(this.$store.getters['apiClientSwagger/GtrCurrentApiFilePaths'][i])
}
// console.log(this.$store.getters['apiClientSwagger/GtrCurrentApiFilePaths']);
} catch (e) {
console.log('Failed to load previously opened query collections', e)
}
@ -794,7 +793,6 @@ export default {
this.api.meta = {}
console.log(this.nodes)
try {
const info = (await this.$axios.get('/nc/projectApiInfo', {
headers: {
@ -818,7 +816,6 @@ export default {
console.log('Node info : ', api)
},
async handleKeyDown ({ metaKey, key, altKey, shiftKey, ctrlKey }) {
console.log(metaKey, key, altKey, shiftKey, ctrlKey)
// cmd + s -> save
// cmd + l -> reload
// cmd + n -> new
@ -857,7 +854,6 @@ export default {
})
if (userChosenPath) {
console.log(userChosenPath)
fs.writeFileSync(userChosenPath, '[]', 'utf-8')
const pathObj = {
path: userChosenPath,
@ -1056,7 +1052,6 @@ export default {
},
async ctxMenuClickHandler (actionEvent, index) {
console.log(actionEvent, index)
switch (actionEvent.value) {
case 'add-folder':
this.tvNodeFolderAdd(index)
@ -1112,8 +1107,6 @@ export default {
},
async apiSend () {
console.log('apiSend')
if (!this.api.path.trim()) {
this.$toast.info('Please enter http url').goAway(3000)
return
@ -1154,7 +1147,6 @@ export default {
async fileCollectionReload () {
const data = new Tree(await this.apiFileCollection.read())
console.log(data)
this.apiTv = data
// this.$set(this, 'apiCollections', data);
@ -1167,12 +1159,10 @@ export default {
},
async tvNodeRename (params) {
console.log(params)
await this.savefileCollections(this.curApiCollectionPanel)
},
async onAddNode (params) {
console.log(params)
await this.savefileCollections(this.curApiCollectionPanel)
},
@ -1180,7 +1170,6 @@ export default {
const { parent, children, ...params } = node
this.currentNode = node
console.log(params)
this.apiClickedOnList(params)
// if (params.query) ;
// this.selectQuery(params)

2
packages/nc-gui/components/project/apis.vue

@ -204,8 +204,6 @@ export default {
_nodes.type = 'apiClientDir'
_nodes.url = url
const tabIndex = this.tabs.findIndex(el => el.key === _nodes.key)
console.log('apis node', this.nodes)
console.log('apiClient node', _nodes)
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex)
} else {

307
packages/nc-gui/components/project/appStore.vue

@ -1,104 +1,109 @@
<template>
<div class="d-flex h-100 nc-app-store-tab">
<v-dialog v-model="pluginInstallOverlay" min-width="400px" max-width="700px" min-height="300">
<v-card
v-if="installPlugin && pluginInstallOverlay"
:dark="$store.state.windows.darkTheme"
:light="!$store.state.windows.darkTheme"
>
<app-install :title="installPlugin.title" :default-config="defaultConfig" @close="pluginInstallOverlay = false" @saved="saved()" />
</v-card>
</v-dialog>
<div>
<h3 class="mb-5 title grey--text">
Configure apps
</h3>
<v-divider />
<div class="d-flex h-100 nc-app-store-tab mt-5">
<v-dialog v-model="pluginInstallOverlay" min-width="400px" max-width="700px" min-height="300">
<v-card
v-if="installPlugin && pluginInstallOverlay"
:dark="$store.state.windows.darkTheme"
:light="!$store.state.windows.darkTheme"
>
<app-install :id="installPlugin.id" :default-config="defaultConfig" @close="pluginInstallOverlay = false" @saved="saved()" />
</v-card>
</v-dialog>
<dlg-ok-new
v-model="pluginUninstallModal"
:heading="`Please click on submit to reset ${resetPluginRef && resetPluginRef.title}`"
ok-label="Submit"
type="primary"
@ok="confirmResetPlugin"
/>
<dlg-ok-new
v-model="pluginUninstallModal"
:heading="`Please click on submit to reset ${resetPluginRef && resetPluginRef.title}`"
ok-label="Submit"
type="primary"
@ok="confirmResetPlugin"
/>
<v-dialog min-width="400px" max-width="700px" min-height="300">
<v-card
v-if="resetPluginRef"
:dark="$store.state.windows.darkTheme"
:light="!$store.state.windows.darkTheme"
>
<v-card-text> Please confirm to reset {{ resetPluginRef.title }}</v-card-text>
<v-card-actions>
<v-btn color="primary" @click="confirmResetPlugin">
Yes
</v-btn>
<v-btn @click="pluginUninstallModal = false">
No
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog min-width="400px" max-width="700px" min-height="300">
<v-card
v-if="resetPluginRef"
:dark="$store.state.windows.darkTheme"
:light="!$store.state.windows.darkTheme"
>
<v-card-text> Please confirm to reset {{ resetPluginRef.title }}</v-card-text>
<v-card-actions>
<v-btn color="primary" @click="confirmResetPlugin">
Yes
</v-btn>
<v-btn @click="pluginUninstallModal = false">
No
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-container class="h-100 app-container">
<v-row class="d-flex align-stretch">
<v-col v-for="(app,i) in filteredApps" :key="i" class="" cols="6">
<!-- @click="installApp(app)"-->
<v-container class="h-100 app-container">
<v-row class="d-flex align-stretch">
<v-col v-for="(app,i) in filteredApps" :key="i" class="" cols="6">
<!-- @click="installApp(app)"-->
<v-card
height="100%"
class="elevatio app-item-card "
>
<div class="install-btn ">
<v-btn
v-if="app.parsedInput"
x-small
outlined
class=" caption text-capitalize"
@click="installApp(app)"
>
<v-icon x-small class="mr-1">
mdi-pencil
</v-icon>
{{ $t('general.edit') }}
</v-btn>
<v-btn
v-if="app.parsedInput"
x-small
outlined
class="caption text-capitalize"
@click="resetApp(app)"
>
<v-icon x-small class="mr-1">
mdi-close-circle-outline
</v-icon>
Reset
</v-btn>
<v-btn v-else x-small outlined class=" caption text-capitalize" @click="installApp(app)">
<v-icon x-small class="mr-1">
mdi-plus
</v-icon>
Install
</v-btn>
</div>
<v-card
height="100%"
class="elevatio app-item-card "
>
<div class="install-btn ">
<v-btn
v-if="app.parsedInput"
x-small
outlined
class=" caption text-capitalize"
@click="installApp(app)"
>
<v-icon x-small class="mr-1">
mdi-pencil
</v-icon>
{{ $t('general.edit') }}
</v-btn>
<v-btn
v-if="app.parsedInput"
x-small
outlined
class="caption text-capitalize"
@click="resetApp(app)"
>
<v-icon x-small class="mr-1">
mdi-close-circle-outline
</v-icon>
Reset
</v-btn>
<v-btn v-else x-small outlined class=" caption text-capitalize" @click="installApp(app)">
<v-icon x-small class="mr-1">
mdi-plus
</v-icon>
Install
</v-btn>
</div>
<div class="d-flex flex-no-wrap">
<v-avatar
class="ma-3 align-self-center"
size="50"
tile
:color="app.title === 'SES' ? '#242f3e' : ''"
>
<v-img v-if="app.logo" :src="app.logo" contain />
<v-icon v-else-if="app.icon" color="#242f3e" size="50">
{{ app.icon }}
</v-icon>
</v-avatar>
<div class="flex-grow-1">
<v-card-title
class="title "
v-text="app.title"
/>
<div class="d-flex flex-no-wrap">
<v-avatar
class="ma-3 align-self-center"
size="50"
tile
:color="app.title === 'SES' ? '#242f3e' : ''"
>
<v-img v-if="app.logo" :src="app.logo" contain />
<v-icon v-else-if="app.icon" color="#242f3e" size="50">
{{ app.icon }}
</v-icon>
</v-avatar>
<div class="flex-grow-1">
<v-card-title
class="title "
v-text="app.title"
/>
<v-card-subtitle class="pb-1" v-text="app.description" />
<v-card-actions>
<div class="d-flex justify-space-between d-100 align-center">
<v-card-subtitle class="pb-1" v-text="app.description" />
<v-card-actions>
<div class="d-flex justify-space-between d-100 align-center">
<!-- <v-rating-->
<!-- full-icon="mdi-star"-->
<!-- readonly-->
@ -109,8 +114,8 @@
<!-- <span class="subtitles" v-if="app.price && app.price !== 'Free'">${{ app.price }} / mo</span>-->
<!-- <span class="subtitles" v-else>Free</span>-->
</div>
</v-card-actions>
</div>
</v-card-actions>
<!-- <v-card-actions>-->
<!-- <v-btn-->
@ -121,50 +126,51 @@
<!-- Download-->
<!-- </v-btn>-->
<!-- </v-card-actions>-->
</div>
</div>
</div>
</v-card>
</v-col>
</v-row>
</v-container>
</v-card>
</v-col>
</v-row>
</v-container>
<v-navigation-drawer width="300" class="pa-1">
<v-text-field
v-model="query"
dense
hide-details
:placeholder="$t('placeholder.searchApps')"
color="primary"
class="search-field caption"
>
<template #prepend-inner>
<v-icon small class="mt-1">
mdi-magnify
</v-icon>
</template>
</v-text-field>
<!-- <v-navigation-drawer width="300" class="pa-1">
<v-text-field
v-model="query"
dense
hide-details
:placeholder="$t('placeholder.searchApps')"
color="primary"
class="search-field caption"
>
<template #prepend-inner>
<v-icon small class="mt-1">
mdi-magnify
</v-icon>
</template>
</v-text-field>
<v-list dense>
<v-list-item v-for="filter of filters" :key="filter" dense>
<v-checkbox
v-model="selectedTags"
class="pt-0 mt-0"
:value="filter"
hide-details
dense
:label="filter"
>
<template #label>
<v-icon small class="mr-1">
{{ icons[filter] }}
</v-icon>
<v-list dense>
<v-list-item v-for="filter of filters" :key="filter" dense>
<v-checkbox
v-model="selectedTags"
class="pt-0 mt-0"
:value="filter"
hide-details
dense
:label="filter"
>
<template #label>
<v-icon small class="mr-1">
{{ icons[filter] }}
</v-icon>
{{ filter }}
</template>
</v-checkbox>
</v-list-item>
</v-list>
</v-navigation-drawer>
{{ filter }}
</template>
</v-checkbox>
</v-list-item>
</v-list>
</v-navigation-drawer>-->
</div>
</div>
</template>
@ -207,37 +213,32 @@ export default {
},
async created() {
await this.loadPluginList()
this.readPluginDefaults()
},
methods: {
async readPluginDefaults() {
try {
this.defaultConfig = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginDemoDefaults'])
} catch (e) {
}
},
async confirmResetPlugin() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginSet', {
await this.$api.plugin.update(this.resetPluginRef.id, {
input: null,
id: this.resetPluginRef.id,
title: this.resetPluginRef.title,
uninstall: true
}])
active: 0
})
this.$toast.success('Plugin uninstalled successfully').goAway(5000)
await this.loadPluginList()
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
this.$tele.emit(`appstore:reset:${this.resetPluginRef.title}`)
},
async saved() {
this.pluginInstallOverlay = false
await this.loadPluginList()
this.$tele.emit(`appstore:install:submit:${this.installPlugin.title}`)
},
async installApp(app) {
this.pluginInstallOverlay = true
this.installPlugin = app
this.$tele.emit(`appstore:install:trigger:${app.title}`)
},
async resetApp(app) {
this.pluginUninstallModal = true
@ -245,8 +246,9 @@ export default {
},
async loadPluginList() {
try {
const plugins = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginList'])
plugins.push(...plugins.splice(0, 3))
// const plugins = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginList'])
const plugins = (await this.$api.plugin.list()).list
// plugins.push(...plugins.splice(0, 3))
this.apps = plugins.map((p) => {
p.tags = p.tags ? p.tags.split(',') : []
p.parsedInput = p.input && JSON.parse(p.input)
@ -261,9 +263,6 @@ export default {
</script>
<style scoped lang="scss">
.title {
color: var(--v-textColor-ligten2) !important;
}
.app-item-card {
transition: .4s background-color;

34
packages/nc-gui/components/project/appStore/appInstall.vue

@ -96,7 +96,7 @@ import FormInput from '@/components/project/appStore/FormInput'
export default {
name: 'AppInstall',
components: { FormInput },
props: ['title', 'defaultConfig'],
props: ['id', 'defaultConfig'],
data: () => ({
plugin: null,
formDetails: null,
@ -153,34 +153,41 @@ export default {
},
async saveSettings() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginSet', {
input: this.settings,
id: this.pluginId,
title: this.plugin.title
}])
await this.$api.plugin.update(this.id, {
input: JSON.stringify(this.settings),
active: 1
})
this.$emit('saved')
this.$toast.success(this.formDetails.msgOnInstall || 'Plugin settings saved successfully').goAway(5000)
this.simpleAnim()
} catch (e) {
} catch (_e) {
const e = await this._extractSdkResponseError(_e)
this.$toast.error(e.message).goAway(3000)
}
},
async testSettings() {
this.testing = true
try {
const res = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginTest', {
// const res = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginTest', {
// input: this.settings,
// id: this.pluginId,
// category: this.plugin.category,
// title: this.plugin.title
// }])
const res = (await this.$api.plugin.test({
input: this.settings,
id: this.pluginId,
category: this.plugin.category,
title: this.plugin.title
}])
}))
if (res) {
this.$toast.success('Successfully tested plugin settings').goAway(3000)
} else {
this.$toast.info('Invalid credentials').goAway(3000)
}
} catch (e) {
} catch (_e) {
const e = await this._extractSdkResponseError(_e)
this.$toast[e.message === 'Not implemented' ? 'info' : 'error'](e.message).goAway(3000)
}
this.testing = false
@ -199,9 +206,10 @@ export default {
},
async readPluginDetails() {
try {
this.plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
title: this.title
}])
// this.plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
// title: this.title
// }])
this.plugin = (await this.$api.plugin.read(this.id))
this.formDetails = JSON.parse(this.plugin.input_schema)
this.pluginId = this.plugin.id
this.settings = JSON.parse(this.plugin.input) || (this.formDetails.array ? [{}] : {})

6
packages/nc-gui/components/project/auditTab.vue

@ -10,9 +10,9 @@
<audit :nodes="nodes" />
</v-tab-item>
<v-tab>
<!-- <v-tab>
<span class="caption text-capitalize">
<!--SQL Migrations-->
&lt;!&ndash;SQL Migrations&ndash;&gt;
{{ $t('title.sqlMigrations') }}
</span>
</v-tab>
@ -20,7 +20,7 @@
<sql-log-and-output>
<db :nodes="nodes" />
</sql-log-and-output>
</v-tab-item>
</v-tab-item>-->
</v-tabs>
</template>

22
packages/nc-gui/components/project/auditTab/audit.vue

@ -1,8 +1,8 @@
<template>
<div class="h-100" style="overflow: auto">
<v-toolbar height="30">
<v-toolbar height="30" class="elevation-0">
<v-spacer />
<v-btn x-small outlined @click="loadAudits">
<v-btn small outlined @click="loadAudits">
<v-icon small class="mr-2">
refresh
</v-icon>
@ -101,12 +101,20 @@ export default {
},
methods: {
async loadAudits() {
const { list, count } = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcAuditList', {
limit: this.limit,
offset: this.limit * (this.page - 1)
}])
// const { list, count } = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcAuditList', {
// limit: this.limit,
// offset: this.limit * (this.page - 1)
// }])
const {
list, pageInfo
} = (await this.$api.project.auditList(
this.$store.state.project.projectId, {
limit: this.limit,
offset: this.limit * (this.page - 1)
}))
this.audits = list
this.count = count
this.count = pageInfo.totalRows
},
calculateDiff(date) {
return dayjs.utc(date).fromNow()

33
packages/nc-gui/components/project/auditTab/db.vue

@ -329,7 +329,6 @@ export default {
// },
isMigrationButtonEnabled(name) {
console.log('menu -- - ', name)
return this.nodes.dbConnection.client === 'sqlite3' && name === 'Migration Down'
},
@ -337,7 +336,6 @@ export default {
this.selectedMigration.migration = ''
this.selectedMigration.up = ''
this.selectedMigration.down = ''
console.log(migration)
this.selectedMigration.migration = migration
// let result = await this.sqlMgr.migrator().migrationsToSql({
// env: this.nodes.env,
@ -360,7 +358,6 @@ export default {
title: migration.title,
titleDown: migration.titleDown
}])
console.log(result)
this.selectedMigration.up = result.data.object.up
this.selectedMigration.down = result.data.object.down
},
@ -377,7 +374,6 @@ export default {
onlyList: true
}
console.log('this.currentProjectFolder', this.currentProjectFolder)
// let result = await this.sqlMgr.migrator().migrationsList(migrationArgs);
// let result = await this.sqlMgr.sqlOp(null, 'migrationsList', migrationArgs);
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'migrationsList', migrationArgs])
@ -394,7 +390,6 @@ export default {
}
}
console.log('loadEnv: ', result)
this.tableMigrationFiles.data = result.data.object.list
this.tableMigrationFiles.status = result.data.object.pending
if (this.tableMigrationFiles.data[0]) {
@ -412,20 +407,6 @@ export default {
},
async migrationUp(steps = 99999999999) {
try {
// await this.sqlMgr.migrator().migrationsUp({
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias,
// migrationSteps: steps,
// folder: this.currentProjectFolder,
// sqlContentMigrate: 1
// });
// await this.sqlMgr.sqlOp(null, 'migrationsUp', {
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias,
// migrationSteps: steps,
// folder: this.currentProjectFolder,
// sqlContentMigrate: 1
// });
await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'migrationsUp', {
env: this.nodes.env,
@ -442,20 +423,6 @@ export default {
},
async migrationDown(steps = 99999999999) {
try {
// await this.sqlMgr.migrator().migrationsDown({
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias,
// migrationSteps: steps,
// folder: this.currentProjectFolder,
// sqlContentMigrate: 1
// });
// await this.sqlMgr.sqlOp(null, 'migrationsDown', {
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias,
// migrationSteps: steps,
// folder: this.currentProjectFolder,
// sqlContentMigrate: 1
// });
await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'migrationsDown', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,

6
packages/nc-gui/components/project/cronJobs.vue

@ -362,18 +362,12 @@ export default {
return false
})
// await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// dbAlias: this.dbAliasList[this.dbsTab].meta.dbAlias,
// env: this.$store.getters['project/GtrEnv']
// }, 'xcCronSave', this.selectedItem]);
if (!errorCrons.length) {
for (const cron of saveList) {
// if (cron !== this.selectedItem && !cron.id) {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
dbAlias: this.dbAliasList[this.dbsTab].meta.dbAlias,
env: this.$store.getters['project/GtrEnv']
}, 'xcCronSave', cron])
// }
}
await this.loadCrons()

28
packages/nc-gui/components/project/dlgs/dlgAddRelation.vue

@ -12,12 +12,12 @@
</template>
<v-card class="elevation-20">
<v-card-title class="grey darken-2 subheading" style="height:30px">
<!-- {{ this.heading }} for {{ this.column.cn }} -->
<!-- {{ this.heading }} for {{ this.column.column_name }} -->
</v-card-title>
<v-form v-model="valid">
<v-card-text class="pt-4 pl-4">
<p class="headline">
{{ heading }} for {{ column.cn }}
{{ heading }} for {{ column.column_name }}
</p>
<v-row
justify="space-between"
@ -30,7 +30,7 @@
label="Select Reference Table"
:full-width="false"
:items="refTables"
item-text="tn"
item-text="table_name"
required
dense
/>
@ -43,7 +43,7 @@
label="Select Reference Column"
:full-width="false"
:items="refColumns"
item-text="cn"
item-text="column_name"
required
dense
/>
@ -129,8 +129,8 @@ export default {
'SET DEFAULT'
],
relation: {
childColumn: this.column.cn,
childTable: this.nodes.tn,
childColumn: this.column.column_name,
childTable: this.nodes.table_name,
parentTable: this.column.rtn || '',
parentColumn: this.column.rcn || '',
onDelete: 'CASCADE',
@ -154,18 +154,18 @@ export default {
// dbAlias: this.nodes.dbAlias
// });
// const result = await client.columnList({
// tn: this.relation.parentTable
// table_name: this.relation.parentTable
// });
// const result = await this.sqlMgr.sqlOp({
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'columnList', { tn: this.relation.parentTable})
// }, 'columnList', { table_name: this.relation.parentTable})
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'columnList', { tn: this.relation.parentTable }])
}, 'columnList', { table_name: this.relation.parentTable }])
const columns = result.data.list
this.refColumns = JSON.parse(JSON.stringify(columns))
@ -177,7 +177,7 @@ export default {
} else {
// find pk column and assign to parentColumn
const pkKeyColumns = this.refColumns.filter(el => el.pk)
this.relation.parentColumn = (pkKeyColumns[0] || {}).cn || ''
this.relation.parentColumn = (pkKeyColumns[0] || {}).column_name || ''
}
this.isRefColumnsLoading = false
@ -219,11 +219,11 @@ export default {
await this.loadTablesList()
if (!this.relation.parentTable) {
let tn = (this.refTables[0] || {}).tn || ''
if (tn === 'nc_evolutions' || tn === '_evolutions') {
tn = (this.refTables[1] || {}).tn || ''
let table_name = (this.refTables[0] || {}).table_name || ''
if (table_name === 'nc_evolutions' || table_name === '_evolutions') {
table_name = (this.refTables[1] || {}).table_name || ''
}
this.relation.parentTable = tn
this.relation.parentTable = table_name
}
if (this.column.rtn) {
this.relation.parentTable = this.column.rtn

2
packages/nc-gui/components/project/dlgs/dlgTriggerAddEdit.vue

@ -110,7 +110,7 @@ export default {
triggerTimingOptions: ['BEFORE', 'AFTER'],
triggerEventOptions: ['INSERT', 'UPDATE', 'DELETE'],
trigger: {
tn: this.nodes.tn,
table_name: this.nodes.table_name,
trigger_name: this.triggerObject.trigger || '',
timing: this.triggerObject.timing || 'BEFORE',
event: this.triggerObject.event || 'INSERT',

3
packages/nc-gui/components/project/functionTab/functionQuery.vue

@ -100,7 +100,6 @@ export default {
}),
async handleKeyDown({ metaKey, key, altKey, shiftKey, ctrlKey }) {
console.log(metaKey, key, altKey, shiftKey, ctrlKey)
// cmd + s -> save
// cmd + l -> reload
// cmd + n -> new
@ -172,7 +171,6 @@ export default {
...this.nodes
}
})
console.log('create function result', result)
this.newFunction = false
this.oldCreateFunction = `${this.functionData.create_function}` + ''
this.$toast.success('Function created successfully').goAway(3000)
@ -197,7 +195,6 @@ export default {
}])
this.oldCreateFunction = `${this.functionData.create_function}` + ''
console.log('update function result', result)
this.$toast.success('Function updated successfully').goAway(3000)
}
} catch (e) {

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

@ -78,7 +78,7 @@ export default {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, this.isMiddleware ? 'defaultResolverMiddlewareCode' : 'defaultResolverHandlerCodeGet', {
tn: this.nodes.tn || this.nodes.view_name,
table_name: this.nodes.table_name || this.nodes.view_name,
resolver: this.resolver
}])
if (functionCode) {
@ -95,7 +95,7 @@ export default {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, this.isMiddleware ? 'xcResolverMiddlewareUpdate' : 'xcResolverHandlerUpdate', {
tn: this.nodes.tn || this.nodes.view_name,
table_name: this.nodes.table_name || this.nodes.view_name,
resolver: this.resolver,
functions: [this.code]
}])

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

@ -73,7 +73,7 @@ export default {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'defaultRpcServiceCodeGet', {
tn: this.nodes.tn || this.nodes.view_name,
tn: this.nodes.table_name || this.nodes.view_name,
service: this.service,
relation_type: this.serviceData.relation_type,
tnc: this.serviceData.tnc
@ -92,7 +92,7 @@ export default {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcRpcHandlerUpdate', {
tn: this.nodes.tn || this.nodes.view_name,
tn: this.nodes.table_name || this.nodes.view_name,
service: this.service,
functions: [this.code]
}])

102
packages/nc-gui/components/project/projectMetadata/disableOrEnableModels.vue

@ -16,66 +16,25 @@
</div>
</v-tab-item>
<template v-for="(db,i) in dbAliasList">
<v-tab :key="db.meta.dbAlias + i" :href="'#' + db.meta.dbAlias" class="text-capitalize caption nc-meta-mgmt-metadata-tab">
<!-- {{ db.connection.database | extractDbName }} {{ db.meta.dbAlias }} -->
<template v-for="(db,i) in bases">
<v-tab :key="db.id + i" :href="'#' + db.id + 'meta'" class="text-capitalize caption nc-meta-mgmt-metadata-tab">
<!-- {{ db.connection.database | extractDbName }} {{ db.id }} -->
<!-- Metadata -->
{{ $t('title.metadata') }}
</v-tab>
<v-tab-item :key="db.meta.dbAlias + 't' + i" :value=" db.meta.dbAlias">
<disable-or-enable-tables
<v-tab-item :key="db.id + 't' + i" :value="db.id + 'meta'">
<metaDiffSync
:nodes="nodes"
:db="db"
:db-alias="db.meta.dbAlias"
:db-id="db.id"
/>
<!-- <v-tabs color="x-active" height="28">
<v-tab class="text-capitalize caption">
Tables
</v-tab>
<v-tab-item>
<disable-or-enable-tables
:nodes="nodes"
:db="db"
:db-alias="db.meta.dbAlias"
/>
</v-tab-item>
&lt;!&ndash; enable extra &ndash;&gt;
<v-tab class="text-capitalize caption">
Views
</v-tab>
<v-tab-item>
<disable-or-enable-views
:nodes="nodes"
:db="db"
:db-alias="db.meta.dbAlias"
/>
</v-tab-item>
&lt;!&ndash; <v-tab class="text-capitalize caption">Functions</v-tab>
<v-tab-item>
<disable-or-enable-functions :nodes="nodes" :db="db"
:db-alias="db.meta.dbAlias"></disable-or-enable-functions>
</v-tab-item>
<v-tab class="text-capitalize caption">Procedures</v-tab>
<v-tab-item>
<disable-or-enable-procedures :nodes="nodes" :db="db"
:db-alias="db.meta.dbAlias"></disable-or-enable-procedures>
</v-tab-item>&ndash;&gt;
<v-tab class="text-capitalize caption">
Relations
</v-tab>
<v-tab-item>
<disable-or-enable-relations :nodes="nodes" :db-alias="db.meta.dbAlias" />
</v-tab-item>
</v-tabs>-->
</v-tab-item>
<template v-if="uiacl">
<v-tab :key="db.meta.dbAlias + 'acl'" :href="'#' + db.meta.dbAlias + 'acl'" class="text-capitalize caption nc-ui-acl-tab">
<!-- {{ db.connection.database | extractDbName }}-->
<v-tab :key="db.id + 'acl'" :href="'#' + db.id + 'acl'" class="text-capitalize caption nc-ui-acl-tab">
<!--UI Access Control-->
{{ $t('title.uiACL') }}
</v-tab>
<v-tab-item :key="db.meta.dbAlias + 'aclt'" :value=" db.meta.dbAlias + 'acl'">
<v-tab-item :key="db.id + 'aclt'" :value=" db.id + 'acl'">
<v-tabs color="x-active" height="28">
<v-tab class="text-capitalize caption">
<!-- Tables -->
@ -85,37 +44,9 @@
<toggle-table-ui-acl
:nodes="nodes"
:db="db"
:db-alias="db.meta.dbAlias"
:db-alias="db.id"
/>
</v-tab-item>
<!-- enable extra -->
<!-- <v-tab class="text-capitalize caption">Views</v-tab>
<v-tab-item>
<toggle-view-ui-acl :nodes="nodes" :db="db"
:db-alias="db.meta.dbAlias"></toggle-view-ui-acl>
</v-tab-item>
<v-tab class="text-capitalize caption">Functions</v-tab>
<v-tab-item>
<toggle-function-ui-acl :nodes="nodes" :db="db"
:db-alias="db.meta.dbAlias"></toggle-function-ui-acl>
</v-tab-item>
<v-tab class="text-capitalize caption">Procedures</v-tab>
<v-tab-item>
<toggle-procedure-ui-acl :nodes="nodes" :db="db"
:db-alias="db.meta.dbAlias"></toggle-procedure-ui-acl>
</v-tab-item>-->
<!-- <v-tab class="text-capitalize caption">
Relations
</v-tab>
<v-tab-item>
<toggle-relations-ui-acl
:nodes="nodes"
:db="db"
:db-alias="db.meta.dbAlias"
/>
</v-tab-item>-->
</v-tabs>
</v-tab-item>
</template>
@ -127,22 +58,16 @@
<script>
import { mapGetters } from 'vuex'
import XcMeta from '../settings/xcMeta'
// import DisableOrEnableRelations from './sync/disableOrEnableRelations'
import { isMetaTable } from '@/helpers/xutils'
import DisableOrEnableTables from '@/components/project/projectMetadata/sync/disableOrEnableTables'
import metaDiffSync from '~/components/project/projectMetadata/sync/metaDiffSync'
import ToggleTableUiAcl from '@/components/project/projectMetadata/uiAcl/toggleTableUIAcl'
// import ToggleRelationsUiAcl from '@/components/project/projectMetadata/uiAcl/toggleRelationsUIAcl'
// import DisableOrEnableViews from '~/components/project/projectMetadata/sync/disableOrEnableViews'
export default {
name: 'DisableOrEnableModels',
components: {
// DisableOrEnableViews,
// ToggleRelationsUiAcl,
ToggleTableUiAcl,
DisableOrEnableTables,
metaDiffSync,
XcMeta
// DisableOrEnableRelations
},
props: ['nodes'],
data: () => ({
@ -158,6 +83,9 @@ export default {
},
methods: {},
computed: {
bases() {
return this.$store.state.project.project && this.$store.state.project.project.bases
},
dbsTab: {
set(tab) {
if (!tab) {
@ -201,7 +129,7 @@ export default {
return 0
}
if (this.tables && this.models) {
const tables = this.tables.filter(t => !isMetaTable(t.tn)).map(t => t.tn)
const tables = this.tables.filter(t => !isMetaTable(t.table_name)).map(t => t.table_name)
res.push(...this.models.map((m) => {
const i = tables.indexOf(m.title)
if (i === -1) {

4
packages/nc-gui/components/project/projectMetadata/sync/disableOrEnableRelations.vue

@ -93,10 +93,10 @@
<template #item="{item,index}">
<tr class="caption">
<td>{{ index + 1 }}</td>
<td>{{ item.relationType === 'hm' ? item.rtn : item.tn }}</td>
<td>{{ item.relationType === 'hm' ? item.rtn : item.table_name }}</td>
<td>{{ item.relationType === 'hm' ? 'HasMany' : 'BelongsTo' }}</td>
<td>{{ item.rtn }}</td>
<td>{{ item.tn }}</td>
<td>{{ item.table_name }}</td>
<!-- <td>
<v-checkbox
v-model="item.enabled"

47
packages/nc-gui/components/project/projectMetadata/sync/disableOrEnableTables.vue → packages/nc-gui/components/project/projectMetadata/sync/metaDiffSync.vue

@ -1,5 +1,5 @@
<template>
<v-container v-if="dbAliasList[dbsTab]" fluid>
<v-container fluid>
<v-card>
<v-row>
<!-- <v-col cols="12">-->
@ -28,7 +28,7 @@
small
color="primary"
icon="refresh"
@click="loadXcDiff()"
@click="clickReload"
>
<!-- Reload -->
{{ $t('general.reload') }}
@ -76,18 +76,18 @@
<tbody>
<tr
v-for="model in diff"
v-show="!filter.trim() || (model.tn || model.title || '').toLowerCase().includes(filter.toLowerCase())"
:key="model.title"
:class="`nc-metasync-row nc-metasync-row-${model.tn}`"
v-show="!filter.trim() || (model.table_name || model.title || '').toLowerCase().includes(filter.toLowerCase())"
:key="model.table_name"
:class="`nc-metasync-row nc-metasync-row-${model.table_name}`"
>
<!-- v-if="model.alias.toLowerCase().indexOf(filter.toLowerCase()) > -1">-->
<td>
<v-icon small :color="viewIcons[model.type==='table'?'grid':'view'].color" v-on="on">
<!-- <v-icon small :color="viewIcons[model.type==='table'?'grid':'view'].color" v-on="on">
{{ viewIcons[model.type === 'table' ? 'grid' : 'view'].icon }}
</v-icon>
</v-icon>-->
<v-tooltip bottom>
<template #activator="{on}">
<span v-on="on">{{ model.tn && model.tn.slice(prefix.length) }}</span>
<span v-on="on">{{ model.table_name && model.table_name.slice(prefix.length) }}</span>
</template>
<span class="caption">{{ model.title }}</span>
</v-tooltip>
@ -244,6 +244,7 @@
<div class="d-flex justify-center">
<v-btn
v-if="isChanged"
v-t="['proj-meta:metadata:metasync']"
x-large
class="mx-auto primary nc-btn-metasync-sync-now"
@click="syncMetaDiff"
@ -272,11 +273,9 @@
<script>
import { mapGetters } from 'vuex'
import viewIcons from '~/helpers/viewIcons'
import XBtn from '~/components/global/xBtn'
export default {
name: 'DisableOrEnableTables',
components: { XBtn },
props: ['nodes', 'db'],
data: () => ({
viewIcons,
@ -290,15 +289,20 @@ export default {
}),
async mounted() {
await this.loadXcDiff()
// await this.loadModels()
// await this.loadTableList()
// await this.loadMode// await this.loadTableList()
},
methods: {
async loadXcDiff() {
this.diff = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
dbAlias: this.db.meta.dbAlias,
env: this.$store.getters['project/GtrEnv']
}, 'xcMetaDiff'])
this.diff = (await this.$api.project.metaDiffGet(this.$store.state.project.projectId, this.db.id))
// this.diff = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// dbAlias: this.db.meta.dbAlias,
// env: this.$store.getters['project/GtrEnv']
// }, 'xcMetaDiff'])
},
clickReload() {
this.loadXcDiff()
this.$tele.emit('proj-meta:metadata:reload')
},
/* async addTableMeta(tables) {
try {
@ -345,10 +349,11 @@ export default {
*/
async syncMetaDiff() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
dbAlias: this.db.meta.dbAlias,
env: this.$store.getters['project/GtrEnv']
}, 'xcMetaDiffSync', {}])
await this.$api.project.metaDiffSync(this.$store.state.project.projectId, this.db.id)
// await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// dbAlias: this.db.meta.dbAlias,
// env: this.$store.getters['project/GtrEnv']
// }, 'xcMetaDiffSync', {}])
this.$toast.success('Table metadata recreated successfully').goAway(3000)
await this.loadXcDiff()
@ -448,7 +453,7 @@ export default {
return 0
}
if (this.tables && this.models) {
const tables = this.tables.filter(t => !isMetaTable(t.tn)).map(t => t.tn)
const tables = this.tables.filter(t => !isMetaTable(t.table_name)).map(t => t.table_name)
res.push(...this.models.map((m) => {
const i = tables.indexOf(m.title)
if (i === -1) {

4
packages/nc-gui/components/project/projectMetadata/uiAcl/toggleRelationsUIAcl.vue

@ -65,10 +65,10 @@
v-for="(relation,i) in relations"
:key="i"
>
<td>{{ relation.relationType === 'hm' ? relation.rtn : relation.tn }}</td>
<td>{{ relation.relationType === 'hm' ? relation.rtn : relation.table_name }}</td>
<td>{{ relation.relationType === 'hm' ? 'HasMany' : 'BelongsTo' }}</td>
<td>{{ relation.rtn }}</td>
<td>{{ relation.tn }}</td>
<td>{{ relation.table_name }}</td>
<td v-for="role in roles" :key="`${i}-${role}`">
<v-tooltip bottom>

66
packages/nc-gui/components/project/projectMetadata/uiAcl/toggleTableUIAcl.vue

@ -9,7 +9,7 @@
dense
hide-details
class="my-2 mx-auto search-field"
:placeholder="`Search '${db.connection.database}' models`"
placeholder="Search models"
style="max-width:300px"
outlined
>
@ -19,7 +19,6 @@
</v-icon>
</template>
</v-text-field>
<v-spacer />
<x-btn
outlined
@ -30,8 +29,7 @@
class="nc-acl-reload"
@click="loadTableList()"
>
<!-- Reload -->
{{ $t('general.reload') }}
Reload
</x-btn>
<x-btn
outlined
@ -53,15 +51,15 @@
<v-simple-table v-if="tables" dense style="min-width: 400px">
<thead>
<tr>
<th class="caption" bgcolor="#F5F5F5" width="100px">
<th class="caption" width="100px">
<!--TableName-->
{{ $t('labels.tableName') }}
</th>
<th class="caption" bgcolor="#F5F5F5" width="150px">
<th class="caption" width="150px">
<!--ViewName-->
{{ $t('labels.viewName') }}
</th>
<th v-for="role in roles" :key="role" class="caption" bgcolor="#F5F5F5" width="100px">
<th v-for="role in roles" :key="role" class="caption" width="100px">
{{ role.charAt(0).toUpperCase() + role.slice(1) }}
</th>
</tr>
@ -71,27 +69,30 @@
v-for="table in tables"
>
<tr
v-if="table._tn.toLowerCase().indexOf(filter.toLowerCase()) > -1"
:key="table.tn"
:class="`nc-acl-table-row nc-acl-table-row-${table._tn}`"
v-if="table.title.toLowerCase().indexOf(filter.toLowerCase()) > -1"
:key="table.table_name"
:class="`nc-acl-table-row nc-acl-table-row-${table.title}`"
>
<td>
<v-tooltip bottom>
<template #activator="{on}">
<span class="caption ml-2" v-on="on">{{ table.type === 'table' ? table._tn:table.type === 'view' ? table._tn : table.ptn.split("__")[1] }}</span>
<span
class="caption ml-2"
v-on="on"
>{{ table.ptype === 'table' ? table._ptn : table.ptype === 'view' ? table._ptn : table._ptn }}</span>
</template>
<span class="caption">{{ table.tn }}</span>
<span class="caption">{{ table.ptn || table._ptn }}</span>
</v-tooltip>
</td>
<td>
<v-icon small :color="viewIcons[table.type === 'vtable' ? table.show_as : table.type].color" v-on="on">
{{ viewIcons[table.type === 'vtable' ? table.show_as : table.type].icon }}
<v-icon small :color="viewIcons[table.type].color" v-on="on">
{{ viewIcons[table.type].icon }}
</v-icon>
<span v-if="table.ptn" class="caption">{{ table._tn }}</span>
<span v-if="table.ptn" class="caption">{{ table.title }}</span>
<span v-else class="caption">{{ $t('general.default') }}</span>
<!-- {{ table.show_as || table.type }}-->
</td>
<td v-for="role in roles" :key="`${table.tn}-${role}`">
<td v-for="role in roles" :key="`${table.table_name}-${role}`">
<v-tooltip bottom>
<template #activator="{on}">
<div
@ -99,7 +100,7 @@
>
<v-checkbox
v-model="table.disabled[role]"
:class="`pt-0 mt-0 nc-acl-${table._tn.toLowerCase().replace('_','')}-${role}-chkbox`"
:class="`pt-0 mt-0 nc-acl-${table.title.toLowerCase().replace('_','')}-${role}-chkbox`"
dense
hide-details
:true-value="false"
@ -109,10 +110,10 @@
</div>
</template>
<span v-if="table.disabled[role]">Click to make '{{ table.tn }}' visible for Role:{{
<span v-if="table.disabled[role]">Click to make '{{ table.table_name }}' visible for Role:{{
role
}} in UI dashboard</span>
<span v-else>Click to hide '{{ table.tn }}' for Role:{{ role }} in UI dashboard</span>
<span v-else>Click to hide '{{ table.table_name }}' for Role:{{ role }} in UI dashboard</span>
</v-tooltip>
</td>
</tr>
@ -147,25 +148,26 @@ export default {
},
methods: {
async loadTableList() {
this.tables = (await this.$store.dispatch('sqlMgr/ActSqlOp', [{
dbAlias: this.db.meta.dbAlias,
env: this.$store.getters['project/GtrEnv']
}, 'xcVisibilityMetaGet', {
type: 'all'
}]))
this.tables = (await this.$api.project.modelVisibilityList(
this.db.project_id, {
includeM2M: this.$store.state.windows.includeM2M || ''
}))
// this.tables = (await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// dbAlias: this.db.meta.dbAlias,
// env: this.$store.getters['project/GtrEnv']
// }, 'xcVisibilityMetaGet', {
// type: 'all'
// }]))
},
async save() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
dbAlias: this.db.meta.dbAlias,
env: this.$store.getters['project/GtrEnv']
}, 'xcVisibilityMetaSetAll', {
disableList: this.tables.filter(t => t.edited)
}])
await this.$api.project.modelVisibilitySet(this.db.project_id, this.tables.filter(t => t.edited))
this.$toast.success('Updated UI ACL for tables successfully').goAway(3000)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
this.$tele.emit('proj-meta:ui-acl:update')
}
},
computed: {
@ -176,7 +178,7 @@ export default {
return this.tables && this.tables.length && this.tables.some(t => t.edited)
},
roles() {
return this.tables && this.tables.length ? Object.keys(this.tables[0].disabled) : []
return ['editor', 'commenter', 'viewer']// this.tables && this.tables.length ? Object.keys(this.tables[0].disabled) : []
}
}
}

16
packages/nc-gui/components/project/restHandlerCodeEditor.vue

@ -82,25 +82,25 @@ export default {
functions = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.nodes.tn || this.nodes.view_name
table_name: this.nodes.table_name || this.nodes.view_name
}, 'defaultRestMiddlewareCodeGet', {
title: this.route.title,
relation_type: this.route.relation_type,
tn: this.nodes.tn || this.nodes.view_name
table_name: this.nodes.table_name || this.nodes.view_name
}])
} else {
functions = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.nodes.tn || this.nodes.view_name
table_name: this.nodes.table_name || this.nodes.view_name
}, 'defaultRestHandlerCodeGet', {
type: this.method,
path: this.route[this.method].path,
title: this.route[this.method].title,
relation_type: this.route[this.method].relation_type,
tnp: this.route[this.method].tnp,
tnc: this.route[this.method].tnc,
tn: this.nodes.tn || this.nodes.view_name
tnp: this.route[this.method].table_namep,
tnc: this.route[this.method].table_namec,
table_name: this.nodes.table_name || this.nodes.view_name
}])
}
if (functions && functions.length) {
@ -118,7 +118,7 @@ export default {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcRoutesMiddlewareUpdate', {
tn: this.nodes.tn || this.nodes.view_name,
table_name: this.nodes.table_name || this.nodes.view_name,
type: this.method,
functions: [this.code],
title: this.route.title
@ -133,7 +133,7 @@ export default {
functions: [this.code],
path: this.route[this.method].path,
relation_type: this.route[this.method].relation_type,
tn: this.nodes.tn || this.nodes.view_name
table_name: this.nodes.table_name || this.nodes.view_name
}])
}
this.$toast.success('API Handler updated successfully').goAway(3000)

13
packages/nc-gui/components/project/sequence.vue

@ -184,21 +184,11 @@ export default {
return
}
// // console.log("env: this.env", this.env, this.dbAlias);
// const client = await this.sqlMgr.projectGetSqlClient({
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// });
// const result = await client.sequenceList({
// sequence_name: this.originalNodes.sequence_name
// });
const result = await this.sqlMgr.sqlOp({
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'sequenceList', { sequence_name: this.originalNodes.sequence_name })
// console.log("sequence read", result);
this.sequence = { ...result.data.list.find(seq => seq.sequence_name === this.originalNodes.sequence_name) }
} catch (e) {
console.log(e)
@ -223,7 +213,6 @@ export default {
...this.nodes
}
})
console.log('create sequence result', result)
this.originalNodes.sequence_name = this.sequence.sequence_name
this.newSequence = false
await this.loadSequences()
@ -238,7 +227,6 @@ export default {
this.sequence
])
console.log('update sequence result', result)
this.$toast.success('Sequence updated successfully').goAway(3000)
}
} catch (e) {
@ -282,7 +270,6 @@ export default {
}
},
sequenceNameChanged() {
console.log('changed', this.sequence.sequence_name.trim())
this.edited = this.sequence.sequence_name.trim() !== ''
}
},

65
packages/nc-gui/components/project/settings/xcMeta.vue

@ -1,64 +1,12 @@
<template>
<div>
<h3 class="text-center mb-5 grey--text text--darken-2">
<!-- Metadata Operations -->
<div class="mt-5">
<!-- <h3 class="text-center mb-5 grey&#45;&#45;text text&#45;&#45;darken-2">
&lt;!&ndash; Metadata Operations &ndash;&gt;
{{ $t('title.metaOperations') }}
</h3>
</h3>-->
<v-simple-table class="ma-2 meta-table text-center mx-auto">
<!-- <thead>-->
<!-- <tr>-->
<!-- <th colspan="2" class="text-center title pa-2">Metadata Operations</th>-->
<!-- </tr>-->
<!-- </thead>-->
<tbody>
<!-- <tr>-->
<!-- <td>-->
<!-- &lt;!&ndash; Export all metadata from the meta tables to meta directory. &ndash;&gt;-->
<!-- {{ $t('tooltip.exportMetadata') }}-->
<!-- </td>-->
<!-- <td>-->
<!-- <v-btn-->
<!-- min-width="150"-->
<!-- color="primary"-->
<!-- small-->
<!-- outlined-->
<!-- :loading="loading === 'export-file'"-->
<!-- @click="exportMeta"-->
<!-- >-->
<!-- <v-icon small>-->
<!-- mdi-export-->
<!-- </v-icon>&nbsp;-->
<!-- &lt;!&ndash; Export to file &ndash;&gt;-->
<!-- {{ $t('activity.exportToFile') }}-->
<!-- </v-btn>-->
<!-- </td>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <td>-->
<!-- &lt;!&ndash; Import all metadata from the meta directory to meta tables. &ndash;&gt;-->
<!-- {{ $t('tooltip.importMetadata') }}-->
<!-- </td>-->
<!-- <td>-->
<!-- <v-btn-->
<!-- :loading="loading === 'import-file'"-->
<!-- min-width="150"-->
<!-- color="info"-->
<!-- small-->
<!-- outlined-->
<!-- @click="importMeta"-->
<!-- >-->
<!-- <v-icon small>-->
<!-- mdi-import-->
<!-- </v-icon>&nbsp;-->
<!-- &lt;!&ndash; Import &ndash;&gt;-->
<!-- {{ $t('activity.import') }}-->
<!-- </v-btn>-->
<!-- </td>-->
<!-- </tr>-->
<tr>
<td>
<!-- Export project meta to zip file and download. -->
@ -66,6 +14,7 @@
</td>
<td>
<v-btn
v-t="['proj-meta:export-zip:trigger']"
min-width="150"
color="primary"
small
@ -88,6 +37,7 @@
</td>
<td>
<v-btn
v-t="['proj-meta:import-zip']"
min-width="150"
:loading="loading === 'import-zip'"
color="info"
@ -232,6 +182,7 @@ export default {
}
this.dialogShow = false
this.loading = null
this.$tele.emit('proj-meta:export-zip:submit')
}
}
},
@ -307,7 +258,7 @@ export default {
zipFile
])
// this.$toast.success('Successfully imported metadata').goAway(3000)
this.$toast.success(`${this.$t('msg.toast.importMetadata')}`).goAway(3000)
this.$toast.success(`${this.$t('msg.toast.importMetadata')}`).goAway(3000)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}

29
packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js

@ -2,7 +2,6 @@ import inflection from 'inflection'
export default class GqlApi {
constructor(table, columns, meta, $ctx) {
// this.table = table;
this.columns = columns
this.meta = meta
this.$ctx = $ctx
@ -70,19 +69,19 @@ export default class GqlApi {
}
get gqlQueryListName() {
return `${this.meta._tn}List`
return `${this.meta.title}List`
}
get gqlQueryReadName() {
return `${this.meta._tn}Read`
return `${this.meta.title}Read`
}
get tableCamelized() {
return `${this.meta._tn}`
return `${this.meta.title}`
}
get gqlReqBody() {
return `\n${this.columns.map(c => c._cn).join('\n')}\n`
return `\n${this.columns.map(c => c.title).join('\n')}\n`
}
async gqlRelationReqBody(params) {
@ -92,11 +91,11 @@ export default class GqlApi {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.nodes.dbAlias,
env: this.$ctx.nodes.env,
tn: child
table_name: child
})
const meta = this.$ctx.$store.state.meta.metas[child]
if (meta) {
str += `\n${meta._tn}List{\n${meta.columns.map(c => c._cn).join('\n')}\n}`
str += `\n${meta.title}List{\n${meta.columns.map(c => c.title).join('\n')}\n}`
}
}
}
@ -105,11 +104,11 @@ export default class GqlApi {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.nodes.dbAlias,
env: this.$ctx.nodes.env,
tn: parent
table_name: parent
})
const meta = this.$ctx.$store.state.meta.metas[parent]
if (meta) {
str += `\n${meta._tn}Read{\n${meta.columns.map(c => c._cn).join('\n')}\n}`
str += `\n${meta.title}Read{\n${meta.columns.map(c => c.title).join('\n')}\n}`
}
}
}
@ -118,11 +117,11 @@ export default class GqlApi {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.nodes.dbAlias,
env: this.$ctx.nodes.env,
tn: mm
table_name: mm
})
const meta = this.$ctx.$store.state.meta.metas[mm]
if (meta) {
str += `\n${meta._tn}MMList{\n${meta.columns.map(c => c._cn).join('\n')}\n}`
str += `\n${meta.title}MMList{\n${meta.columns.map(c => c.title).join('\n')}\n}`
}
}
}
@ -170,7 +169,7 @@ export default class GqlApi {
const colName = Object.keys(data)[0]
this.$ctx.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: this.$ctx.nodes.dbAlias }, 'xcAuditCreate', {
tn: this.table,
table_name: this.table,
cn: colName,
pk: id,
value: data[colName],
@ -216,7 +215,7 @@ export default class GqlApi {
}
get table() {
return this.meta && this.meta._tn && inflection.camelize(this.meta._tn)
return this.meta && this.meta.title && inflection.camelize(this.meta.title)
}
async paginatedM2mNotChildrenList(params, assoc, pid) {
@ -225,7 +224,7 @@ export default class GqlApi {
m2mNotChildren(pid: $pid,assoc:$assoc,parent:$parent,limit:$limit, offset:$offset)
}`,
variables: {
parent: this.meta.tn, assoc, pid: pid + '', ...params
parent: this.meta.table_name, assoc, pid: pid + '', ...params
}
})
const count = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, {
@ -233,7 +232,7 @@ export default class GqlApi {
m2mNotChildrenCount(pid: $pid,assoc:$assoc,parent:$parent)
}`,
variables: {
parent: this.meta.tn, assoc, pid: pid + ''
parent: this.meta.table_name, assoc, pid: pid + ''
}
})
return { list: list.data.data.m2mNotChildren, count: count.data.data.m2mNotChildrenCount.count }

2
packages/nc-gui/components/project/spreadsheet/apis/grpcApi.js

@ -11,7 +11,7 @@ export default class GrpcApi {
env: this.ctx.nodes.env,
dbAlias: this.ctx.nodes.dbAlias
}, 'list', {
tn: this.table,
table_name: this.table,
size: params.limit,
page: ((params.offset || 0) / (params.limit || 20)) + 1
// orderBy:

6
packages/nc-gui/components/project/spreadsheet/apis/restApi.js

@ -6,7 +6,6 @@ export default class RestApi {
// todo: - get version letter and use table alias
async list(params) {
// const data = await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}`, params)
const data = await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}`, params)
return data.data
}
@ -44,8 +43,6 @@ export default class RestApi {
}
async paginatedList(params) {
// const list = await this.list(params);
// const count = (await this.count({where: params.where || ''})).count;
const [list, { count }] = await Promise.all([this.list(params), this.count({
where: params.where || '',
conditionGraph: params.conditionGraph
@ -54,9 +51,6 @@ export default class RestApi {
}
async paginatedM2mNotChildrenList(params, assoc, pid) {
/// api/v1/Film/m2mNotChildren/film_actor/44
// const list = await this.list(params);
// const count = (await this.count({where: params.where || ''})).count;
const { list, info: { count } } = (await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}/m2mNotChildren/${assoc}/${pid}`, params)).data
return { list, count }
}

499
packages/nc-gui/components/project/spreadsheet/components/columnFilter.vue

@ -1,131 +1,167 @@
<template>
<div
class="backgroundColor pa-2"
style="width:530px"
:style="{width:nested ? '100%' : '530px'}"
>
<div class="grid" @click.stop>
<template v-for="(filter,i) in filters" dense>
<v-icon
v-if="!filter.readOnly"
:key="i + '_3'"
small
class="nc-filter-item-remove-btn"
@click.stop="filters.splice(i,1)"
>
mdi-close-box
</v-icon>
<span v-else :key="i + '_1'" />
<template v-if="filter.status !== 'delete'">
<div v-if="filter.is_group" :key="i" style="grid-column: span 4; padding:6px" class="elevation-4 ">
<div class="d-flex" style="gap:6px; padding: 0 6px">
<v-icon
v-if="!filter.readOnly"
:key="i + '_3'"
small
class="nc-filter-item-remove-btn"
@click.stop="deleteFilter(filter,i)"
>
mdi-close-box
</v-icon>
<span v-else :key="i + '_1'" />
<v-select
v-model="filter.logical_op"
class="flex-shrink-1 flex-grow-0 elevation-0 caption "
:items="['and' ,'or']"
solo
flat
dense
hide-details
placeholder="Group op"
@click.stop
@change="saveOrUpdate(filter, i)"
>
<template #item="{item}">
<span class="caption font-weight-regular">{{ item }}</span>
</template>
</v-select>
</div>
<column-filter
v-if="filter.id || shared"
ref="nestedFilter"
v-model="filter.children"
:parent-id="filter.id"
:view-id="viewId"
nested
:meta="meta"
:shared="shared"
:web-hook="webHook"
:hook-id="hookId"
@updated="$emit('updated')"
@input="$emit('input', filters)"
/>
</div>
<template v-else>
<v-icon
v-if="!filter.readOnly"
:key="i + '_3'"
small
class="nc-filter-item-remove-btn"
@click.stop="deleteFilter(filter,i)"
>
mdi-close-box
</v-icon>
<span v-else :key="i + '_1'" />
<span
v-if="!i"
:key="i + '_2'"
class="caption d-flex align-center"
>{{ $t('labels.where') }}</span>
<span
v-if="!i"
:key="i + '_2'"
class="caption d-flex align-center"
>
<!-- where -->
{{ $t('labels.where') }}
</span>
<v-select
v-else
:key="i + '_4'"
v-model="filter.logical_op"
class="flex-shrink-1 flex-grow-0 elevation-0 caption "
:items="['and' ,'or']"
solo
flat
dense
hide-details
:disabled="filter.readOnly"
@click.stop
@change="filterUpdateCondition(filter, i)"
>
<template #item="{item}">
<span class="caption font-weight-regular">{{ item }}</span>
</template>
</v-select>
<v-select
v-else
:key="i + '_4'"
v-model="filter.logicOp"
class="flex-shrink-1 flex-grow-0 elevation-0 caption "
:items="['and' ,'or']"
solo
flat
dense
hide-details
:disabled="filter.readOnly"
@click.stop
>
<template #item="{item}">
<span class="caption font-weight-regular">{{ item }}</span>
<v-select
:key="i + '_6'"
v-model="filter.fk_column_id"
class="caption nc-filter-field-select"
:items="columns"
:placeholder="$t('objects.field')"
solo
flat
dense
:disabled="filter.readOnly"
hide-details
item-value="id"
item-text="title"
@click.stop
@change="saveOrUpdate(filter, i)"
>
<template #item="{item}">
<span
:class="`caption font-weight-regular nc-filter-fld-${item.title}`"
>
{{ item.title }}
</span>
</template>
</v-select>
<v-select
:key="'k' + i"
v-model="filter.comparison_op"
class="flex-shrink-1 flex-grow-0 caption nc-filter-operation-select"
:items="filterComparisonOp(filter)"
:placeholder="$t('labels.operation')"
solo
flat
style="max-width:120px"
dense
:disabled="filter.readOnly"
hide-details
item-value="value"
@click.stop
@change="filterUpdateCondition(filter, i)"
>
<template #item="{item}">
<span class="caption font-weight-regular">{{ item.text }}</span>
</template>
</v-select>
<span v-if="['null', 'notnull', 'empty', 'notempty'].includes(filter.comparison_op)" :key="'span' + i" />
<v-checkbox
v-else-if="types[filter.field] === 'boolean'"
:key="i + '_7'"
v-model="filter.value"
dense
:disabled="filter.readOnly"
@change="saveOrUpdate(filter, i)"
/>
<v-text-field
v-else
:key="i + '_7'"
v-model="filter.value"
solo
flat
hide-details
dense
class="caption nc-filter-value-select"
:disabled="filter.readOnly"
@click.stop
@input="saveOrUpdate(filter, i)"
/>
</template>
</v-select>
<v-text-field
v-if="filter.readOnly"
:key="i + '_5'"
v-model="filter.field"
class="caption "
:placeholder="$t('objects.field')"
solo
flat
dense
disabled
hide-details
@click.stop
>
<template #item="{item}">
<span class="caption font-weight-regular">{{ item }}</span>
</template>
</v-text-field>
<v-select
v-else
:key="i + '_6'"
v-model="filter.field"
class="caption nc-filter-field-select"
:items="fieldList"
:placeholder="$t('objects.field')"
solo
flat
dense
:disabled="filter.readOnly"
hide-details
@click.stop
>
<template #item="{item}">
<span class="caption font-weight-regular">{{ item }}</span>
</template>
</v-select>
<v-select
:key="'k' + i"
v-model="filter.op"
class="flex-shrink-1 flex-grow-0 caption nc-filter-operation-select"
:items="opList"
:placeholder="$t('labels.operation')"
solo
flat
style="max-width:120px"
dense
:disabled="filter.readOnly"
hide-details
@click.stop
>
<template #item="{item}">
<span class="caption font-weight-regular">{{ item }}</span>
</template>
</v-select>
<span v-if="['is null', 'is not null'].includes(filter.op)" :key="'span' + i" />
<v-checkbox
v-else-if="types[filter.field] === 'boolean'"
:key="i + '_7'"
v-model="filter.value"
dense
:disabled="filter.readOnly"
/>
<v-text-field
v-else
:key="i + '_7'"
v-model="filter.value"
solo
flat
hide-details
dense
class="caption nc-filter-value-select"
:disabled="filter.readOnly"
@click.stop
/>
</template>
</template>
</div>
<!-- <v-list-item dense class="pt-2 list-btn">
<v-btn @click.stop="addFilter" small class="elevation-0 grey&#45;&#45;text">
<v-icon small color="grey">mdi-plus</v-icon>
Add Filter
</v-btn>
</v-list-item>-->
<v-btn small class="elevation-0 grey--text my-3" @click.stop="addFilter">
<v-btn
small
class="elevation-0 grey--text my-3"
@click.stop="addFilter"
>
<v-icon small color="grey">
mdi-plus
</v-icon>
@ -141,7 +177,16 @@ import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes'
export default {
name: 'ColumnFilter',
props: ['fieldList', 'value', 'meta'],
props: {
fieldList: [Array],
meta: Object,
nested: Boolean,
parentId: String,
viewId: String,
shared: Boolean,
webHook: Boolean,
hookId: String
},
data: () => ({
filters: [],
opList: [
@ -152,20 +197,85 @@ export default {
'<',
'>=',
'<='
],
comparisonOp: [
{
text: 'is equal',
value: 'eq'
},
{
text: 'is not equal',
value: 'neq'
},
{
text: 'is like',
value: 'like'
},
{
text: 'is not like',
value: 'nlike'
},
{
text: 'is empty',
value: 'empty',
ignoreVal: true
},
{
text: 'is not empty',
value: 'notempty',
ignoreVal: true
},
{
text: 'is null',
value: 'null',
ignoreVal: true
},
{
text: 'is not null',
value: 'notnull',
ignoreVal: true
},
{
text: '>',
value: 'gt'
},
{
text: '<',
value: 'lt'
},
{
text: '>=',
value: 'gte'
},
{
text: '<=',
value: 'lte'
}
]
}),
computed: {
columnsById() {
return (this.columns || []).reduce((o, c) => ({ ...o, [c.id]: c }), {})
},
autoApply() {
return this.$store.state.windows.autoApplyFilter && !this.webHook
},
columns() {
return (this.meta && this.meta.columns.filter(c => c && (!c.colOptions || !c.system)))
},
types() {
if (!this.meta || !this.meta.columns || !this.meta.columns.length) { return {} }
if (!this.meta || !this.meta.columns || !this.meta.columns.length) {
return {}
}
return this.meta.columns.reduce((obj, col) => {
switch (col.uidt) {
case UITypes.Number:
case UITypes.Decimal:
obj[col._cn] = obj[col.cn] = 'number'
obj[col.title] = obj[col.column_name] = 'number'
break
case UITypes.Checkbox:
obj[col._cn] = obj[col.cn] = 'boolean'
obj[col.title] = obj[col.column_name] = 'boolean'
break
default:
break
@ -175,28 +285,159 @@ export default {
}
},
watch: {
async viewId(v) {
if (v) {
await this.loadFilter()
}
},
filters: {
handler(v) {
this.$emit('input', v)
this.$emit('input', v && v.filter(f => (f.fk_column_id && f.comparison_op) || f.is_group))
},
deep: true
},
value(v) {
this.filters = v || []
}
},
created() {
this.filters = this.value || []
this.loadFilter()
},
methods: {
filterComparisonOp(f) {
return this.comparisonOp.filter((op) => {
if (f && f.fk_column_id && this.columnsById[f.fk_column_id] &&
this.columnsById[f.fk_column_id].uidt === UITypes.LinkToAnotherRecord &&
this.columnsById[f.fk_column_id].uidt === UITypes.Lookup
) {
return ![
'notempty',
'empty',
'notnull',
'null'
].includes(op.value)
}
return true
})
},
async applyChanges(nested = false, { hookId } = {}) {
for (const [i, filter] of Object.entries(this.filters)) {
if (filter.status === 'delete') {
if (this.hookId || hookId) {
await this.$api.dbTableFilter.delete(filter.id)
} else {
await this.$api.dbTableFilter.delete(filter.id)
}
} else if (filter.status === 'update') {
if (filter.id) {
if (this.hookId || hookId) {
await this.$api.dbTableFilter.update(filter.id, {
...filter,
fk_parent_id: this.parentId
})
} else {
await this.$api.dbTableFilter.update(filter.id, {
...filter,
fk_parent_id: this.parentId
})
}
} else if (this.hookId || hookId) {
this.$set(this.filters, i, (await this.$api.dbTableWebhookFilter.create(this.hookId || hookId, {
...filter,
fk_parent_id: this.parentId
})))
} else {
this.$set(this.filters, i, (await this.$api.dbTableFilter.create(this.viewId, {
...filter,
fk_parent_id: this.parentId
})))
}
}
}
if (this.$refs.nestedFilter) {
for (const nestedFilter of this.$refs.nestedFilter) {
await nestedFilter.applyChanges(true)
}
}
this.loadFilter()
if (!nested) { this.$emit('updated') }
},
async loadFilter() {
let filters = []
if (this.viewId && this._isUIAllowed('filterSync')) {
filters = this.parentId
? (await this.$api.dbTableFilter.childrenRead(this.parentId))
: (await this.$api.dbTableFilter.read(this.viewId))
}
if (this.hookId && this._isUIAllowed('filterSync')) {
filters = this.parentId
? (await this.$api.dbTableFilter.childrenRead(this.parentId))
: (await this.$api.dbTableWebhookFilter.read(this.hookId))
}
this.filters = filters
},
addFilter() {
this.filters.push({
field: '',
op: '',
fk_column_id: null,
comparison_op: 'eq',
value: '',
logicOp: 'and'
status: 'update',
logical_op: 'and'
})
this.filters = this.filters.slice()
this.$tele.emit(`filter:add:trigger:${this.filters.length}`)
},
addFilterGroup() {
this.filters.push({
parentId: this.parentId,
is_group: true,
status: 'update'
})
this.filters = this.filters.slice()
const index = this.filters.length - 1
this.saveOrUpdate(this.filters[index], index)
},
filterUpdateCondition(filter, i) {
this.saveOrUpdate(filter, i)
this.$tele.emit(`filter:condition:${filter.logical_op}:${filter.comparison_op}`)
},
async saveOrUpdate(filter, i) {
if (this.shared || !this._isUIAllowed('filterSync')) {
// this.$emit('input', this.filters.filter(f => f.fk_column_id && f.comparison_op))
this.$emit('updated')
} else if (!this.autoApply) {
filter.status = 'update'
} else if (filter.id) {
await this.$api.dbTableFilter.update(filter.id, {
...filter,
fk_parent_id: this.parentId
})
this.$emit('updated')
} else {
this.$set(this.filters, i, (await this.$api.dbTableFilter.create(this.viewId, {
...filter,
fk_parent_id: this.parentId
})))
this.$emit('updated')
}
},
async deleteFilter(filter, i) {
if (this.shared || !this._isUIAllowed('filterSync')) {
this.filters.splice(i, 1)
this.$emit('updated')
} else if (filter.id) {
if (!this.autoApply) {
this.$set(filter, 'status', 'delete')
} else {
await this.$api.dbTableFilter.delete(filter.id)
await this.loadFilter()
this.$emit('updated')
}
} else {
this.filters.splice(i, 1)
this.$emit('updated')
}
this.$tele.emit('filter:delete')
}
}
}
@ -205,7 +446,7 @@ export default {
<style scoped>
.grid {
display: grid;
grid-template-columns:22px 80px auto 110px auto;
grid-template-columns:22px 80px auto auto auto;
column-gap: 6px;
row-gap: 6px
}

28
packages/nc-gui/components/project/spreadsheet/components/columnFilterMenu.vue

@ -1,5 +1,5 @@
<template>
<v-menu offset-y>
<v-menu offset-y eager>
<template #activator="{ on, }">
<v-badge
:value="filters.length"
@ -8,6 +8,7 @@
overlap
>
<v-btn
v-t="['filter:trigger']"
class="nc-filter-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
outlined
@ -27,7 +28,15 @@
</v-btn>
</v-badge>
</template>
<column-filter v-model="filters" :field-list="fieldList" :meta="meta">
<column-filter
ref="filter"
v-model="filters"
:shared="shared"
:view-id="viewId"
:field-list="fieldList"
:meta="meta"
v-on="$listeners"
>
<div class="d-flex align-center mx-2" @click.stop>
<v-checkbox
id="col-filter-checkbox"
@ -42,12 +51,12 @@
<span class="grey--text caption">
{{ $t('msg.info.filterAutoApply') }}
<!-- Auto apply -->
</span>
</span>
</template>
</v-checkbox>
<v-spacer />
<v-btn v-show="!autosave" color="primary" small class="caption ml-2" @click="$emit('input', filters)">
<v-btn v-show="!autosave" color="primary" small class="caption ml-2" @click="applyChanges">
Apply
changes
</v-btn>
@ -62,7 +71,7 @@ import ColumnFilter from '@/components/project/spreadsheet/components/columnFilt
export default {
name: 'ColumnFilterMenu',
components: { ColumnFilter },
props: ['fieldList', 'isLocked', 'value', 'meta'],
props: ['fieldList', 'isLocked', 'value', 'meta', 'viewId', 'shared'],
data: () => ({
filters: []
}),
@ -70,6 +79,7 @@ export default {
autosave: {
set(v) {
this.$store.commit('windows/MutAutoApplyFilter', v)
this.$tele.emit(`filter:auto-apply:${v}`)
},
get() {
return this.$store.state.windows.autoApplyFilter
@ -97,7 +107,13 @@ export default {
created() {
this.filters = this.autosave ? this.value || [] : JSON.parse(JSON.stringify(this.value || []))
},
methods: {}
methods: {
applyChanges() {
this.$emit('input', this.filters)
if (this.$refs.filter) { this.$refs.filter.applyChanges() }
this.$tele.emit('filter:apply-explicit')
}
}
}
</script>

115
packages/nc-gui/components/project/spreadsheet/components/editColumn.vue

@ -10,15 +10,15 @@
<v-container fluid @click.stop.prevent>
<v-row>
<v-col cols="12" class="mt-2">
<!--label: Column Name-->
<v-text-field
ref="column"
v-model="newColumn.cn"
v-model="newColumn.column_name"
hide-details="auto"
color="primary"
:rules="[
v => !!v || 'Required',
v => !meta || !meta.columns || meta.columns.every(c => column && c.cn === column.cn || v !== c.cn ) && meta.v.every(c => v !== c._cn ) || 'Duplicate column name',
v => !meta || !meta.columns || meta.columns.every(c => column && (c.column_name || '').toLowerCase() === (column.column_name || '').toLowerCase() ||(
(v||'').toLowerCase() !== (c.column_name||'').toLowerCase() && (v||'').toLowerCase() !== (c.title||'').toLowerCase())) || 'Duplicate column name' ,// && meta.v.every(c => v !== c.title ) || 'Duplicate column name',
validateColumnName
]"
class="caption nc-column-name-input"
@ -105,7 +105,8 @@
mdi-alert-outline
</v-icon>
</template>
Changing MultiSelect to SingleSelect can lead to errors when there are multiple values associated with a cell
Changing MultiSelect to SingleSelect can lead to errors when there are multiple values associated
with a cell
</v-alert>
</v-col>
@ -140,7 +141,7 @@
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:alias="newColumn.column_name"
:is-m-s-s-q-l="isMSSQL"
v-on="$listeners"
/>
@ -155,7 +156,7 @@
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:alias="newColumn.column_name"
:is-m-s-s-q-l="isMSSQL"
v-on="$listeners"
/>
@ -170,7 +171,7 @@
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:alias="newColumn.column_name"
:is-m-s-s-q-l="isMSSQL"
@onColumnSelect="onRelColumnSelect"
/>
@ -190,7 +191,7 @@
/>
</v-col>
<template v-if="newColumn.cn && newColumn.uidt && !isVirtual">
<template v-if="newColumn.column_name && newColumn.uidt && !isVirtual">
<v-col cols="12">
<v-container fluid class="wrapper">
<v-row>
@ -369,24 +370,11 @@
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:alias="newColumn.column_name"
:is-m-s-s-q-l="isMSSQL"
:sql-ui="sqlUi"
v-on="$listeners"
/>
<!-- <v-autocomplete
label="Formula"
hide-details
class="caption formula-type"
outlined
dense
:items="formulas"
>
<template #item="{item}">
<span class="green&#45;&#45;text text&#45;&#45;darken-2 caption font-weight-regular">{{ item }}</span>
</template>
</v-autocomplete>-->
</v-col>
</template>
</v-row>
@ -406,11 +394,20 @@
</v-container>
<v-col cols="12" class="d-flex pt-0">
<v-spacer />
<v-btn small outlined @click="close">
<v-btn
small
outlined
@click="close"
>
<!-- Cancel -->
{{ $t('general.cancel') }}
</v-btn>
<v-btn small color="primary" :disabled="!valid" @click="save">
<v-btn
small
color="primary"
:disabled="!valid"
@click="save"
>
<!-- Save -->
{{ $t('general.save') }}
</v-btn>
@ -429,6 +426,7 @@
</template>
<script>
import { MssqlUi, SqliteUi } from 'nocodb-sdk'
import { UITypes, uiTypes } from '../helpers/uiTypes'
import RollupOptions from './editColumn/rollupOptions'
import FormulaOptions from '@/components/project/spreadsheet/components/editColumn/formulaOptions'
@ -437,7 +435,6 @@ import CustomSelectOptions from '@/components/project/spreadsheet/components/edi
import RelationOptions from '@/components/project/spreadsheet/components/editColumn/relationOptions'
import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel'
import LinkedToAnotherOptions from '@/components/project/spreadsheet/components/editColumn/linkedToAnotherOptions'
import { SqliteUi, MssqlUi } from '@/helpers/sqlUi'
import { validateColumnName } from '~/helpers'
export default {
@ -502,7 +499,7 @@ export default {
return this.newColumn && this.newColumn.uidt === 'Rollup'
},
relation() {
return this.meta && this.column && this.meta.belongsTo && this.meta.belongsTo.find(bt => bt.cn === this.column.cn)
return this.meta && this.column && this.meta.belongsTo && this.meta.belongsTo.find(bt => bt.column_name === this.column.column_name)
},
isVirtual() {
return this.isLinkToAnotherRecord || this.isLookup || this.isRollup
@ -515,7 +512,6 @@ export default {
},
async created() {
this.genColumnData()
// await this.loadDataTypes();
},
mounted() {
this.focusInput()
@ -534,23 +530,8 @@ export default {
},
genColumnData() {
this.newColumn = this.column ? { ...this.column } : this.sqlUi.getNewColumn([...this.meta.columns, ...(this.meta.v || [])].length + 1)
this.newColumn.cno = this.newColumn.cn
this.newColumn.cno = this.newColumn.column_name
},
/*
async loadDataTypes() {
try {
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'getKnexDataTypes', {}])
this.dataTypes = result.data.list;
} catch (e) {
this.$toast.error('Error loading datatypes :' + e).goAway(4000);
throw e;
}
},
*/
close() {
this.$emit('close')
this.newColumn = {}
@ -560,9 +541,11 @@ export default {
return
}
try {
// if (this.newColumn.uidt === 'Formula') {
// return this.$toast.info('Coming Soon...').goAway(3000)
// }
if (this.newColumn.uidt === 'Formula') {
await this.$refs.formula.save()
return this.$emit('saved')
// return this.$toast.info('Coming Soon...').goAway(3000)
}
if (this.isLinkToAnotherRecord && this.$refs.relation) {
await this.$refs.relation.saveRelation()
@ -578,46 +561,28 @@ export default {
return await this.$refs.formula.save()
}
this.newColumn.tn = this.nodes.tn
this.newColumn._cn = this.newColumn.cn
const columns = [...this.meta.columns]
if (columns.length) {
columns[0].tn = this.nodes.tn
}
this.newColumn.table_name = this.nodes.table_name
this.newColumn.title = this.newColumn.column_name
if (this.editColumn) {
columns[this.columnIndex] = this.newColumn
await this.$api.dbTableColumn.update(this.column.id, this.newColumn)
} else {
columns.push(this.newColumn)
await this.$api.dbTableColumn.create(this.meta.id, this.newColumn)
}
await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableUpdate', {
tn: this.nodes.tn,
_tn: this.meta._tn,
originalColumns: this.meta.columns,
columns
}])
if (this.isRelation && this.$refs.relation) {
await this.$refs.relation.saveRelation()
}
this.$emit('saved', this.newColumn._cn, this.editColumn ? this.meta.columns[this.columnIndex]._cn : null)
this.$emit('saved', this.newColumn.title, this.editColumn ? this.meta.columns[this.columnIndex].title : null)
} catch (e) {
console.log(e)
}
this.$emit('close')
this.$tele.emit(`column:edit:save:${this.newColumn.uidt}`)
},
onDataTypeChange() {
this.newColumn.rqd = false
if (this.newColumn.uidt !== UITypes.ID) {
this.newColumn.pk = false
this.newColumn.primaryKey = false
}
this.newColumn.ai = false
this.newColumn.cdf = null
@ -682,16 +647,14 @@ export default {
},
this.relation.type === 'virtual' ? 'xcVirtualRelationDelete' : 'relationDelete',
{
childColumn: this.relation.cn,
childTable: this.nodes.tn,
childColumn: this.relation.column_name,
childTable: this.nodes.table_name,
parentTable: this.relation
.rtn,
parentColumn: this.relation
.rcn
}
])
console.log('relationDelete result ', result)
// await this.loadColumnList();
this.relationDeleteDlg = false
this.relation = null
this.$toast.success('Foreign Key deleted successfully').goAway(3000)

88
packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue

@ -28,9 +28,6 @@
@keydown.up.prevent="suggestionListUp"
@keydown.enter.prevent="selectText"
/>
<!-- </template>-->
<!-- <span class="caption">Example: AVG(column1, column2, column3)</span>-->
<!-- </v-tooltip>-->
</template>
<v-list v-if="suggestion" ref="sugList" dense max-height="50vh" style="overflow: auto">
<v-list-item-group
@ -60,11 +57,12 @@
<script>
import NcAutocompleteTree from '@/helpers/NcAutocompleteTree'
import { getWordUntilCaret, insertAtCursor } from '@/helpers'
import debounce from 'debounce'
import jsep from 'jsep'
import { UITypes } from 'nocodb-sdk'
import formulaList, { validations } from '../../../../../helpers/formulaList'
import { getWordUntilCaret, insertAtCursor } from '@/helpers'
import NcAutocompleteTree from '@/helpers/NcAutocompleteTree'
export default {
name: 'FormulaOptions',
@ -82,15 +80,21 @@ export default {
}),
computed: {
suggestionsList() {
console.log(this)
const unsupportedFnList = this.sqlUi.getUnsupportedFnList()
return [
...this.availableFunctions.filter(fn => !unsupportedFnList.includes(fn)).map(fn => ({
text: fn,
type: 'function'
})),
...this.meta.columns.map(c => ({ text: c._cn, type: 'column', c })),
...this.availableBinOps.map(op => ({ text: op, type: 'op' }))
...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({
text: c.title,
type: 'column',
c
})),
...this.availableBinOps.map(op => ({
text: op,
type: 'op'
}))
]
},
acTree() {
@ -101,35 +105,32 @@ export default {
return ref
}
},
watch: {
value(v, o) {
if (v !== o) {
this.formula = this.formula || {}
this.formula.value = v || ''
}
},
'formula.value'(v, o) {
if (v !== o) {
this.$emit('input', v)
}
}
},
created() {
this.formula = this.value ? { ...this.value } : {}
this.formula = { value: this.value || '' }
},
methods: {
async save() {
try {
await this.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.nodes.dbAlias,
env: this.nodes.env,
tn: this.meta.tn,
force: true
})
const meta = JSON.parse(JSON.stringify(this.$store.state.meta.metas[this.meta.tn]))
meta.v.push({
_cn: this.alias,
formula: {
...this.formula,
tree: jsep(this.formula.value)
}
})
const formulaCol = {
title: this.alias,
uidt: UITypes.Formula,
formula_raw: this.formula.value
}
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}])
const col = await this.$api.dbTableColumn.create(this.meta.id, formulaCol)
this.$toast.success('Formula column saved successfully').goAway(3000)
return this.$emit('saved', this.alias)
@ -139,9 +140,9 @@ export default {
},
async update() {
try {
const meta = JSON.parse(JSON.stringify(this.$store.state.meta.metas[this.meta.tn]))
const meta = JSON.parse(JSON.stringify(this.$store.state.meta.metas[this.meta.table_name]))
const col = meta.v.find(c => c._cn === this.column._cn && c.formula)
const col = meta.v.find(c => c.title === this.column.title && c.formula)
Object.assign(col, {
_cn: this.alias,
@ -156,7 +157,7 @@ export default {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
tn: this.nodes.table_name,
meta
}])
this.$toast.success('Formula column updated successfully').goAway(3000)
@ -194,7 +195,7 @@ export default {
}
pt.arguments.map(arg => this.validateAgainstMeta(arg, arr))
} else if (pt.type === 'Identifier') {
if (this.meta.columns.every(c => c._cn !== pt.name)) {
if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).every(c => c.title !== pt.name)) {
arr.push(`Column with name '${pt.name}' is not available`)
}
} else if (pt.type === 'BinaryExpression') {
@ -223,27 +224,12 @@ export default {
},
handleInput() {
this.selected = 0
// const $fakeDiv = this.$refs.fakeDiv
this.suggestion = null
const query = getWordUntilCaret(this.$refs.input.$el.querySelector('input')) // this.formula.value
// if (query !== '') {
const query = getWordUntilCaret(this.$refs.input.$el.querySelector('input'))
const parts = query.split(/\W+/)
this.wordToComplete = parts.pop()
// if (this.wordToComplete !== '') {
// get best match using popularity
this.suggestion = this.acTree.complete(this.wordToComplete)
this.autocomplete = !!this.suggestion.length
// } else {
// // $span.textContent = '' // clear ghost span
// }
// } else {
// this.autocomplete = false
// // $time.textContent = ''
// // $span.textContent = '' // clear ghost span
// }
},
selectText() {
if (this.selected > -1 && this.selected < this.suggestion.length) {

203
packages/nc-gui/components/project/spreadsheet/components/editColumn/linkedToAnotherOptions.vue

@ -13,13 +13,12 @@
>
<v-radio value="hm" label="Has Many" />
<v-radio value="mm" label="Many To Many" />
<!-- <v-radio disabled value="oo" label="One To One" />-->
</v-radio-group>
</v-col>
<v-col cols="12">
<v-autocomplete
ref="input"
v-model="relation.childTable"
v-model="relation.childId"
outlined
class="caption"
hide-details="auto"
@ -27,8 +26,8 @@
:label="$t('labels.childTable')"
:full-width="false"
:items="refTables"
item-text="_tn"
item-value="tn"
item-text="title"
item-value="id"
required
dense
:rules="tableRules"
@ -53,23 +52,7 @@
</v-container>
<v-container v-show="advanceOptions" fluid class="wrapper">
<v-row>
<!-- <v-col cols="6">
<v-text-field
outlined
class="caption"
hide-details
:label="$t('labels.childColumn')"
:full-width="false"
v-model="relation.childColumn"
required
dense
ref="childColumnRef"
@change="onColumnSelect"
></v-text-field>
</v-col
>-->
</v-row>
<v-row />
<template v-if="!isSQLite">
<v-row>
<v-col cols="6">
@ -83,7 +66,7 @@
:items="onUpdateDeleteOptions"
required
dense
:disabled="relation.type !== 'real'"
:disabled="relation.virtual"
/>
</v-col>
<v-col cols="6">
@ -97,30 +80,30 @@
:items="onUpdateDeleteOptions"
required
dense
:disabled="relation.type !== 'real'"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-checkbox
v-model="relation.type"
false-value="real"
true-value="virtual"
label="Virtual Relation"
:full-width="false"
required
class="mt-0"
dense
:disabled="relation.virtual"
/>
</v-col>
</v-row>
</template>
<v-row>
<v-col>
<v-checkbox
v-model="relation.virtual"
label="Virtual Relation"
:full-width="false"
required
class="mt-0"
dense
/>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
import { ModelTypes, UITypes } from 'nocodb-sdk'
export default {
name: 'LinkedToAnotherOptions',
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias'],
@ -147,159 +130,47 @@ export default {
]
},
tableRules() {
return [
v => !!v || 'Required',
(v) => {
if (this.type === 'mm') {
return !(this.meta.manyToMany || [])
.some(mm => (mm.tn === v && mm.rtn === this.meta.tn) || (mm.rtn === v && mm.tn === this.meta.tn)) ||
'Duplicate many to many relation is not allowed at the moment'
}
if (this.type === 'hm') {
return !(this.meta.hasMany || [])
.some(hm => hm.tn === v) ||
'Duplicate has many relation is not allowed at the moment'
}
}
]
return []
}
},
async created() {
await this.loadTablesList()
this.relation = {
childColumn: `${this.meta.tn}_id`,
childTable: this.nodes.tn,
parentId: null,
childID: null,
childColumn: `${this.meta.table_name}_id`,
childTable: this.nodes.table_name,
parentTable: this.column.rtn || '',
parentColumn: this.column.rcn || '',
onDelete: 'NO ACTION',
onUpdate: 'NO ACTION',
updateRelation: !!this.column.rtn,
type: 'real'
virtual: this.isSQLite
}
},
methods: {
async loadColumnList() {
this.isRefColumnsLoading = true
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'columnList', { tn: this.meta.tn }])
const columns = result.data.list
this.refColumns = JSON.parse(JSON.stringify(columns))
if (this.relation.updateRelation && !this.relationColumnChanged) {
// only first time when editing add defaault value to this field
this.relation.parentColumn = this.column.rcn
this.relationColumnChanged = true
} else {
// find pk column and assign to parentColumn
const pkKeyColumns = this.refColumns.filter(el => el.pk)
this.relation.parentColumn = (pkKeyColumns[0] || {}).cn || ''
}
this.onColumnSelect()
this.isRefColumnsLoading = false
},
async loadTablesList() {
this.isRefTablesLoading = true
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableList'])
const result = (await this.$api.dbTable.list(this.$store.state.project.projectId, this.$store.state.project.project.bases[0].id))
.list.filter(t => t.type === ModelTypes.TABLE)
this.refTables = result.data.list.map(({ tn, _tn }) => ({ tn, _tn }))
this.refTables = result // .data.list.map(({ table_name, title }) => ({ table_name, title }))
this.isRefTablesLoading = false
},
async saveManyToMany() {
// try {
// todo: toast
await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [
{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
},
'xcM2MRelationCreate',
{
_cn: this.alias,
...this.relation,
type: this.isSQLite || this.relation.type === 'virtual' ? 'virtual' : 'real',
parentTable: this.meta.tn,
updateRelation: !!this.column.rtn,
alias: this.alias
}
])
// } catch (e) {
// throw e
// }
},
async saveRelation() {
if (this.type === 'mm') {
await this.saveManyToMany()
return
}
// try {
const parentPK = this.meta.columns.find(c => c.pk)
const childTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableXcModelGet', {
tn: this.relation.childTable
}])
const childMeta = JSON.parse(childTableData.meta)
const newChildColumn = {}
Object.assign(newChildColumn, {
cn: this.relation.childColumn,
_cn: this.relation.childColumn,
rqd: false,
pk: false,
ai: false,
cdf: null,
dt: parentPK.dt,
dtxp: parentPK.dtxp,
dtxs: parentPK.dtxs,
un: parentPK.un,
altered: 1
await this.$api.dbTableColumn.create(this.meta.id, {
...this.relation,
parentId: this.meta.id,
uidt: UITypes.LinkToAnotherRecord,
title: this.alias,
type: this.type
})
const columns = [...childMeta.columns, newChildColumn]
await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableUpdate', {
tn: childMeta.tn,
_tn: childMeta._tn,
originalColumns: childMeta.columns,
columns
}])
await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [
{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
},
this.relation.type === 'real' && !this.isSQLite ? 'relationCreate' : 'xcVirtualRelationCreate',
{
...this.relation,
parentTable: this.meta.tn,
parentColumn: parentPK.cn,
updateRelation: !!this.column.rtn,
type: 'real',
alias: this.alias
}
])
// } catch (e) {
// throw e
// }
await this.$store.dispatch('meta/ActLoadMeta', { id: this.relation.childId, force: true })
},
onColumnSelect() {
const col = this.refColumns.find(c => this.relation.parentColumn === c.cn)
const col = this.refColumns.find(c => this.relation.parentColumn === c.column_name)
this.$emit('onColumnSelect', col)
}
}

116
packages/nc-gui/components/project/spreadsheet/components/editColumn/lookupOptions.vue

@ -12,15 +12,15 @@
:label="$t('labels.childTable')"
:full-width="false"
:items="refTables"
item-text="_ltn"
item-text="title"
:item-value="v => v"
:rules="[v => !!v || 'Required']"
dense
>
<template #item="{item}">
<span class="caption"><span class="font-weight-bold"> {{
item._ltn
}}</span> <small>({{ relationNames[item.type] }})
item.title || item.table_name
}}</span> <small>({{ relationNames[item.col.type] }})
</small></span>
</template>
</v-autocomplete>
@ -35,7 +35,7 @@
:label="$t('labels.childColumn')"
:full-width="false"
:items="columnList"
item-text="_lcn"
item-text="title"
dense
:loading="loadingColumns"
:item-value="v => v"
@ -49,6 +49,8 @@
<script>
import { isSystemColumn, UITypes } from 'nocodb-sdk'
export default {
name: 'LookupOptions',
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias'],
@ -64,62 +66,28 @@ export default {
}),
computed: {
refTables() {
return (this.meta
? [
...(this.meta.belongsTo || []).map(({ rtn, _rtn, rcn, tn, cn }) => ({
type: 'bt',
rtn,
_rtn,
rcn,
tn,
cn,
ltn: rtn,
_ltn: _rtn
})),
...(this.meta.hasMany || []).map(({
tn,
_tn,
cn,
rcn,
rtn
}) => ({
type: 'hm',
tn,
_tn,
cn,
rcn,
rtn,
ltn: tn,
_ltn: _tn
})),
...(this.meta.manyToMany || []).map(({ vtn, _vtn, vrcn, vcn, rtn, _rtn, rcn, tn, cn }) => ({
type: 'mm',
tn,
cn,
vtn,
_vtn,
vrcn,
rcn,
rtn,
vcn,
_rtn,
ltn: rtn,
_ltn: _rtn
}))
]
: []).filter(t => this.tables.includes(t.ltn))
if (!this.tables || !this.tables.length) {
return []
}
const refTables = this.meta.columns.filter(c =>
c.uidt === UITypes.LinkToAnotherRecord && !c.system
).map(c => ({
col: c.colOptions,
...this.tables.find(t => t.id === c.colOptions.fk_related_model_id)
}))
return refTables
},
columnList() {
return ((
this.lookup &&
this.lookup.table &&
this.$store.state.meta.metas &&
this.$store.state.meta.metas[this.lookup.table.ltn] &&
this.$store.state.meta.metas[this.lookup.table.ltn].columns
) || []).map(({ cn, _cn }) => ({
lcn: cn,
_lcn: _cn
}))
this.$store.state.meta.metas[this.lookup.table.id] &&
this.$store.state.meta.metas[this.lookup.table.id].columns
) || []).filter(c => !isSystemColumn(c))
}
},
async mounted() {
@ -127,12 +95,8 @@ export default {
},
methods: {
async loadTablesList() {
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableList'])
this.tables = result.data.list.map(({ tn }) => tn)
const result = (await this.$api.dbTable.list(this.$store.state.project.projectId, this.$store.state.project.project.bases[0].id))
this.tables = result.list
},
checkLookupExist(v) {
return (this.lookup.table && (this.meta.v || []).every(c => !(
@ -149,7 +113,7 @@ export default {
await this.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.nodes.dbAlias,
env: this.nodes.env,
tn: this.lookup.table.ltn
id: this.lookup.table.id
})
} catch (e) {
// ignore
@ -160,32 +124,18 @@ export default {
},
async save() {
try {
await this.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.nodes.dbAlias,
env: this.nodes.env,
tn: this.meta.tn,
force: true
})
const meta = JSON.parse(JSON.stringify(this.$store.state.meta.metas[this.meta.tn]))
meta.v.push({
_cn: this.alias,
lk: {
...this.lookup.table,
...this.lookup.column
}
})
const lookupCol = {
title: this.alias,
fk_relation_column_id: this.lookup.table.col.fk_column_id,
fk_lookup_column_id: this.lookup.column.id,
uidt: UITypes.Lookup
}
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}])
await this.$api.dbTableColumn.create(this.meta.id, lookupCol)
return this.$emit('saved', this.alias)
} catch (e) {
console.log(e)
this.$toast.error(e.message).goAway(3000)
}
}

25
packages/nc-gui/components/project/spreadsheet/components/editColumn/relationOptions.vue

@ -12,8 +12,8 @@
label="Reference Table"
:full-width="false"
:items="refTables"
item-text="_tn"
item-value="tn"
item-text="title"
item-value="table_name"
required
dense
@change="loadColumnList"
@ -31,8 +31,8 @@
label="Reference Column"
:full-width="false"
:items="refColumns"
item-text="_cn"
item-value="cn"
item-text="title"
item-value="column_name"
required
dense
@change="onColumnSelect"
@ -117,7 +117,7 @@ export default {
}
},
watch: {
'column.cn'(c) {
'column.column_name'(c) {
this.$set(this.relation, 'childColumn', c)
},
isSQLite(v) {
@ -127,8 +127,8 @@ export default {
async created() {
await this.loadTablesList()
this.relation = {
childColumn: this.column.cn,
childTable: this.nodes.tn,
childColumn: this.column.column_name,
childTable: this.nodes.table_name,
parentTable: this.column.rtn || '',
parentColumn: this.column.rcn || '',
onDelete: 'NO ACTION',
@ -148,7 +148,7 @@ export default {
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'columnList', { tn: this.relation.parentTable }])
}, 'columnList', { table_name: this.relation.parentTable }])
const columns = result.data.list
this.refColumns = JSON.parse(JSON.stringify(columns))
@ -160,7 +160,7 @@ export default {
} else {
// find pk column and assign to parentColumn
const pkKeyColumns = this.refColumns.filter(el => el.pk)
this.relation.parentColumn = (pkKeyColumns[0] || {}).cn || ''
this.relation.parentColumn = (pkKeyColumns[0] || {}).column_name || ''
}
this.onColumnSelect()
@ -174,7 +174,7 @@ export default {
dbAlias: this.nodes.dbAlias
}, 'tableList'])
this.refTables = result.data.list.map(({ tn, _tn }) => ({ tn, _tn }))
this.refTables = result.data.list.map(({ table_name, title }) => ({ table_name, title }))
this.isRefTablesLoading = false
},
async saveRelation() {
@ -187,12 +187,9 @@ export default {
this.relation.type === 'real' && !this.isSQLite ? 'relationCreate' : 'xcVirtualRelationCreate',
{ alias: this.alias, ...this.relation }
])
// } catch (e) {
// throw e
// }
},
onColumnSelect() {
const col = this.refColumns.find(c => this.relation.parentColumn === c.cn)
const col = this.refColumns.find(c => this.relation.parentColumn === c.column_name)
this.$emit('onColumnSelect', col)
}
}

113
packages/nc-gui/components/project/spreadsheet/components/editColumn/rollupOptions.vue

@ -12,15 +12,15 @@
:label="$t('labels.childTable')"
:full-width="false"
:items="refTables"
item-text="_rltn"
item-text="title"
:item-value="v => v"
:rules="[v => !!v || 'Required']"
dense
>
<template #item="{item}">
<span class="caption"><span class="font-weight-bold"> {{
item._rltn
}}</span> <small>({{ relationNames[item.type] }})
item.title || item.table_name
}}</span> <small>({{ relationNames[item.col.type] }})
</small></span>
</template>
</v-autocomplete>
@ -35,7 +35,7 @@
:label="$t('labels.childColumn')"
:full-width="false"
:items="columnList"
item-text="_rlcn"
item-text="title"
dense
:loading="loadingColumns"
:item-value="v => v"
@ -64,6 +64,8 @@
<script>
import { isSystemColumn, UITypes } from 'nocodb-sdk'
export default {
name: 'RollupOptions',
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias'],
@ -90,62 +92,25 @@ export default {
}),
computed: {
refTables() {
return (this.meta
? [
// ...(this.meta.belongsTo || []).map(({ rtn, _rtn, rcn, tn, cn }) => ({
// type: 'bt',
// rtn,
// _rtn,
// rcn,
// tn,
// cn,
// ltn: rtn,
// _ltn: _rtn
// })),
...(this.meta.hasMany || []).map(({
tn,
_tn,
cn,
rcn,
rtn
}) => ({
type: 'hm',
tn,
_tn,
cn,
rcn,
rtn,
rltn: tn,
_rltn: _tn
})),
...(this.meta.manyToMany || []).map(({ vtn, _vtn, vrcn, vcn, rtn, _rtn, rcn, tn, cn }) => ({
type: 'mm',
tn,
cn,
vtn,
_vtn,
vrcn,
rcn,
rtn,
vcn,
_rtn,
rltn: rtn,
_rltn: _rtn
}))
]
: []).filter(t => this.tables.includes(t.rltn))
if (!this.tables || !this.tables.length) { return [] }
const refTables = this.meta.columns.filter(c =>
c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== 'bt' && !c.system
).map(c => ({
col: c.colOptions,
...this.tables.find(t => t.id === c.colOptions.fk_related_model_id)
}))
return refTables
},
columnList() {
return ((
this.rollup &&
this.rollup.table &&
this.$store.state.meta.metas &&
this.$store.state.meta.metas[this.rollup.table.rltn] &&
this.$store.state.meta.metas[this.rollup.table.rltn].columns
) || []).map(({ cn, _cn }) => ({
rlcn: cn,
_rlcn: _cn
}))
this.$store.state.meta.metas[this.rollup.table.table_name] &&
this.$store.state.meta.metas[this.rollup.table.table_name].columns
) || []).filter(col => ![UITypes.Lookup, UITypes.Rollup, UITypes.LinkToAnotherRecord].includes(col.uidt) && !isSystemColumn(col))
}
},
async mounted() {
@ -153,12 +118,9 @@ export default {
},
methods: {
async loadTablesList() {
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableList'])
const result = (await this.$api.dbTable.list(this.$store.state.project.projectId, this.$store.state.project.project.bases[0].id))
this.tables = result.data.list.map(({ tn }) => tn)
this.tables = result.list
},
async onTableChange() {
this.loadingColumns = true
@ -167,7 +129,7 @@ export default {
await this.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.nodes.dbAlias,
env: this.nodes.env,
tn: this.rollup.table.ltn
id: this.rollup.table.id
})
} catch (e) {
// ignore
@ -178,30 +140,15 @@ export default {
},
async save() {
try {
await this.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.nodes.dbAlias,
env: this.nodes.env,
tn: this.meta.tn,
force: true
})
const meta = JSON.parse(JSON.stringify(this.$store.state.meta.metas[this.meta.tn]))
meta.v.push({
_cn: this.alias,
rl: {
...this.rollup.table,
...this.rollup.column,
fn: this.rollup.fn
}
})
const rollupCol = {
title: this.alias,
fk_relation_column_id: this.rollup.table.col.fk_column_id,
fk_rollup_column_id: this.rollup.column.id,
uidt: UITypes.Rollup,
rollup_function: this.rollup.fn
}
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}])
await this.$api.dbTableColumn.create(this.meta.id, rollupCol)
return this.$emit('saved', this.alias)
} catch (e) {

62
packages/nc-gui/components/project/spreadsheet/components/editVirtualColumn.vue

@ -12,40 +12,49 @@
<v-col cols="12">
<v-text-field
ref="column"
v-model="newColumn._cn"
v-model="newColumn.title"
hide-details="auto"
color="primary"
class="caption nc-column-name-input"
:label="$t('labels.columnName')"
:rules="[
v => !!v || 'Required',
v => !meta || !meta.columns || !column ||meta.columns.every(c => v !== c.cn ) && meta.v.every(c => column && c._cn === column._cn || v !== c._cn ) || 'Duplicate column name',
v => !meta || !meta.columns || !column ||meta.columns.every(c => column === c || (v !== c.title)) || 'Duplicate column name',
validateColumnName
]"
dense
outlined
/>
</v-col>
<v-col v-if="column.formula" cols="12">
<v-col v-if="newColumn && newColumn.uidt === UITypes.Formula" cols="12">
<formula-options
ref="formula"
:value="column.formula"
v-model="newColumn.formula_raw"
:column="column"
:new-column="newColumn"
:nodes="nodes"
:meta="meta"
:alias="newColumn._cn"
:alias="newColumn.title"
:sql-ui="sqlUi"
/>
</v-col>
<v-col cols="12" class="d-flex pt-0">
<v-spacer />
<v-btn x-small outlined @click="close">
<v-btn
x-small
outlined
@click="close"
>
<!-- Cancel -->
{{ $t('general.cancel') }}
</v-btn>
<v-btn x-small color="primary" :disabled="!valid" @click="save">
<v-btn
v-t="['virtual:column:edit']"
x-small
color="primary"
:disabled="!valid"
@click="save"
>
<!-- Save -->
{{ $t('general.save') }}
</v-btn>
@ -57,6 +66,7 @@
</template>
<script>
import { UITypes, substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import FormulaOptions from '@/components/project/spreadsheet/components/editColumn/formulaOptions'
import { validateColumnName } from '~/helpers'
@ -72,17 +82,28 @@ export default {
},
data: () => ({
valid: false,
newColumn: {}
newColumn: {},
UITypes
}),
watch: {
column(c) {
this.newColumn = { ...c }
const { colOptions, ...rest } = c
this.newColumn = rest
if (rest.uidt === UITypes.Formula) {
this.newColumn.formula_raw = substituteColumnIdWithAliasInFormula(colOptions.formula, this.meta.columns, colOptions.formula_raw)
}
}
},
async created() {
},
mounted() {
this.newColumn = { ...this.column }
const { colOptions, ...rest } = this.column
this.newColumn = rest
if (rest.uidt === UITypes.Formula) {
this.newColumn.formula_raw = substituteColumnIdWithAliasInFormula(colOptions.formula, this.meta.columns, colOptions.formula_raw)
}
},
methods: {
close() {
@ -92,25 +113,12 @@ export default {
async save() {
// todo: rollup update
try {
if (this.column.formula) {
await this.$refs.formula.update()
} else {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcUpdateVirtualKeyAlias', {
tn: this.nodes.tn,
oldAlias: this.column._cn,
newAlias: this.newColumn._cn
}])
this.$toast.success('Successfully updated alias').goAway(3000)
}
await this.$api.dbTableColumn.update(this.column.id, this.newColumn)
} catch (e) {
console.log(e)
console.log(this._extractSdkResponseErrorMsg(e))
this.$toast.error('Failed to update column alias').goAway(3000)
}
this.$emit('saved', this.newColumn._cn, this.column._cn)
this.$emit('saved', this.newColumn.title, this.column.title)
this.$emit('input', false)
},

7
packages/nc-gui/components/project/spreadsheet/components/editable.vue

@ -19,6 +19,13 @@ export default {
return { ...this.$listeners, input: this.onInput }
}
},
watch: {
value(v) {
if (this.$refs.editable.innerText !== v) {
this.$refs.editable.innerText = v
}
}
},
mounted() {
this.$refs.editable.innerText = this.value
},

4
packages/nc-gui/components/project/spreadsheet/components/editableCell.vue

@ -18,6 +18,7 @@
:column="column"
:is-public-grid="isPublic && !isForm"
:is-public-form="isPublic && isForm"
:view-id="viewId"
:is-locked="isLocked"
v-on="$listeners"
/>
@ -162,7 +163,8 @@ export default {
dummy: Boolean,
hint: String,
isLocked: Boolean,
isPublic: Boolean
isPublic: Boolean,
viewId: String
},
data: () => ({
changed: false,

9
packages/nc-gui/components/project/spreadsheet/components/editableCell/booleanCell.vue

@ -18,20 +18,11 @@ export default {
},
set(val) {
this.$emit('input', val)
// this.$emit('update');
}
},
parentListeners() {
const $listeners = {}
// if (this.$listeners.blur) {
// $listeners.blur = this.$listeners.blur
// }
// if (this.$listeners.focus) {
// $listeners.focus = this.$listeners.focus
// }
return $listeners
}
},

8
packages/nc-gui/components/project/spreadsheet/components/editableCell/dateTimePickerCell.vue

@ -41,11 +41,11 @@ export default {
return (/^\d+$/.test(this.value) ? dayjs(+this.value) : dayjs(this.value))
.format('YYYY-MM-DD HH:mm')
},
set(val) {
if(this.$parent.sqlUi.name == 'MysqlUi') {
this.$emit('input', val && dayjs(val).format('YYYY-MM-DD HH:mm:ss'))
set(value) {
if (this.$parent.sqlUi.name === 'MysqlUi') {
this.$emit('input', value && dayjs(value).format('YYYY-MM-DD HH:mm:ss'))
} else {
this.$emit('input', val && dayjs(val).format('YYYY-MM-DD HH:mm:ssZ'))
this.$emit('input', value && dayjs(value).format('YYYY-MM-DD HH:mm:ssZ'))
}
}
},

113
packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue

@ -20,47 +20,46 @@
<div class="d-flex align-center img-container">
<div class="d-flex no-overflow">
<div
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
:key="item.url || item.title"
class="thumbnail align-center justify-center d-flex"
>
<v-tooltip bottom>
<template #activator="{on}">
<!-- <img alt="#" v-if="isImage(item.title)" :src="item.url" v-on="on" @click="selectImage(item.url,i)">-->
<v-img
v-if="isImage(item.title)"
lazy-src="https://via.placeholder.com/60.png?text=Loading..."
alt="#"
max-height="33px"
contain
:src="item.url || item.data"
v-on="on"
@click="selectImage(item.url || item.data, i)"
>
<template #placeholder>
<v-skeleton-loader
type="image"
:height="active ? 33 : 22"
:width="active ? 33 : 22"
/>
</template>
</v-img>
<v-icon
v-else-if="item.icon"
:size="active ? 33 : 22"
v-on="on"
@click="openUrl(item.url || item.data,'_blank')"
>
{{
item.icon
}}
</v-icon>
<v-icon v-else :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url|| item.data,'_blank')">
mdi-file
</v-icon>
</template>
<span>{{ item.title }}</span>
</v-tooltip>
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
:key="item.url || item.title"
class="thumbnail align-center justify-center d-flex"
>
<v-tooltip bottom>
<template #activator="{on}">
<v-img
v-if="isImage(item.title)"
lazy-src="https://via.placeholder.com/60.png?text=Loading..."
alt="#"
max-height="33px"
contain
:src="item.url || item.data"
v-on="on"
@click="selectImage(item.url || item.data, i)"
>
<template #placeholder>
<v-skeleton-loader
type="image"
:height="active ? 33 : 22"
:width="active ? 33 : 22"
/>
</template>
</v-img>
<v-icon
v-else-if="item.icon"
:size="active ? 33 : 22"
v-on="on"
@click="openUrl(item.url || item.data,'_blank')"
>
{{
item.icon
}}
</v-icon>
<v-icon v-else :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url|| item.data,'_blank')">
mdi-file
</v-icon>
</template>
<span>{{ item.title }}</span>
</v-tooltip>
</div>
</div>
<div v-if="isForm || active && !isPublicGrid && !isLocked" class="add d-flex align-center justify-center px-1 nc-attachment-add" @click="addFile">
@ -214,7 +213,6 @@
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
:key="i"
>
<!-- <div class="d-flex justify-center" style="height:80px">-->
<v-card
:key="i"
class="ma-2 pa-2 d-flex align-center justify-center overlay-thumbnail"
@ -235,12 +233,9 @@
mdi-file
</v-icon>
</v-card>
<!-- </div>-->
</v-slide-item>
</v-slide-group>
</v-sheet>
<!-- <v-img v-if="showImage && selectedImage" max-width="90vh" max-height="95vh"-->
<!-- :src="selectedImage"></v-img>-->
<v-icon x-large class="close-icon" @click="showImage=false">
mdi-close-circle
</v-icon>
@ -257,7 +252,7 @@ import { isImage } from '@/components/project/spreadsheet/helpers/imageExt'
export default {
name: 'EditableAttachmentCell',
components: { draggable },
props: ['dbAlias', 'value', 'active', 'isLocked', 'meta', 'column', 'isPublicGrid', 'isForm', 'isPublicForm'],
props: ['dbAlias', 'value', 'active', 'isLocked', 'meta', 'column', 'isPublicGrid', 'isForm', 'isPublicForm', 'viewId'],
data: () => ({
carousel: null,
uploading: false,
@ -271,20 +266,15 @@ export default {
watch: {
value(val, prev) {
try {
this.localState = (typeof val === 'string' && val !== prev ? JSON.parse(val) : val) || []
this.localState = ((typeof val === 'string' && val !== prev ? JSON.parse(val) : val) || []).filter(Boolean)
} catch (e) {
this.localState = []
}
}
// localState(val) {
// if (this.isForm) {
// this.$emit('input', JSON.stringify(val))
// }
// }
},
created() {
try {
this.localState = (typeof this.value === 'string' ? JSON.parse(this.value) : this.value) || []
this.localState = ((typeof this.value === 'string' ? JSON.parse(this.value) : this.value) || []).filter(Boolean)
} catch (e) {
this.localState = []
}
@ -343,13 +333,16 @@ export default {
this.uploading = true
for (const file of this.$refs.file.files) {
try {
const item = await this.$store.dispatch('sqlMgr/ActUploadOld', [{
dbAlias: this.dbAlias
}, 'xcAttachmentUpload', {
appendPath: [this.meta.tn],
prependName: [this.column.cn]
}, file])
this.localState.push(item)
const data = await this.$api.storage.upload(
{
path: ['noco', this.projectName, this.meta.title, this.column.title].join('/')
}, {
files: file,
json: '{}'
}
)
this.localState.push(...data)
} catch (e) {
this.$toast.error((e.message) || 'Some internal error occurred').goAway(3000)
this.uploading = false

1
packages/nc-gui/components/project/spreadsheet/components/editableCell/editableUrlCell.vue

@ -16,7 +16,6 @@ export default {
return this.value
},
set(val) {
console.log(isValidURL(val))
if (isValidURL(val)) { this.$emit('input', val) }
}
},

1
packages/nc-gui/components/project/spreadsheet/components/editableCell/enumListEditableCell.vue

@ -10,7 +10,6 @@
:clearable="!column.rqd"
v-on="parentListeners"
>
<!-- <option v-for="eVal of enumValues" :key="eVal" :value="eVal">{{ eVal }}</option>-->
<template #selection="{item}">
<div
class="d-100"

2
packages/nc-gui/components/project/spreadsheet/components/editableCell/jsonEditableCell.vue

@ -98,8 +98,6 @@ export default {
<style scoped>
.cell-container {
/*margin: 0 -5px;*/
/*position: relative;*/
width: 100%
}
</style>

5
packages/nc-gui/components/project/spreadsheet/components/editableCell/setListEditableCell.vue

@ -1,8 +1,5 @@
<template>
<div>
<!-- <select v-on="parentListeners" v-model="localState" multiple>
<option v-for="val of setValues" :key="val" :value="val">{{ val }}</option>
</select>-->
<v-combobox
v-model="localState"
@ -18,7 +15,7 @@
>
<template #selection="data">
<v-chip
:key="data"
:key="data.item"
small
class="ma-1 "
:color="colors[setValues.indexOf(data.item) % colors.length]"

2
packages/nc-gui/components/project/spreadsheet/components/editableCell/textCell.vue

@ -6,7 +6,7 @@
export default {
name: 'TextCell',
props: {
value: String
value: [String, Object, Number, Boolean, Array]
},
computed: {
localState: {

1
packages/nc-gui/components/project/spreadsheet/components/editableCell/timePickerCell.vue

@ -40,7 +40,6 @@ export default {
return dateTime.format('HH:mm:ss')
},
set(val) {
console.log(val)
const dateTime = dayjs(`1999-01-01 ${val}:00`)
if (dateTime.isValid()) { this.$emit('input', dateTime.format('YYYY-MM-DD HH:mm:ssZ')) }
}

226
packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue

@ -8,7 +8,7 @@
</v-icon>
<template v-if="meta">
{{ meta._tn }}
{{ meta.title }}
</template>
<template v-else>
{{ table }}
@ -22,15 +22,6 @@
</v-icon>
</v-btn>
<x-icon
:tooltip="`${showSystemFields ? 'Hide' : 'Show'} system fields`"
icon.class="mr-3 mt-n1"
small
@click="showSystemFields = !showSystemFields"
>
mdi-table-headers-eye
</x-icon>
<x-icon
v-if="!isNew && _isUIAllowed('rowComments')"
icon-class="mr-2"
@ -82,18 +73,18 @@
v-for="(col,i) in fields"
>
<div
v-if="!col.lk"
v-if="!col.lk && (!showFields || showFields[col.title])"
:key="i"
:class="{
'active-row' : active === col._cn,
'active-row' : active === col.title,
required: isValid(col, localState)
}"
class="row-col my-4"
>
<div>
<label :for="`data-table-form-${col._cn}`" class="body-2 text-capitalize">
<label :for="`data-table-form-${col.title}`" class="body-2 text-capitalize">
<virtual-header-cell
v-if="col.virtual"
v-if="col.colOptions"
:column="col"
:nodes="nodes"
:is-form="true"
@ -102,15 +93,15 @@
<header-cell
v-else
:is-form="true"
:is-foreign-key="col.cn in belongsTo || col.cn in hasMany"
:value="col._cn"
:is-foreign-key="col.type === UITypes.ForeignKey"
:value="col.title"
:column="col"
:sql-ui="sqlUi"
/>
</label>
<virtual-cell
v-if="col.virtual"
v-if="isVirtualCol(col)"
ref="virtual"
:disabled-columns="disabledColumns"
:column="col"
@ -119,7 +110,6 @@
:meta="meta"
:api="api"
:active="true"
:sql-ui="sqlUi"
:is-new="isNew"
:is-form="true"
:breadcrumbs="localBreadcrumbs"
@ -128,7 +118,7 @@
/>
<div
v-else-if="col.ai || (col.pk && !isNew) || disabledColumns[col._cn]"
v-else-if="col.ai || (col.pk && !isNew) || disabledColumns[col.title]"
style="height:100%; width:100%"
class="caption xc-input"
@click="col.ai && $toast.info('Auto Increment field is not editable').goAway(3000)"
@ -137,14 +127,14 @@
style="height:100%; width: 100%"
readonly
disabled
:value="localState[col._cn]"
:value="localState[col.title]"
>
</div>
<editable-cell
v-else
:id="`data-table-form-${col._cn}`"
v-model="localState[col._cn]"
:id="`data-table-form-${col.title}`"
v-model="localState[col.title]"
:db-alias="dbAlias"
:column="col"
class="xc-input body-2"
@ -152,9 +142,9 @@
:sql-ui="sqlUi"
:is-form="true"
:is-locked="isLocked"
@focus="active = col._cn"
@focus="active = col.title"
@blur="active = ''"
@input="$set(changedColumns,col._cn, true)"
@input="$set(changedColumns,col.title, true)"
/>
</div>
</div>
@ -182,37 +172,46 @@
'darken-4':$vuetify.theme.dark
}"
>
<v-list-item v-for="log in logs" :key="log.id" class="d-flex">
<v-list-item-icon class="ma-0 mr-2">
<v-icon :color="isYou(log.user) ? 'pink lighten-2' : 'blue lighten-2'">
mdi-account-circle
</v-icon>
</v-list-item-icon>
<div class="flex-grow-1" style="min-width: 0">
<p class="mb-1 caption edited-text">
{{ isYou(log.user) ? 'You' : log.user==null?'Shared base':log.user }} {{
log.op_type === 'COMMENT' ? 'commented' : (
log.op_sub_type === 'INSERT' ? 'created' : 'edited'
)
}}
</p>
<p v-if="log.op_type === 'COMMENT'" class="caption mb-0 nc-chip" :style="{background :colors[2]}">
{{ log.description }}
</p>
<p v-else class="caption mb-0" style="word-break: break-all;" v-html="log.details" />
<p class="time text-right mb-0">
{{ calculateDiff(log.created_at) }}
</p>
</div>
</v-list-item>
<div>
<v-list-item v-for="log in logs" :key="log.id" class="d-flex">
<v-list-item-icon class="ma-0 mr-2">
<v-icon :color="isYou(log.user) ? 'pink lighten-2' : 'blue lighten-2'">
mdi-account-circle
</v-icon>
</v-list-item-icon>
<div class="flex-grow-1" style="min-width: 0">
<p class="mb-1 caption edited-text">
{{ isYou(log.user) ? 'You' : log.user == null ? 'Shared base' : log.user }} {{
log.op_type === 'COMMENT' ? 'commented' : (
log.op_sub_type === 'INSERT' ? 'created' : 'edited'
)
}}
</p>
<p v-if="log.op_type === 'COMMENT'" class="caption mb-0 nc-chip" :style="{background :colors[2]}">
{{ log.description }}
</p>
<p v-else class="caption mb-0" style="word-break: break-all;" v-html="log.details" />
<p class="time text-right mb-0">
{{ calculateDiff(log.created_at) }}
</p>
</div>
</v-list-item>
</div>
</v-list>
<v-spacer />
<v-divider />
<div class="d-flex align-center justify-center">
<v-switch v-model="commentsOnly" class="mt-1" dense hide-details @change="getAuditsAndComments">
<v-switch
v-model="commentsOnly"
v-t="['record:comment:comments-only']"
class="mt-1"
dense
hide-details
@change="getAuditsAndComments"
>
<template #label>
<span class="caption grey--text">Comments only</span>
</template>
@ -251,6 +250,7 @@
<v-btn
v-if="_isUIAllowed('rowComments')"
v-show="!toggleDrawer"
v-t="['record:comment-toggle']"
class="comment-icon"
color="primary"
fab
@ -264,13 +264,13 @@
<script>
import dayjs from 'dayjs'
import { AuditOperationSubTypes, AuditOperationTypes, isVirtualCol, UITypes } from 'nocodb-sdk'
import form from '../mixins/form'
import HeaderCell from '@/components/project/spreadsheet/components/headerCell'
import EditableCell from '@/components/project/spreadsheet/components/editableCell'
import colors from '@/mixins/colors'
import VirtualCell from '@/components/project/spreadsheet/components/virtualCell'
import VirtualHeaderCell from '@/components/project/spreadsheet/components/virtualHeaderCell'
import { UITypes } from '@/components/project/spreadsheet/helpers/uiTypes'
const relativeTime = require('dayjs/plugin/relativeTime')
const utc = require('dayjs/plugin/utc')
@ -278,9 +278,15 @@ dayjs.extend(utc)
dayjs.extend(relativeTime)
export default {
name: 'ExpandedForm',
components: { VirtualHeaderCell, VirtualCell, EditableCell, HeaderCell },
components: {
VirtualHeaderCell,
VirtualCell,
EditableCell,
HeaderCell
},
mixins: [colors, form],
props: {
showFields: Object,
showNextPrev: {
type: Boolean,
default: false
@ -295,8 +301,8 @@ export default {
value: Object,
table: String,
primaryValueColumn: String,
hasMany: [Object, Array],
belongsTo: [Object, Array],
// hasMany: [Object, Array],
// belongsTo: [Object, Array],
isNew: Boolean,
oldRow: Object,
iconColor: {
@ -307,9 +313,11 @@ export default {
queryParams: Object,
meta: Object,
presetValues: Object,
isLocked: Boolean,
isLocked: Boolean
},
data: () => ({
isVirtualCol,
UITypes,
showborder: false,
loadingLogs: true,
toggleDrawer: false,
@ -323,7 +331,7 @@ export default {
}),
computed: {
primaryKey() {
return this.isNew ? '' : this.meta.columns.filter(c => c.pk).map(c => this.localState[c._cn]).join('___')
return this.isNew ? '' : this.meta.columns.filter(c => c.pk).map(c => this.localState[c.title]).join('___')
},
edited() {
return !!Object.keys(this.changedColumns).length
@ -338,8 +346,8 @@ export default {
if (this.showSystemFields) {
return this.meta.columns || []
} else {
return this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn) &&
!((this.meta.v || []).some(v => v.bt && v.bt.cn === c.cn))
return this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.column_name) &&
!((this.meta.v || []).some(v => v.bt && v.bt.column_name === c.column_name))
) || []
}
},
@ -347,7 +355,7 @@ export default {
return Object.values(this.changedColumns).some(Boolean)
},
localBreadcrumbs() {
return [...this.breadcrumbs, `${this.meta ? this.meta._tn : this.table} (${this.primaryValue()})`]
return [...this.breadcrumbs, `${this.meta ? this.meta.title : this.table} ${this.primaryValue() ? `(${this.primaryValue()})` : ''}`]
}
},
watch: {
@ -389,17 +397,25 @@ export default {
},
async getAuditsAndComments() {
this.loadingLogs = true
const data = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: this.dbAlias }, 'xcModelRowAuditAndCommentList', {
model_id: this.meta.columns.filter(c => c.pk).map(c => this.localState[c._cn]).join('___'),
model_name: this.meta._tn,
comments: this.commentsOnly
}])
this.logs = data.list
const data = (await this.$api.utils.commentList({
row_id: this.meta.columns.filter(c => c.pk).map(c => this.localState[c.title]).join('___'),
fk_model_id: this.meta.id,
comments_only: this.commentsOnly
}))
this.logs = data.reverse()
this.loadingLogs = false
this.$nextTick(() => {
if (this.$refs.commentsList && this.$refs.commentsList.$el && this.$refs.commentsList.$el.firstElementChild) {
this.$refs.commentsList.$el.scrollTop = this.$refs.commentsList.$el.firstElementChild.offsetHeight
}
})
},
async save() {
try {
const id = this.meta.columns.filter(c => c.pk).map(c => this.localState[c._cn]).join('___')
const id = this.meta.columns.filter(c => c.pk).map(c => this.localState[c.title]).join('___')
if (this.presetValues) {
// cater presetValues
@ -414,7 +430,10 @@ export default {
}, {})
if (this.isNew) {
const data = await this.api.insert(updatedObj)
const data = (await this.$api.dbTableRow.create(
'noco',
this.projectName,
this.meta.title, updatedObj))
this.localState = { ...this.localState, ...data }
// save hasmany and manytomany relations from local state
@ -431,7 +450,24 @@ export default {
if (!id) {
return this.$toast.info('Update not allowed for table which doesn\'t have primary Key').goAway(3000)
}
await this.api.update(id, updatedObj, this.oldRow)
await this.$api.dbTableRow.update(
'noco',
this.projectName,
this.meta.title,
id,
updatedObj
)
for (const key of Object.keys(updatedObj)) {
// audit
this.$api.utils.auditRowUpdate(id, {
fk_model_id: this.meta.id,
column_name: key,
row_id: id,
value: updatedObj[key],
prev_value: this.oldRow[key]
}).then(() => {
})
}
} else {
return this.$toast.info('No columns to update').goAway(3000)
}
@ -447,31 +483,27 @@ export default {
} catch (e) {
this.$toast.error(`Failed to update row : ${e.message}`).goAway(3000)
}
this.$tele.emit('record:add:submit')
},
async reload() {
const id = this.meta.columns.filter(c => c.pk).map(c => this.localState[c._cn]).join('___')
// const where = this.meta.columns.filter(c => c.pk).map(c => `(${c._cn},eq,${this.localState[c._cn]})`).join('~and')
const id = this.meta.columns.filter(c => c.pk).map(c => this.localState[c.title]).join('___')
this.$set(this, 'changedColumns', {})
this.localState = await this.api.read(id, this.queryParams || {})
// const data = await this.api.list({ ...(this.queryParams || {}), where }) || [{}]
// this.localState = data[0] || this.localState
if (!this.isNew && this.toggleDrawer) {
this.getAuditsAndComments()
}
this.localState = (await this.$api.dbTableRow.read(
'noco',
this.projectName,
this.meta.title, id, { query: this.queryParams || {} }))
},
calculateDiff(date) {
return dayjs.utc(date).fromNow()
},
async saveComment() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [
{ dbAlias: this.dbAlias },
'xcAuditCommentInsert', {
model_id: this.meta.columns.filter(c => c.pk).map(c => this.localState[c._cn]).join('___'),
model_name: this.meta._tn,
description: this.comment
}
])
await this.$api.utils.commentRow({
fk_model_id: this.meta.id,
row_id: this.meta.columns.filter(c => c.pk).map(c => this.localState[c.title]).join('___'),
description: this.comment
})
this.comment = ''
this.$toast.success('Comment added successfully').goAway(3000)
this.$emit('commented')
@ -479,22 +511,32 @@ export default {
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
this.$tele.emit('record:comment:insert')
},
primaryValue() {
if (this.localState) {
const value = this.localState[this.primaryValueColumn]
const col = this.meta.columns.find(c => c._cn == this.primaryValueColumn)
if (!col) { return }
const col = this.meta.columns.find(c => c.title == this.primaryValueColumn)
if (!col) {
return
}
const uidt = col.uidt
if (uidt == UITypes.Date) {
return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD')
} else if (uidt == UITypes.DateTime) {
return (/^\d+$/.test(this.value) ? dayjs(+this.value) : dayjs(this.value)).format('YYYY-MM-DD HH:mm')
return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD HH:mm')
} else if (uidt == UITypes.Time) {
let dateTime = dayjs(value)
if (!dateTime.isValid()) { dateTime = dayjs(value, 'HH:mm:ss') }
if (!dateTime.isValid()) { dateTime = dayjs(`1999-01-01 ${value}`) }
if (!dateTime.isValid()) { return value }
if (!dateTime.isValid()) {
dateTime = dayjs(value, 'HH:mm:ss')
}
if (!dateTime.isValid()) {
dateTime = dayjs(`1999-01-01 ${value}`)
}
if (!dateTime.isValid()) {
return value
}
return dateTime.format('HH:mm:ss')
}
return value
@ -635,8 +677,8 @@ h5 {
background: var(--v-backgroundColorDefault-base);
}
.nc-chip{
padding:8px;
.nc-chip {
padding: 8px;
border-radius: 8px;
}
</style>

85
packages/nc-gui/components/project/spreadsheet/components/extras.vue

@ -13,14 +13,6 @@
<div class="text-center caption grey--text mt-3 mb-1">
Built with Vue JS<br><img src="vue.svg" class="vue-icon mt-1 mb-n1" alt="vue.js" width="30">
</div>
<!-- <div class="justify-center caption grey&#45;&#45;text mt-2 d-flex align-center ">
<img src="favicon-32.png" alt="nocodb" width="20px">
<v-icon size="13" color="red" class="mx-3">
mdi-heart
</v-icon>
<img src="vue.svg" class="vue-icon" alt="vue.js" width="20px">
</div>-->
</div>
<template v-else>
<div class="d-flex justify-end">
@ -29,73 +21,75 @@
class="
flex-shrink-1
text-left
elevation-1
elevation-0
rounded-sm
community-card
item
"
:class="{ active: showCommunity }"
:class="{ active: true }"
dense
>
<v-list-item dense href="https://discord.gg/5RgZmkW" target="_blank">
<!-- Get your questions answered -->
<!-- Join Discord -->
<v-list-item-title>
<v-icon class="mr-1" small :color="textColors[0]">
mdi-discord
</v-icon>
<span class="caption" :title="$t('labels.community.joinDiscord')">{{
<span class="caption" :title="$t('labels.community.joinDiscord')" v-t="['community:discord']">{{
$t('labels.community.joinDiscord')
}}</span>
</v-list-item-title>
</v-list-item>
<v-divider />
<!-- Join Community -->
<v-list-item dense href="https://community.nocodb.com/" target="_blank">
<v-list-item-title>
<v-icon class="mr-1 discourse" small :color="textColors[0]">
mdi-discourse
</v-icon>
<span class="caption" :title="$t('labels.community.joinCommunity')" v-t="['community:discourse']">{{
$t('labels.community.joinCommunity')
}}</span>
</v-list-item-title>
</v-list-item>
<v-list-item dense href="https://twitter.com/NocoDB" target="_blank">
<!-- Join Reddit -->
<v-list-item-title>
<v-icon class="mr-1" small color="#ff4600">
mdi-reddit
</v-icon>
<span class="caption" :title="$t('labels.community.joinReddit')" v-t="['community:reddit']">{{
$t('labels.community.joinReddit')
}}</span>
</v-list-item-title>
</v-list-item>
<v-list-item
dense
target="_blank"
href="https://calendly.com/nocodb-meeting"
>
<!-- Follow NocoDB -->
<v-list-item-title>
<v-icon class="mr-1" small :color="textColors[1]">
mdi-twitter
</v-icon>
<span class="caption" title="$t('labels.community.followNocodb')"> {{
<span class="caption" title="$t('labels.community.followNocodb')" v-t="['community:twitter']"> {{
$t('labels.community.followNocodb')
}}</span>
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item dense href="https://www.reddit.com/r/NocoDB/" target="_blank">
<!-- Get your questions answered -->
<v-list-item-title>
<v-icon class="mr-1" small color="#ff4600">
mdi-reddit
</v-icon>
<span class="caption" :title="$t('labels.community.joinReddit')">{{
$t('labels.community.joinReddit')
}}</span>
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item
dense
target="_blank"
href="https://calendly.com/nocodb-meeting"
>
<!-- Book a Free DEMO -->
<v-list-item-title>
<v-icon class="mr-1" small :color="textColors[3]">
mdi-calendar-month
</v-icon>
<span class="caption" :title="$t('labels.community.bookDemo')">{{
<span class="caption" :title="$t('labels.community.bookDemo')" v-t="['community:book-demo']">{{
$t('labels.community.bookDemo')
}}</span>
</v-list-item-title>
</v-list-item>
</v-list>
</div>
<sponsor-mini
:class="{ active: !showCommunity }"
class="item"
:nav="true"
/>
</template>
</div>
</template>
@ -107,7 +101,7 @@ import colors from '~/mixins/colors'
export default {
name: 'Extras',
components: { ShareIcons, SponsorMini },
components: { ShareIcons },
mixins: [colors],
data: () => ({
showCommunity: true
@ -143,6 +137,19 @@ export default {
}
}
.v-icon.discourse {
height: 16px;
width: 16px;
background-image: url('~/assets/img/discourse-icon.png');
background-size: contain;
background-repeat: no-repeat;
}
.v-icon.discourse::before {
visibility: hidden;
content: "";
}
//
//@keyframes anim {
// 0%, 100% {

162
packages/nc-gui/components/project/spreadsheet/components/fieldsMenu.vue

@ -8,6 +8,7 @@
overlap
>
<v-btn
v-t="['fields:trigger']"
class="nc-fields-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
outlined
@ -39,7 +40,7 @@
outlined
:items="attachmentFields"
item-text="alias"
item-value="_cn"
item-value="id"
hide-details
@click.stop
>
@ -63,7 +64,7 @@
outlined
:items="singleSelectFields"
item-text="alias"
item-value="_cn"
item-value="title"
hide-details
@click.stop
>
@ -98,29 +99,40 @@
</template>-->
</v-text-field>
</v-list-item>
<draggable v-model="fieldsOrderLoc" @start="drag=true" @end="drag=false">
<draggable
v-model="fields"
@start="drag=true"
@end="drag=false"
@change="onMove($event)"
>
<template
v-for="field in fieldsOrderLoc"
v-for="(field,i) in fields"
>
<v-list-item
v-if="field && field.toLowerCase().indexOf(fieldFilter.toLowerCase()) > -1"
:key="field"
v-show="(!fieldFilter || (field.title||'').toLowerCase().includes(fieldFilter.toLowerCase()))
&& !(!showSystemFieldsLoc && systemColumnsIds.includes(field.fk_column_id))
"
:key="field.id"
dense
>
<v-checkbox
v-model="showFields[field]"
v-model="field.show"
class="mt-0 pt-0"
dense
hide-details
@click.stop
@change="saveOrUpdate(field, i)"
>
<template #label>
<span class="caption">{{ field }}</span>
<span class="caption">{{ field.title }}</span>
</template>
</v-checkbox>
<v-spacer />
<v-icon small color="grey"
:class="`align-self-center drag-icon nc-child-draggable-icon-${field}`">
<v-icon
small
color="grey"
:class="`align-self-center drag-icon nc-child-draggable-icon-${field}`"
>
mdi-drag
</v-icon>
</v-list-item>
@ -142,7 +154,7 @@
<span class="caption">
<!-- Show System Fields -->
{{ $t('activity.showSystemFields') }}
</span>
</span>
</template>
</v-checkbox>
</v-list-item>
@ -162,6 +174,7 @@
<script>
import draggable from 'vuedraggable'
import { getSystemColumnsIds } from 'nocodb-sdk'
export default {
name: 'FieldsMenu',
@ -179,28 +192,33 @@ export default {
value: [Object, Array],
fieldList: [Array, Object],
showSystemFields: {
type: Boolean,
type: [Boolean, Number],
default: false
},
isLocked: Boolean,
isPublic: Boolean
isPublic: Boolean,
viewId: String
},
data: () => ({
fields: [],
fieldFilter: '',
showFields: {},
fieldsOrderLoc: []
}),
computed: {
systemColumnsIds() {
return getSystemColumnsIds(this.meta && this.meta.columns)
},
attachmentFields() {
return [...(this.meta && this.meta.columns ? this.meta.columns.filter(f => f.uidt === 'Attachment') : []), {
alias: 'None',
_cn: ''
id: null
}]
},
singleSelectFields() {
return [...(this.meta && this.meta.columns ? this.meta.columns.filter(f => f.uidt === 'SingleSelect') : []), {
alias: 'None',
_cn: ''
id: null
}]
},
coverImageFieldLoc: {
@ -220,11 +238,18 @@ export default {
}
},
columnMeta() {
return this.meta && this.meta.columns ? this.meta.columns.reduce((o, c) => ({ ...o, [c._cn]: c }), {}) : {}
return this.meta && this.meta.columns
? this.meta.columns.reduce((o, c) => ({
...o,
[c.title]: c
}), {})
: {}
},
isAnyFieldHidden() {
return Object.values(this.showFields).some(v => !v)
return this.fields.some(f => !(!this.showSystemFieldsLoc && this.systemColumnsIds.includes(f.fk_column_id)) &&
!f.show
)// Object.values(this.showFields).some(v => !v)
},
showSystemFieldsLoc: {
get() {
@ -232,16 +257,27 @@ export default {
},
set(v) {
this.$emit('update:showSystemFields', v)
this.showFields = this.fields.reduce((o, c) => ({ [c.title]: c.show, ...o }), {})
this.$emit('update:fieldsOrder', this.fields.map(c => c.title))
this.$tele.emit('fields:system-field-checkbox')
}
}
},
watch: {
async viewId(v) {
if (v) {
await this.loadFields()
}
},
fieldList(f) {
this.fieldsOrderLoc = [...f]
},
showFields: {
handler(v) {
this.$emit('input', v)
this.$nextTick(() => {
this.$emit('input', v)
})
},
deep: true
},
@ -265,17 +301,97 @@ export default {
}
},
created() {
this.loadFields()
this.showFields = this.value
this.fieldsOrderLoc = this.fieldsOrder && this.fieldsOrder.length ? this.fieldsOrder : [...this.fieldList]
},
methods: {
showAll() {
async loadFields() {
let fields = []
let order = 1
if (this.viewId) {
const data = await this.$api.dbViewColumn.list(this.viewId)
const fieldById = data.reduce((o, f) => ({
...o,
[f.fk_column_id]: f
}), {})
fields = this.meta.columns.map(c => ({
title: c.title,
fk_column_id: c.id,
...(fieldById[c.id] ? fieldById[c.id] : {}),
order: (fieldById[c.id] && fieldById[c.id].order) || order++
})
).sort((a, b) => a.order - b.order)
} else if (this.isPublic) {
fields = this.meta.columns
}
this.fields = fields
this.$emit('input', this.fields.reduce((o, c) => ({
...o,
[c.title]: c.show
}), {}))
this.$emit('update:fieldsOrder', this.fields.map(c => c.title))
},
async saveOrUpdate(field, i) {
if (!this.isPublic && this._isUIAllowed('fieldsSync')) {
if (field.id) {
await this.$api.dbViewColumn.update(this.viewId, field.id, field)
} else {
this.fields[i] = (await this.$api.dbViewColumn.create(this.viewId, field))
}
}
this.$emit('updated')
this.$emit('input', this.fields.reduce((o, c) => ({
...o,
[c.title]: c.show
}), {}))
this.$emit('update:fieldsOrder', this.fields.map(c => c.title))
this.$tele.emit('fields:show-hide-checkbox')
},
async showAll() {
if (!this.isPublic) {
await this.$api.dbView.showAllColumn(this.viewId)
}
for (const f of this.fields) {
f.show = true
}
this.$emit('updated')
// eslint-disable-next-line no-return-assign,no-sequences
this.showFields = (this.fieldsOrderLoc || Object.keys(this.showFields)).reduce((o, k) => (o[k] = true, o), {})
this.$tele.emit('fields:show-all')
},
hideAll() {
// eslint-disable-next-line no-return-assign,no-sequences
this.showFields = (this.fieldsOrderLoc || Object.keys(this.showFields)).reduce((o, k) => (o[k] = false, o), {})
async hideAll() {
if (!this.isPublic) {
await this.$api.dbView.hideAllColumn(this.viewId)
}
for (const f of this.fields) {
f.show = false
}
this.$emit('updated')
this.$nextTick(() => {
this.showFields = (this.fieldsOrderLoc || Object.keys(this.showFields)).reduce((o, k) => (o[k] = false, o), {})
})
this.$tele.emit('fields:hide-all')
},
onMove(event) {
if (this.fields.length - 1 === event.moved.newIndex) {
this.$set(this.fields[event.moved.newIndex], 'order', this.fields[event.moved.newIndex - 1].order + 1)
} else if (event.moved.newIndex === 0) {
this.$set(this.fields[event.moved.newIndex], 'order', this.fields[1].order / 2)
} else {
this.$set(this.fields[event.moved.newIndex], 'order', (
this.fields[event.moved.newIndex - 1].order + this.fields[event.moved.newIndex + 1].order) / 2
)
}
this.saveOrUpdate(this.fields[event.moved.newIndex], event.moved.newIndex)
this.$tele.emit('fields:drag')
}
}
}
@ -304,7 +420,7 @@ export default {
max-height: 20px !important;
}
.field-icon{
.field-icon {
margin-top: 2px;
}
}

1
packages/nc-gui/components/project/spreadsheet/components/fieldsMenuItem.vue

@ -12,7 +12,6 @@
</v-icon>
<span v-else-if="isInt" class="font-weight-bold mr-1" style="font-size: 15px">#</span>
<!-- <v-icon color="grey" class="mr-1" v-if="isInt">mdi-numeric</v-icon>-->
<v-icon v-else-if="isFloat" color="grey" class="mr-1 mt-n1">
mdi-decimal
</v-icon>

71
packages/nc-gui/components/project/spreadsheet/components/headerCell.vue

@ -15,7 +15,6 @@
</v-icon>
<span v-else-if="isInt" class="font-weight-bold mr-1" style="font-size: 15px">#</span>
<!-- <v-icon color="grey" class="mr-1" v-if="isInt">mdi-numeric</v-icon>-->
<v-icon v-else-if="isFloat" color="grey" class="mr-1 mt-n1">
mdi-decimal
</v-icon>
@ -43,15 +42,15 @@
<span class="name" style="white-space: nowrap" :title="value">{{ value }}</span>
<span v-if="(column.rqd && !column.default) || required" class="error--text text--lighten-1">&nbsp;*</span>
<span v-if="(column.rqd && !column.cdf) || required" class="error--text text--lighten-1">&nbsp;*</span>
<v-spacer />
<v-menu
v-if="!isLocked &&!isPublicView && _isUIAllowed('edit-column') && !isForm"
offset-y
open-on-hover
left
z-index="999"
>
<template #activator="{on}">
<v-icon v-if="!isLocked && !isVirtual" small v-on="on">
@ -59,7 +58,11 @@
</v-icon>
</template>
<v-list dense>
<v-list-item class="nc-column-edit" dense @click="editColumnMenu = true">
<v-list-item
class="nc-column-edit"
dense
@click="editColumnMenu = true"
>
<x-icon small class="mr-1" color="primary">
mdi-pencil
</x-icon>
@ -68,7 +71,11 @@
{{ $t('general.edit') }}
</span>
</v-list-item>
<v-list-item dense @click="setAsPrimaryValue">
<v-list-item
v-t="['column:set-as-primary']"
dense
@click="setAsPrimaryValue"
>
<x-icon small class="mr-1" color="primary">
mdi-key-star
</x-icon>
@ -82,7 +89,10 @@
<span class="caption font-weight-bold">Primary value will be shown in place of primary key</span>
</v-tooltip>
</v-list-item>
<v-list-item class="nc-column-delete" @click="columnDeleteDialog = true">
<v-list-item
class="nc-column-delete"
@click="columnDeleteDialog = true"
>
<x-icon small class="mr-1" color="error">
mdi-delete-outline
</x-icon>
@ -94,7 +104,7 @@
</v-list>
</v-menu>
<v-menu v-model="editColumnMenu" offset-y content-class="" left>
<v-menu v-model="editColumnMenu" z-index="999" offset-y content-class="" left>
<template #activator="{on}">
<span v-on="on" />
</template>
@ -126,17 +136,26 @@
<v-divider />
<v-card-text class="mt-4 title">
Do you want to delete <span class="font-weight-bold">'{{
column._cn
column.title
}}'</span> column ?
</v-card-text>
<v-divider />
<v-card-actions class="d-flex pa-4">
<v-spacer />
<v-btn small @click="columnDeleteDialog = false">
<v-btn
v-t="['column:delete:cancel']"
small
@click="columnDeleteDialog = false"
>
<!-- Cancel -->
{{ $t('general.cancel') }}
</v-btn>
<v-btn small color="error" @click="deleteColumn">
<v-btn
v-t="['column:delete']"
small
color="error"
@click="deleteColumn"
>
Confirm
</v-btn>
</v-card-actions>
@ -161,19 +180,13 @@ export default {
methods: {
async deleteColumn() {
try {
const column = { ...this.column, cno: this.column.cn }
const column = { ...this.column, cno: this.column.column_name }
column.altered = 4
const columns = this.meta.columns.slice()
columns[this.columnIndex] = column
await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableUpdate', {
tn: this.nodes.tn,
_tn: this.meta._tn,
originalColumns: this.meta.columns,
columns
}])
await this.$api.dbTableColumn.delete(column.id)
this.$emit('colDelete')
this.$emit('saved')
this.columnDeleteDialog = false
} catch (e) {
@ -183,23 +196,7 @@ export default {
async setAsPrimaryValue() {
// todo: pass only updated fields
try {
const meta = JSON.parse(JSON.stringify(this.meta))
for (const col of meta.columns) {
if (col.pv) {
delete col.pv
}
if (col.cn === this.column.cn) {
col.pv = true
}
}
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}])
await this.$api.dbTableColumn.primaryColumnSet(this.column.id)
this.$toast.success('Successfully updated as primary column').goAway(3000)
} catch (e) {
console.log(e)

60
packages/nc-gui/components/project/spreadsheet/components/importExport/columnMappingModal.vue

@ -3,14 +3,14 @@
<v-card>
<v-card-actions>
<v-card-title>
Table : {{ meta._tn }}
Table : {{ meta.title }}
</v-card-title>
<v-spacer />
<v-btn
:disabled="
!valid ||
(typeof requiredColumnValidationError === 'string' || requiredColumnValidationError) ||
(typeof noSelectedColumnError === 'string' || noSelectedColumnError)
!valid ||
(typeof requiredColumnValidationError === 'string' || requiredColumnValidationError) ||
(typeof noSelectedColumnError === 'string' || noSelectedColumnError)
"
color="primary"
large
@ -46,7 +46,7 @@
<tbody>
<tr v-for="(r,i) in mappings" :key="i">
<td>
<v-checkbox v-model="r.enabled" class="mt-0" dense hide-details @change="$refs.form.validate()"/>
<v-checkbox v-model="r.enabled" class="mt-0" dense hide-details @change="$refs.form.validate()" />
</td>
<td class="caption" style="width:45%">
<div :title="r.sourceCn" style="">
@ -60,8 +60,8 @@
dense
hide-details="auto"
:items="meta.columns"
item-text="_cn"
:item-value="v => v && v._cn"
item-text="title"
:item-value="v => v && v.title"
:rules="[
v => validateField(v,r)
]"
@ -71,13 +71,13 @@
<v-icon small class="mr-1">
{{ getIcon(item.uidt) }}
</v-icon>
{{ item._cn }}
{{ item.title }}
</template>
<template #item="{item}">
<v-icon small class="mr-1">
{{ getIcon(item.uidt) }}
</v-icon>
<span class="caption"> {{ item._cn }}</span>
<span class="caption"> {{ item.title }}</span>
</template>
</v-select>
</td>
@ -118,16 +118,16 @@ export default {
},
requiredColumnValidationError() {
const missingRequiredColumns = this.meta.columns.filter(c => (c.pk ? !c.ai && !c.cdf : !c.cdf && c.rqd) &&
!this.mappings.some(r => r.destCn === c._cn))
!this.mappings.some(r => r.destCn === c.title))
if (missingRequiredColumns.length) {
return `Following columns are required : ${missingRequiredColumns.map(c => c._cn).join(', ')}`
return `Following columns are required : ${missingRequiredColumns.map(c => c.title).join(', ')}`
}
return false
},
noSelectedColumnError() {
if ((this.mappings || []).filter(v => v.enabled === true).length == 0) {
return 'At least one column has to be selected'
if ((this.mappings || []).filter(v => v.enabled === true).length == 0) {
return 'At least one column has to be selected'
}
return false
}
@ -147,7 +147,7 @@ export default {
return true
}
const v = this.meta && this.meta.columns.find(c => c._cn === _cn)
const v = this.meta && this.meta.columns.find(c => c.title === _cn)
if ((this.mappings || []).filter(v => v.destCn === _cn).length > 1) { return 'Duplicate mapping found, please remove one of the mapping' }
@ -169,20 +169,20 @@ export default {
case UITypes.Checkbox:
if (
this.parsedCsv && this.parsedCsv.data && this.parsedCsv.data.slice(0, 500)
.some((r) => {
if (r => r[row.sourceCn] !== null && r[row.sourceCn] !== undefined) {
var input = r[row.sourceCn]
if (typeof input === 'string') {
input = input.replace(/["']/g, "").toLowerCase().trim()
return (
input == "false" || input == "no" || input == "n" || input == "0" ||
input == "true" || input == "yes" || input == "y" || input == "1"
) ? false : true
.some((r) => {
if (r => r[row.sourceCn] !== null && r[row.sourceCn] !== undefined) {
let input = r[row.sourceCn]
if (typeof input === 'string') {
input = input.replace(/["']/g, '').toLowerCase().trim()
return !((
input == 'false' || input == 'no' || input == 'n' || input == '0' ||
input == 'true' || input == 'yes' || input == 'y' || input == '1'
))
}
return input != 1 && input != 0 && input != true && input != false
}
return input != 1 && input != 0 && input != true && input != false
}
return false
})
return false
})
) {
return 'Source data contains some invalid boolean values'
}
@ -194,13 +194,13 @@ export default {
this.mappings = []
for (const col of this.importDataColumns) {
const o = { sourceCn: col, enabled: true }
const tableColumn = this.meta.columns.find(c => c._cn === col)
const tableColumn = this.meta.columns.find(c => c.title === col)
if (tableColumn) {
o.destCn = tableColumn._cn
o.destCn = tableColumn.title
}
this.mappings.push(o)
}
this.$nextTick(()=> this.$refs.form.validate())
this.$nextTick(() => this.$refs.form.validate())
},
getIcon(uidt) {
return getUIDTIcon(uidt) || 'mdi-alpha-v-circle-outline'

1
packages/nc-gui/components/project/spreadsheet/components/lockMenu.vue

@ -86,6 +86,7 @@ export default {
}),
methods: {
changeLockType(type) {
this.$tele.emit(`lockmenu:${type}`)
if (type === 'personal') {
return this.$toast.info('Coming soon').goAway(3000)
}

132
packages/nc-gui/components/project/spreadsheet/components/moreActions.vue

@ -7,6 +7,7 @@
>
<template #activator="{on}">
<v-btn
v-t="['actions:trigger']"
outlined
class="nc-actions-menu-btn caption px-2 nc-remove-border font-weight-medium"
small
@ -27,6 +28,7 @@
<v-list dense>
<v-list-item
v-t="['actions:download-csv']"
dense
@click="exportCsv"
>
@ -42,6 +44,7 @@
</v-list-item>
<v-list-item
v-if="_isUIAllowed('csvImport') && !isView"
v-t="['actions:upload-csv']"
dense
@click="importModal = true"
>
@ -61,6 +64,7 @@
</v-list-item>
<v-list-item
v-if="_isUIAllowed('csvImport') && !isView"
v-t="['actions:shared-view-list']"
dense
@click="$emit('showAdditionalFeatOverlay', 'shared-views')"
>
@ -73,8 +77,10 @@
{{ $t('activity.listSharedView') }}
</span>
</v-list-item-title>
</v-list-item> <v-list-item
</v-list-item>
<v-list-item
v-if="_isUIAllowed('csvImport') && !isView"
v-t="['actions:webhook:trigger']"
dense
@click="$emit('webhook')"
>
@ -104,6 +110,7 @@
<script>
import FileSaver from 'file-saver'
import { ExportTypes } from 'nocodb-sdk'
import DropOrSelectFileModal from '~/components/import/dropOrSelectFileModal'
import ColumnMappingModal from '~/components/project/spreadsheet/components/importExport/columnMappingModal'
import CSVTemplateAdapter from '~/components/import/templateParsers/CSVTemplateAdapter'
@ -111,14 +118,18 @@ import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes'
export default {
name: 'ExportImport',
components: { ColumnMappingModal, DropOrSelectFileModal },
components: {
ColumnMappingModal,
DropOrSelectFileModal
},
props: {
meta: Object,
nodes: Object,
selectedView: Object,
publicViewId: String,
queryParams: Object,
isView: Boolean
isView: Boolean,
reqPayload: Object
},
data() {
return {
@ -152,22 +163,22 @@ export default {
let prop, cn
if (col.mm || (col.lk && col.lk.type === 'mm')) {
const tn = col.mm ? col.mm.rtn : col.lk.ltn
const _tn = col.mm ? col.mm._rtn : col.lk._ltn
const title = col.mm ? col.mm._rtn : col.lk._ltn
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn
})
prop = `${_tn}MMList`
prop = `${title}MMList`
cn = col.lk
? col.lk._lcn
: (this.$store.state.meta.metas[tn].columns.find(c => c.pv) || this.$store.state.meta.metas[tn].columns.find(c => c.pk) || {})._cn
: (this.$store.state.meta.metas[tn].columns.find(c => c.pv) || this.$store.state.meta.metas[tn].columns.find(c => c.pk) || {}).title
row[col._cn] = r.row[prop] && r.row[prop].map(r => cn && r[cn])
row[col.title] = r.row[prop] && r.row[prop].map(r => cn && r[cn])
} else if (col.hm || (col.lk && col.lk.type === 'hm')) {
const tn = col.hm ? col.hm.tn : col.lk.ltn
const _tn = col.hm ? col.hm._tn : col.lk._ltn
const tn = col.hm ? col.hm.table_name : col.lk.ltn
const title = col.hm ? col.hm.title : col.lk._ltn
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
@ -175,89 +186,93 @@ export default {
tn
})
prop = `${_tn}List`
prop = `${title}List`
cn = col.lk
? col.lk._lcn
: (this.$store.state.meta.metas[tn].columns.find(c => c.pv) ||
this.$store.state.meta.metas[tn].columns.find(c => c.pk))._cn
row[col._cn] = r.row[prop] && r.row[prop].map(r => cn && r[cn])
this.$store.state.meta.metas[tn].columns.find(c => c.pk)).title
row[col.title] = r.row[prop] && r.row[prop].map(r => cn && r[cn])
} else if (col.bt || (col.lk && col.lk.type === 'bt')) {
const tn = col.bt ? col.bt.rtn : col.lk.ltn
const _tn = col.bt ? col.bt._rtn : col.lk._ltn
const title = col.bt ? col.bt._rtn : col.lk._ltn
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn
})
prop = `${_tn}Read`
prop = `${title}Read`
cn = col.lk
? col.lk._lcn
: (this.$store.state.meta.metas[tn].columns.find(c => c.pv) ||
this.$store.state.meta.metas[tn].columns.find(c => c.pk) || {})._cn
row[col._cn] = r.row[prop] &&
this.$store.state.meta.metas[tn].columns.find(c => c.pk) || {}).title
row[col.title] = r.row[prop] &&
r.row[prop] && cn && r.row[prop][cn]
} else {
row[col._cn] = r.row[col._cn]
row[col.title] = r.row[col.title]
}
} else if (col.uidt === 'Attachment') {
let data = []
try {
if (typeof r.row[col._cn] === 'string') {
data = JSON.parse(r.row[col._cn])
} else if (r.row[col._cn]) {
data = r.row[col._cn]
if (typeof r.row[col.title] === 'string') {
data = JSON.parse(r.row[col.title])
} else if (r.row[col.title]) {
data = r.row[col.title]
}
} catch {
}
row[col._cn] = (data || []).map(a => `${a.title}(${a.url})`)
row[col.title] = (data || []).map(a => `${a.title}(${a.url})`)
} else {
row[col._cn] = r.row[col._cn]
row[col.title] = r.row[col.title]
}
}
return row
}))
},
async exportCsv() {
// const fields = this.availableColumns.map(c => c._cn)
// const blob = new Blob([Papaparse.unparse(await this.extractCsvData())], { type: 'text/plain;charset=utf-8' })
let offset = 0
let c = 1
try {
while (!isNaN(offset) && offset > -1) {
const res = await this.$store.dispatch('sqlMgr/ActSqlOp', [
this.publicViewId
? null
: {
dbAlias: this.nodes.dbAlias,
env: '_noco'
},
this.publicViewId ? 'sharedViewExportAsCsv' : 'xcExportAsCsv',
{
query: { offset },
localQuery: this.queryParams || {},
...(this.publicViewId
? {
view_id: this.publicViewId
}
: {
view_name: this.selectedView.title,
model_name: this.meta.tn
})
},
null,
{
responseType: 'blob'
},
null,
true
])
const data = res.data
let res
if (this.publicViewId) {
res = await this.$api.public.csvExport(this.publicViewId, ExportTypes.CSV, {
responseType: 'blob',
query: {
fields: this.queryParams && this.queryParams.fieldsOrder && this.queryParams.fieldsOrder.filter(c => this.queryParams.showFields[c]),
offset,
sortArrJson: JSON.stringify(this.reqPayload && this.reqPayload.sorts && this.reqPayload.sorts.map(({
fk_column_id,
direction
}) => ({
direction,
fk_column_id
}))),
filterArrJson: JSON.stringify(this.reqPayload && this.reqPayload.filters)
},
headers: {
'xc-password': this.reqPayload && this.reqPayload.password
}
})
} else {
res = await this.$api.dbViewRow.export(
'noco',
this.projectName,
this.meta.title,
this.selectedView.title,
ExportTypes.CSV, {
responseType: 'blob',
query: {
offset
}
})
}
const { data } = res
offset = +res.headers['nc-export-offset']
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
FileSaver.saveAs(blob, `${this.meta._tn}_exported_${c++}.csv`)
FileSaver.saveAs(blob, `${this.meta.title}_exported_${c++}.csv`)
if (offset > -1) {
this.$toast.info('Downloading more files').goAway(3000)
} else {
@ -265,13 +280,14 @@ export default {
}
}
} catch (e) {
console.log(e)
this.$toast.error(e.message).goAway(3000)
}
},
async importData(columnMappings) {
try {
const api = this.$ncApis.get({
table: this.meta.tn
table: this.meta.table_name
})
const data = this.parsedCsv.data
@ -279,7 +295,7 @@ export default {
const batchData = data.slice(i, i + 500).map(row => columnMappings.reduce((res, col) => {
// todo: parse data
if (col.enabled && col.destCn) {
const v = this.meta && this.meta.columns.find(c => c._cn === col.destCn)
const v = this.meta && this.meta.columns.find(c => c.title === col.destCn)
let input = row[col.sourceCn]
// parse potential boolean values
if (v.uidt == UITypes.Checkbox) {
@ -290,7 +306,7 @@ export default {
input = '1'
}
} else if (v.uidt === UITypes.Number) {
if (input == "") input = null
if (input == '') { input = null }
}
res[col.destCn] = input
}

3
packages/nc-gui/components/project/spreadsheet/components/pagination.vue

@ -55,7 +55,8 @@ export default {
this.page = v
},
count(c) {
this.$emit('input', Math.max(1, Math.min(this.page, Math.ceil(c / this.size))))
const page = Math.max(1, Math.min(this.page, Math.ceil(c / this.size)))
if (this.value !== page) { this.$emit('input', page) }
}
},
mounted() {

38
packages/nc-gui/components/project/spreadsheet/components/shareViewMenu.vue

@ -1,12 +1,7 @@
<template>
<div>
<!-- <v-menu
open-on-hover
bottom
offset-y
>
<template #activator="{on}">-->
<v-btn
v-t="['share-view:trigger']"
v-if="_isUIAllowed('add-user')"
outlined
class="nc-btn-share-view caption px-2 nc-remove-border font-weight-medium"
@ -20,37 +15,6 @@
<!-- Share View -->
{{ $t('activity.shareView') }}
</v-btn>
<!-- </template>
<v-list dense>
<v-list-item
dense
>
<v-list-item-title>
<v-icon small class="mr-1">
mdi-open-in-new
</v-icon>
<span class="caption">
Share View
</span>
</v-list-item-title>
</v-list-item>
<v-list dense>
<v-list-item
dense
>
<v-list-item-title>
<v-icon small class="mr-1">
mdi-download-outline
</v-icon>
<span class="caption">
Shared Views List
</span>
</v-list-item-title>
</v-list-item>
</v-list>
</v-list>
</v-menu>-->
</div>
</template>

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

Loading…
Cancel
Save