Browse Source

wip: towards v1

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/1620/head
Pranav C 3 years ago
parent
commit
8733948056
  1. 3
      .gitignore
  2. 42
      Run.md
  3. 15116
      package-lock.json
  4. 7
      package.json
  5. 34
      packages/nc-common/.eslintrc.json
  6. 9587
      packages/nc-common/package-lock.json
  7. 2
      packages/nc-common/src/index.ts
  8. 5
      packages/nc-gui/.eslintrc.json
  9. 918
      packages/nc-gui/components/ProjectTreeView.vue
  10. 1170
      packages/nc-gui/components/ProjectTreeViewOld.vue
  11. 4
      packages/nc-gui/components/apiOverlay.vue
  12. 46
      packages/nc-gui/components/auth/apiTokens.vue
  13. 1
      packages/nc-gui/components/auth/authHooks.vue
  14. 2
      packages/nc-gui/components/auth/roles.vue
  15. 285
      packages/nc-gui/components/auth/shareOrInviteModal.vue
  16. 221
      packages/nc-gui/components/auth/userManagement.vue
  17. 15
      packages/nc-gui/components/authTab.vue
  18. 59
      packages/nc-gui/components/base/shareBase.vue
  19. 549
      packages/nc-gui/components/createOrEditProject.vue
  20. 15
      packages/nc-gui/components/createProjectComingSoon.vue
  21. 5
      packages/nc-gui/components/environment.vue
  22. 46
      packages/nc-gui/components/githubStarBtn.vue
  23. 62
      packages/nc-gui/components/globalAcl.vue
  24. 2
      packages/nc-gui/components/import/dropOrSelectFile.vue
  25. 3
      packages/nc-gui/components/import/excelImport.vue
  26. 24
      packages/nc-gui/components/import/templateParsers/ExcelTemplateAdapter.js
  27. 8
      packages/nc-gui/components/importantAnnouncement.vue
  28. 2
      packages/nc-gui/components/loader.vue
  29. 2
      packages/nc-gui/components/monaco/Monaco.vue
  30. 3
      packages/nc-gui/components/monaco/MonacoSingleLineEditor.js
  31. 8
      packages/nc-gui/components/monaco/MonacoSqlEditor.vue
  32. 167
      packages/nc-gui/components/previewAs.vue
  33. 11
      packages/nc-gui/components/project/apiClientSwagger.vue
  34. 2
      packages/nc-gui/components/project/apis.vue
  35. 300
      packages/nc-gui/components/project/appStore.vue
  36. 34
      packages/nc-gui/components/project/appStore/appInstall.vue
  37. 6
      packages/nc-gui/components/project/auditTab.vue
  38. 22
      packages/nc-gui/components/project/auditTab/audit.vue
  39. 33
      packages/nc-gui/components/project/auditTab/db.vue
  40. 6
      packages/nc-gui/components/project/cronJobs.vue
  41. 28
      packages/nc-gui/components/project/dlgs/dlgAddRelation.vue
  42. 2
      packages/nc-gui/components/project/dlgs/dlgTriggerAddEdit.vue
  43. 3
      packages/nc-gui/components/project/functionTab/functionQuery.vue
  44. 4
      packages/nc-gui/components/project/gqlHandlerCodeEditor.vue
  45. 4
      packages/nc-gui/components/project/grpcHandlerCodeEditor.vue
  46. 102
      packages/nc-gui/components/project/projectMetadata/disableOrEnableModels.vue
  47. 4
      packages/nc-gui/components/project/projectMetadata/sync/disableOrEnableRelations.vue
  48. 47
      packages/nc-gui/components/project/projectMetadata/sync/metaDiffSync.vue
  49. 4
      packages/nc-gui/components/project/projectMetadata/uiAcl/toggleRelationsUIAcl.vue
  50. 66
      packages/nc-gui/components/project/projectMetadata/uiAcl/toggleTableUIAcl.vue
  51. 16
      packages/nc-gui/components/project/restHandlerCodeEditor.vue
  52. 13
      packages/nc-gui/components/project/sequence.vue
  53. 65
      packages/nc-gui/components/project/settings/xcMeta.vue
  54. 29
      packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js
  55. 2
      packages/nc-gui/components/project/spreadsheet/apis/grpcApi.js
  56. 6
      packages/nc-gui/components/project/spreadsheet/apis/restApi.js
  57. 501
      packages/nc-gui/components/project/spreadsheet/components/columnFilter.vue
  58. 28
      packages/nc-gui/components/project/spreadsheet/components/columnFilterMenu.vue
  59. 115
      packages/nc-gui/components/project/spreadsheet/components/editColumn.vue
  60. 88
      packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue
  61. 170
      packages/nc-gui/components/project/spreadsheet/components/editColumn/linkedToAnotherOptions.vue
  62. 116
      packages/nc-gui/components/project/spreadsheet/components/editColumn/lookupOptions.vue
  63. 25
      packages/nc-gui/components/project/spreadsheet/components/editColumn/relationOptions.vue
  64. 113
      packages/nc-gui/components/project/spreadsheet/components/editColumn/rollupOptions.vue
  65. 62
      packages/nc-gui/components/project/spreadsheet/components/editVirtualColumn.vue
  66. 7
      packages/nc-gui/components/project/spreadsheet/components/editable.vue
  67. 4
      packages/nc-gui/components/project/spreadsheet/components/editableCell.vue
  68. 9
      packages/nc-gui/components/project/spreadsheet/components/editableCell/booleanCell.vue
  69. 8
      packages/nc-gui/components/project/spreadsheet/components/editableCell/dateTimePickerCell.vue
  70. 110
      packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue
  71. 1
      packages/nc-gui/components/project/spreadsheet/components/editableCell/editableUrlCell.vue
  72. 1
      packages/nc-gui/components/project/spreadsheet/components/editableCell/enumListEditableCell.vue
  73. 2
      packages/nc-gui/components/project/spreadsheet/components/editableCell/jsonEditableCell.vue
  74. 5
      packages/nc-gui/components/project/spreadsheet/components/editableCell/setListEditableCell.vue
  75. 2
      packages/nc-gui/components/project/spreadsheet/components/editableCell/textCell.vue
  76. 1
      packages/nc-gui/components/project/spreadsheet/components/editableCell/timePickerCell.vue
  77. 215
      packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue
  78. 31
      packages/nc-gui/components/project/spreadsheet/components/extras.vue
  79. 162
      packages/nc-gui/components/project/spreadsheet/components/fieldsMenu.vue
  80. 1
      packages/nc-gui/components/project/spreadsheet/components/fieldsMenuItem.vue
  81. 71
      packages/nc-gui/components/project/spreadsheet/components/headerCell.vue
  82. 60
      packages/nc-gui/components/project/spreadsheet/components/importExport/columnMappingModal.vue
  83. 1
      packages/nc-gui/components/project/spreadsheet/components/lockMenu.vue
  84. 113
      packages/nc-gui/components/project/spreadsheet/components/moreActions.vue
  85. 3
      packages/nc-gui/components/project/spreadsheet/components/pagination.vue
  86. 38
      packages/nc-gui/components/project/spreadsheet/components/shareViewMenu.vue
  87. 55
      packages/nc-gui/components/project/spreadsheet/components/sharedViewsList.vue
  88. 100
      packages/nc-gui/components/project/spreadsheet/components/sortListMenu.vue
  89. 348
      packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue
  90. 22
      packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue
  91. 124
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/belongsToCell.vue
  92. 1
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/itemChip.vue
  93. 82
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listChildItems.vue
  94. 76
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listItems.vue
  95. 39
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/formulaCell.vue
  96. 127
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  97. 217
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/lookupCell.vue
  98. 144
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue
  99. 2
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/rollupCell.vue
  100. 158
      packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue
  101. Some files were not shown because too many files have changed in this diff Show More

3
.gitignore vendored

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

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",

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>

549
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">
@ -163,52 +88,11 @@
: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 +101,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 +124,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 +199,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 +285,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 +378,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 +505,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 +523,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="
@ -776,157 +632,10 @@
</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 +650,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 +708,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 +766,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 +851,8 @@ export default {
graphqlDepthLimit: 10
},
inflection: {
tn: ['camelize'],
cn: ['camelize']
table_name: 'camelize',
column_name: 'camelize'
}
},
ui: {
@ -1374,7 +1110,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() {
@ -1416,18 +1155,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 +1182,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 +1217,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 +1285,11 @@ 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 +1370,48 @@ 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)
}
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 +1428,8 @@ export default {
tn: 'nc_evolutions',
dbAlias: 'db',
inflection: {
tn: 'camelize',
cn: 'camelize'
table_name: 'camelize',
column_name: 'camelize'
},
api: {
type: ''
@ -1734,7 +1451,6 @@ export default {
}
},
mtdDialogGetEnvNameCancel() {
console.log('mtdDialogGetTableNameCancel cancelled')
this.dialogGetEnvName.dialogShow = false
},
@ -1761,8 +1477,8 @@ export default {
tn: 'nc_evolutions',
dbAlias,
inflection: {
tn: 'camelize',
cn: 'camelize'
table_name: 'camelize',
column_name: 'camelize'
},
api: {
type: ''
@ -1786,24 +1502,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 +1538,6 @@ export default {
}
},
async newTestConnection(db, env, panelIndex) {
console.log(this.project.envs[env][0])
if (
db.connection.host === 'localhost' &&
!this.edit &&
@ -1869,7 +1568,6 @@ export default {
'testConnection',
c1
])
console.log('test connection result', result)
if (result.code === 0) {
db.ui.setup = 1
@ -1884,10 +1582,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 +1633,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 +1669,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 +1684,6 @@ export default {
this.dialog.type = 'error'
this.dialog.show = true
}
console.log('testconnection params : after', db)
}
}
} catch (e) {
@ -2038,7 +1710,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 +1739,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 +1754,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 +1765,10 @@ export default {
Vue.set(this.project, 'envs', { ...this.project.envs })
}
},
fetch({ store, params }) {
fetch({
store,
params
}) {
},
beforeCreated() {
},
@ -2115,12 +1788,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 +1835,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 +1879,10 @@ 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()
}

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)

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

@ -32,14 +32,6 @@
API Changes in v0.90.0
</span>
</v-list-item>
<!-- <v-list-item dense href="#" target="_blank">
<v-icon small class="mr-2">
mdi-rocket-launch-outline
</v-icon>
<span class="caption">
Migration Guide
</span>
</v-list-item> -->
<v-list-item @click="announcementAlert = false">
<v-icon small class="mr-2">
mdi-close

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 {

300
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>
@ -218,26 +224,28 @@ export default {
},
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 +253,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 +270,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 }
}

501
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,161 @@ 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(this.hookId || hookId, filter.id)
} else {
await this.$api.dbTableFilter.delete(this.viewId, filter.id)
}
} else if (filter.status === 'update') {
if (filter.id) {
if (this.hookId || hookId) {
await this.$api.dbTableFilter.update(this.hookId || hookId, filter.id, {
...filter,
fk_parent_id: this.parentId
})
} else {
await this.$api.dbTableFilter.update(this.viewId, filter.id, {
...filter,
fk_parent_id: this.parentId
})
}
} else if (this.hookId || hookId) {
this.$set(this.filters, i, (await this.$api.dbTableFilter.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.viewId, this.parentId))
: (await this.$api.dbTableFilter.read(this.viewId))
}
if (this.hookId && this._isUIAllowed('filterSync')) {
filters = this.parentId
? (await this.$api.dbTableWebhookFilter.childrenRead(this.hookId, 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(this.viewId, 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('input', this.filters.filter(f => f.fk_column_id && f.comparison_op))
this.$emit('updated')
} else if (filter.id) {
if (!this.autoApply) {
this.$set(filter, 'status', 'delete')
} else {
await this.$api.dbTableFilter.delete(this.viewId, filter.id)
await this.loadFilter()
this.$emit('updated')
}
} else {
this.filters.splice(i, 1)
this.$emit('updated')
}
// this.$emit('input', this.filters.filter(f => f.fk_column_id && f.comparison_op))
this.$tele.emit('filter:delete')
}
}
}
@ -205,7 +448,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.meta.id, 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) {

170
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"
@ -54,21 +53,6 @@
<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>
<template v-if="!isSQLite">
<v-row>
@ -121,6 +105,8 @@
</template>
<script>
import { ModelTypes, UITypes } from 'nocodb-sdk'
export default {
name: 'LinkedToAnotherOptions',
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias'],
@ -147,159 +133,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'
relationType: 'real'
}
},
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.meta.id, 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'))
}
}
},

110
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,13 @@ 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.dbView.upload(this.$store.state.project.projectId, this.viewId, {
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')) }
}

215
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,7 @@ export default {
}, {})
if (this.isNew) {
const data = await this.api.insert(updatedObj)
const data = (await this.$api.data.create(this.viewId || this.meta.id, updatedObj))
this.localState = { ...this.localState, ...data }
// save hasmany and manytomany relations from local state
@ -431,7 +447,18 @@ 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.data.update(this.viewId || this.meta.id, id, updatedObj)
for (const key of Object.keys(updatedObj)) {
// audit
this.$api.utils.auditRowUpdate({
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 +474,25 @@ 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.data.read(this.viewId || this.meta.id, 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 +500,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 +666,8 @@ h5 {
background: var(--v-backgroundColorDefault-base);
}
.nc-chip{
padding:8px;
.nc-chip {
padding: 8px;
border-radius: 8px;
}
</style>

31
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,12 +21,12 @@
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">
@ -43,36 +35,33 @@
<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 />
<v-list-item dense href="https://twitter.com/NocoDB" target="_blank">
<!-- 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')">{{
<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-divider />
<v-list-item
dense
target="_blank"
@ -83,19 +72,13 @@
<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 +90,7 @@ import colors from '~/mixins/colors'
export default {
name: 'Extras',
components: { ShareIcons, SponsorMini },
components: { ShareIcons },
mixins: [colors],
data: () => ({
showCommunity: true

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({ viewId: 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(this.meta.id, 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.meta.id, 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)
}

113
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,76 @@ 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, this.reqPayload, {
responseType: 'blob',
query: {
offset
}
})
} else {
res = await this.$api.data.csvExport(this.selectedView.id, 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 +263,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 +278,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) {

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>

55
packages/nc-gui/components/project/spreadsheet/components/sharedViewsList.vue

@ -27,11 +27,11 @@
<tbody>
<tr v-if="currentView">
<td class="font-weight-bold caption text-left">
<v-icon v-if="viewIcons[currentView.view_type]" small :color="viewIcons[currentView.view_type].color">
{{ viewIcons[currentView.view_type].icon }}
<v-icon v-if="viewIcons[currentView.type]" small :color="viewIcons[currentView.type].color">
{{ viewIcons[currentView.type].icon }}
</v-icon>
{{ currentView.view_name }}
{{ currentView.title }}
</td>
<td class="caption text-left">
<nuxt-link :to="sharedViewUrl(currentView)">
@ -63,13 +63,13 @@
</td>
</tr>
<template v-if="allSharedLinks">
<tr v-for="link of viewsList" :key="link.id">
<tr v-for="link of viewList" :key="link.id">
<td class="caption text-left">
<v-icon v-if="viewIcons[link.view_type]" small :color="viewIcons[link.view_type].color">
{{ viewIcons[link.view_type].icon }}
<v-icon v-if="viewIcons[link.type]" small :color="viewIcons[link.type].color">
{{ viewIcons[link.type].icon }}
</v-icon>
{{ link.view_name }}
{{ link.title }}
</td>
<td class="caption text-left">
<nuxt-link :to="sharedViewUrl(link)">
@ -117,13 +117,15 @@
</template>
<script>
import { ViewTypes } from 'nocodb-sdk'
import viewIcons from '~/helpers/viewIcons'
import { copyTextToClipboard } from '~/helpers/xutils'
export default {
name: 'SharedViewsList',
props: ['modelName', 'nodes', 'selectedView'],
props: ['modelName', 'nodes', 'selectedView', 'meta'],
data: () => ({
viewsList: null,
viewList: null,
currentView: null,
viewIcons,
allSharedLinks: false
@ -138,36 +140,31 @@ export default {
},
methods: {
copyLink(view) {
this.$clipboard(`${this.dashboardUrl}#${this.sharedViewUrl(view)}`)
copyTextToClipboard(`${this.dashboardUrl}#${this.sharedViewUrl(view)}`)
this.$toast.info('Copied to clipboard').goAway(1000)
},
async loadSharedViewsList() {
const viewsList = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: this.nodes.dbAlias }, 'listSharedViewLinks', {
model_name: this.modelName
}])
// const viewList = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: this.nodes.dbAlias }, 'listSharedViewLinks', {
// model_name: this.modelName
// }])
const index = viewsList.findIndex((v) => {
if (this.selectedView) {
// if current view is main view compare with model name
return (['table', 'view'].includes(this.selectedView.type) ? this.modelName : this.selectedView.title) === v.view_name
} else {
return (v.view_name || '').toLowerCase() === (this.$route.query.view || '').toLowerCase()
}
const viewList = (await this.$api.dbViewShare.list(this.meta.id))
const index = viewList.findIndex((v) => {
return this.selectedView && this.selectedView.id === v.id
})
if (index > -1) {
this.currentView = viewsList.splice(index, 1)[0]
this.currentView = viewList.splice(index, 1)[0]
} else {
this.currentView = null
}
this.viewsList = viewsList
this.viewList = viewList
},
async deleteLink(id) {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: this.nodes.dbAlias }, 'deleteSharedViewLink', {
id
}])
await this.$api.dbViewShare.delete(id)
this.$toast.success('Deleted shared view successfully').goAway(3000)
await this.loadSharedViewsList()
} catch (e) {
@ -176,17 +173,17 @@ export default {
},
sharedViewUrl(view) {
let viewType
switch (view.view_type) {
case 'form':
switch (view.type) {
case ViewTypes.FORM:
viewType = 'form'
break
case 'kanban':
case ViewTypes.KANBAN:
viewType = 'kanban'
break
default:
viewType = 'view'
}
return `/nc/${viewType}/${view.view_id}`
return `/nc/${viewType}/${view.uuid}`
}
}
}

100
packages/nc-gui/components/project/spreadsheet/components/sortListMenu.vue

@ -2,18 +2,19 @@
<v-menu offset-y>
<template #activator="{ on }">
<v-badge
:value="sortList.length"
:value="sortList && sortList.length"
color="primary"
dot
overlap
>
<v-btn
v-t="['sort:trigger']"
class="nc-sort-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
small
text
outlined
:class=" { 'primary lighten-5 grey--text text--darken-3' : sortList.length}"
:class=" { 'primary lighten-5 grey--text text--darken-3' : sortList && sortList.length}"
v-on="on"
>
<v-icon small class="mr-1" color="#777">
@ -29,38 +30,46 @@
</template>
<div class="backgroundColor pa-2" style="min-width: 330px">
<div class="sort-grid" @click.stop>
<template v-for="(sort,i) in sortList" dense>
<v-icon :key="i + 'icon'" class="nc-sort-item-remove-btn" small @click.stop="sortList.splice(i,1)">
<template v-for="(sort,i) in sortList||[]" dense>
<v-icon :key="i + 'icon'" class="nc-sort-item-remove-btn" small @click.stop="deleteSort(sort)">
mdi-close-box
</v-icon>
<v-select
:key="i + 'sel1'"
v-model="sort.field"
v-model="sort.fk_column_id"
class="caption nc-sort-field-select"
:items="fieldList"
:items="columns"
item-value="id"
item-text="title"
:label="$t('objects.field')"
solo
flat
dense
hide-details
@click.stop
@change="saveOrUpdate(sort, i)"
>
<template #item="{item}">
<span class="caption font-weight-regular">{{ item }}</span>
<span
:class="`caption font-weight-regular nc-sort-fld-${item.title}`"
>
{{ item.title }}
</span>
</template>
</v-select>
<v-select
:key="i + 'sel2'"
v-model="sort.order"
v-model="sort.direction"
class="flex-shrink-1 flex-grow-0 caption nc-sort-dir-select"
:items="[{text : 'A -> Z', value: ''},{text : 'Z -> A', value: '-'}]"
:items="[{text : 'A -> Z', value: 'asc'},{text : 'Z -> A', value: 'desc'}]"
:label="$t('labels.operation')"
solo
flat
dense
hide-details
@click.stop
@change="saveOrUpdate(sort, i)"
>
<template #item="{item}">
<span class="caption font-weight-regular">{{ item.text }}</span>
@ -80,33 +89,86 @@
</template>
<script>
import { RelationTypes, UITypes } from 'nocodb-sdk'
export default {
name: 'SortListMenu',
props: ['fieldList', 'value', 'isLocked'],
props: {
fieldList: Array,
value: [Array, Object],
isLocked: Boolean,
meta: [Object],
viewId: String,
shared: Boolean
},
data: () => ({
sortList: []
}),
computed: {
columns() {
if (!this.meta || !this.meta.columns) { return [] }
return this.meta.columns.filter(c => !(c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== RelationTypes.BELONGS_TO))
}
},
watch: {
sortList: {
handler(v) {
this.$emit('input', v)
},
deep: true
},
value(v) {
this.sortList = v || []
},
async viewId(v) {
if (v) {
await this.loadSortList()
}
}
},
created() {
async created() {
this.sortList = this.value || []
this.loadSortList()
},
methods: {
addSort() {
this.sortList.push({
field: '',
order: ''
fk_column_id: null,
direction: 'asc'
})
this.sortList = this.sortList.slice()
this.$tele.emit(`sort:add:${this.sortList.length}`)
},
async loadSortList() {
if (!this.shared) { // && !this._isUIAllowed('sortSync')) {
let sortList = []
if (this.viewId) {
const data = await this.$api.dbTableSort.list(this.viewId)
sortList = data.sorts.list
}
this.sortList = sortList
}
},
async saveOrUpdate(sort, i) {
if (!this.shared && this._isUIAllowed('sortSync')) {
if (sort.id) {
await this.$api.dbTableSort.update(this.viewId, sort.id, sort)
} else {
this.$set(this.sortList, i, (await this.$api.dbTableSort.create(this.viewId, sort)))
}
} else {
this.$emit('input', this.sortList)
}
this.$emit('updated')
this.$tele.emit(`sort:dir:${sort.direction}`)
},
async deleteSort(sort, i) {
if (!this.shared && sort.id && this._isUIAllowed('sortSync')) {
await this.$api.dbTableSort.delete(this.viewId, sort.id)
await this.loadSortList()
} else {
this.sortList.splice(i, 1)
this.$emit('input', this.sortList)
}
this.$emit('updated')
this.$tele.emit('sort:delete')
}
}
}

348
packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue

@ -10,7 +10,7 @@
<v-container fluid class="h-100 py-0">
<div class="d-flex flex-column h-100">
<div class="flex-grow-1" style="overflow: auto; min-height: 350px">
<v-list v-if="viewsList && viewsList.length" dense>
<v-list v-if="views && views.length" dense>
<v-list-item dense>
<!-- Views -->
<span class="body-2 font-weight-medium">{{ $t('objects.views') }}</span>
@ -27,12 +27,12 @@
<v-list-item
v-for="(view, i) in viewsList"
:key="view.id"
v-t="['view:open']"
dense
:value="view.id"
active-class="x-active--text"
class="body-2 view nc-view-item nc-draggable-child"
:class="`nc-${view.show_as}-view-item`"
@click="$emit('generateNewViewKey')"
:class="`body-2 view nc-view-item nc-draggable-child nc-${viewTypeAlias[view.type]}-view-item`"
@click="$emit('rerender')"
>
<v-icon
v-if="_isUIAllowed('viewlist-drag-n-drop')"
@ -44,11 +44,11 @@
</v-icon>
<v-list-item-icon class="mr-n1">
<v-icon
v-if="viewIcons[view.show_as]"
v-if="viewIcons[view.type]"
x-small
:color="viewIcons[view.show_as].color"
:color="viewIcons[view.type].color"
>
{{ viewIcons[view.show_as].icon }}
{{ viewIcons[view.type].icon }}
</v-icon>
<v-icon v-else color="primary" small>
mdi-table
@ -83,7 +83,7 @@
<template v-if="_isUIAllowed('virtualViewsCreateOrEdit')">
<!-- Copy view -->
<x-icon
v-if="view.type === 'vtable' && !view.edit"
v-if="!view.edit"
:tooltip="$t('activity.copyView')"
x-small
color="primary"
@ -94,7 +94,7 @@
</x-icon>
<!-- Rename view -->
<x-icon
v-if="view.type === 'vtable' && !view.edit"
v-if="!view.edit"
:tooltip="$t('activity.renameView')"
x-small
color="primary"
@ -105,7 +105,7 @@
</x-icon>
<!-- Delete view" -->
<x-icon
v-if="view.type === 'vtable'"
v-if="!view.is_default"
:tooltip="$t('activity.deleteView')"
small
color="error"
@ -162,7 +162,7 @@
</v-list-item>
<v-tooltip bottom>
<template #activator="{ on }">
<v-list-item dense class="body-2 nc-create-grid-view" v-on="on" @click="openCreateViewDlg('grid')">
<v-list-item dense class="body-2 nc-create-grid-view" v-on="on" @click="openCreateViewDlg(viewTypes.GRID)">
<v-list-item-icon class="mr-n1">
<v-icon color="blue" x-small>
mdi-grid-large
@ -189,7 +189,7 @@
dense
class="body-2 nc-create-gallery-view"
v-on="on"
@click="openCreateViewDlg('gallery')"
@click="openCreateViewDlg(viewTypes.GALLERY)"
>
<v-list-item-icon class="mr-n1">
<v-icon color="orange" x-small>
@ -212,75 +212,20 @@
<!-- Add Gallery View -->
{{ $t('msg.info.addView.gallery') }}
</v-tooltip>
<!-- <v-tooltip bottom>
<template #activator="{ on }">
<v-list-item
dense
class="body-2"
v-on="on"
@click="enableDummyFeat ? openCreateViewDlg('calendar') : comingSoon()"
>
<v-list-item-icon class="mr-n1">
<v-icon x-small>
mdi-calendar
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<span class="font-weight-regular">
&lt;!&ndash; Calendar &ndash;&gt;
{{ $t('objects.viewType.calendar') }}
</span>
</v-list-item-title>
<v-spacer />
<v-icon class="mr-1" small>
mdi-plus
</v-icon>
</v-list-item>
</template>
&lt;!&ndash; Add Calendar View &ndash;&gt;
{{ $t('msg.info.addView.calendar') }}
</v-tooltip> -->
<!-- <v-tooltip bottom>
<template #activator="{ on }">
<v-list-item
dense
open-class="body-2"
v-on="on"
@click="openCreateViewDlg('kanban')"
>
<v-list-item-icon class="mr-n1">
<v-icon x-small>
mdi-tablet-dashboard
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<span class="font-weight-regular">
&lt;!&ndash; Kanban &ndash;&gt;
{{ $t('objects.viewType.kanban') }}
</span>
</v-list-item-title>
<v-spacer />
<v-icon class="mr-1" small>
mdi-plus
</v-icon>
</v-list-item>
</template>
Add Kanban View
{{ $t('msg.info.addView.kanban') }}
</v-tooltip>-->
<v-tooltip
bottom
>
<template #activator="{ on }">
<v-list-item
v-if="!isView"
dense
class="body-2 nc-create-form-view"
v-on="on"
@click="openCreateViewDlg('form')"
@click="openCreateViewDlg(viewTypes.FORM)"
>
<v-list-item-icon class="mr-n1">
<v-icon x-small :color="viewIcons['form'].color" class="mt-n1">
<v-icon x-small :color="viewIcons[viewTypes.FORM].color" class="mt-n1">
mdi-form-select
</v-icon>
</v-list-item-icon>
@ -317,7 +262,8 @@
mdi-close-circle-outline
</v-icon>
<extras />
<!-- <extras />-->
<sponsor-mini nav />
</div>
<!--<div class="text-center">
<v-hover >
@ -435,12 +381,13 @@
:nodes="nodes"
:table="table"
:show_as="createViewType"
:views-count="viewsList.length"
:views-count="views.length"
:primary-value-column="primaryValueColumn"
:meta="meta"
:copy-view="copyViewRef"
:alias="meta._tn"
:views-list="viewsList"
:alias="meta.title"
:views-list="views"
:selected-view-id="selectedViewId"
@created="onViewCreate"
/>
@ -458,6 +405,7 @@
{{ sharedViewUrl }}
<v-spacer />
<a
v-t="['share-view:open-url']"
:href="`${sharedViewUrl}`"
style="text-decoration: none"
target="_blank"
@ -519,16 +467,20 @@
<script>
import draggable from 'vuedraggable'
import { ViewTypes } from 'nocodb-sdk'
import CreateViewDialog from '@/components/project/spreadsheet/dialog/createViewDialog'
import Extras from '~/components/project/spreadsheet/components/extras'
import viewIcons from '~/helpers/viewIcons'
import { copyTextToClipboard } from '~/helpers/xutils'
import SponsorMini from '~/components/sponsorMini'
export default {
name: 'SpreadsheetNavDrawer',
components: { Extras, CreateViewDialog, draggable },
components: { SponsorMini, Extras, CreateViewDialog, draggable },
props: {
extraViewParams: Object,
showAdvanceOptions: Boolean,
isView: Boolean,
hideViews: Boolean,
primaryValueColumn: [Number, String],
toggleDrawer: {
@ -552,10 +504,11 @@ export default {
currentApiUrl: String,
fieldsOrder: Array,
viewStatus: Object,
columnsWidth: Object,
coverImageField: String,
// columnsWidth: Object,
// coverImageField: String,
groupingField: String,
showSystemFields: Boolean
// showSystemFields: Boolean,
views: Array
},
data: () => ({
drag: false,
@ -574,15 +527,26 @@ export default {
sharedViewPassword: '',
overAdvShieldIcon: false,
overShieldIcon: false,
viewsList: [],
viewIcons,
copyViewRef: null,
shareLink: {},
showShareModel: false,
showCreateView: false,
loading: false
loading: false,
viewTypeAlias: { [ViewTypes.GRID]: 'grid', [ViewTypes.FORM]: 'form', [ViewTypes.GALLERY]: 'gallery' }
}),
computed: {
viewsList: {
set(v) {
this.$emit('update:views', v)
},
get() {
return this.views
}
},
viewTypes() {
return ViewTypes
},
newViewParams() {
if (!this.showFields) {
return {}
@ -595,37 +559,37 @@ export default {
},
selectedViewIdLocal: {
set(val) {
const view = (this.viewsList || []).find(v => v.id === val)
const view = (this.views || []).find(v => v.id === val)
this.$router.push({
query: {
...this.$route.query,
view: view && (view.alias || view.title)
view: view && (view.id)
}
})
},
get() {
let id
if (this.viewsList) {
console.log(this.viewsList)
const view = this.viewsList.find(v => (v.alias ? v.alias : v.title) === this.$route.query.view)
id = (view && view.id) || ((this.viewsList && this.viewsList[0]) || {}).id
if (this.views) {
const view = this.views.find(v => v.id === this.$route.query.view)
id = (view && view.id) || ((this.views && this.views[0]) || {}).id
}
return id
}
},
sharedViewUrl() {
let viewType
switch (this.shareLink.view_type) {
case 'form':
switch (this.shareLink.type) {
case this.viewTypes.FORM:
viewType = 'form'
break
case 'kanban':
case this.viewTypes.KANBAN:
viewType = 'kanban'
break
default:
viewType = 'view'
}
return `${this.dashboardUrl}#/nc/${viewType}/${this.shareLink.view_id}`
return `${this.dashboardUrl}#/nc/${viewType}/${this.shareLink.uuid}`
}
},
watch: {
@ -648,47 +612,46 @@ export default {
methods: {
async onMove(event) {
if (this.viewsList.length - 1 === event.moved.newIndex) {
this.$set(this.viewsList[event.moved.newIndex], 'view_order', this.viewsList[event.moved.newIndex - 1].view_order + 1)
this.$set(this.viewsList[event.moved.newIndex], 'order', this.viewsList[event.moved.newIndex - 1].order + 1)
} else if (event.moved.newIndex === 0) {
this.$set(this.viewsList[event.moved.newIndex], 'view_order', this.viewsList[1].view_order / 2)
this.$set(this.viewsList[event.moved.newIndex], 'order', this.viewsList[1].order / 2)
} else {
this.$set(this.viewsList[event.moved.newIndex], 'view_order', (this.viewsList[event.moved.newIndex - 1].view_order + this.viewsList[event.moved.newIndex + 1].view_order) / 2)
this.$set(this.viewsList[event.moved.newIndex], 'order', (this.viewsList[event.moved.newIndex - 1].order + this.viewsList[event.moved.newIndex + 1].order) / 2)
}
await this.$api.dbView.update(this.viewsList[event.moved.newIndex].id, {
title: this.viewsList[event.moved.newIndex].title,
order: this.viewsList[event.moved.newIndex].order
})
console.log(this.viewsList)
await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: 'db' }, 'xcModelViewOrderSet', {
id: this.viewsList[event.moved.newIndex].id,
view_order: this.viewsList[event.moved.newIndex].view_order
}])
this.$tele.emit('view:drag')
},
onViewIdChange(id) {
const selectedView = this.viewsList && this.viewsList.find(v => v.id === id)
let queryParams = {}
const selectedView = this.views && this.views.find(v => v.id === id)
// const queryParams = {}
this.$emit('update:selectedViewId', id)
this.$emit('update:selectedView', selectedView)
// if (selectedView.type === 'table') {
// return;
// }
try {
queryParams = JSON.parse(selectedView.query_params) || {}
} catch (e) {
// console.log(e)
}
this.$emit('update:filters', queryParams.filters || [])
this.$emit('update:sortList', queryParams.sortList || [])
this.$emit('update:fieldsOrder', queryParams.fieldsOrder || [])
this.$emit('update:viewStatus', queryParams.viewStatus || {})
this.$emit('update:columnsWidth', queryParams.columnsWidth || {})
this.$emit('update:extraViewParams', queryParams.extraViewParams || {})
this.$emit('update:coverImageField', queryParams.coverImageField)
this.$emit('update:groupingField', queryParams.groupingField)
this.$emit('update:showSystemFields', queryParams.showSystemFields)
if (queryParams.showFields) {
this.$emit('update:showFields', queryParams.showFields)
} else {
this.$emit('mapFieldsAndShowFields')
}
// try {
// queryParams = JSON.parse(selectedView.query_params) || {}
// } catch (e) {
// // console.log(e)
// }
// this.$emit('update:filters', queryParams.filters || [])
// this.$emit('update:sortList', queryParams.sortList || [])
// this.$emit('update:fieldsOrder', queryParams.fieldsOrder || [])
// this.$emit('update:viewStatus', queryParams.viewStatus || {})
// this.$emit('update:columnsWidth', queryParams.columnsWidth || {})
// this.$emit('update:extraViewParams', queryParams.extraViewParams || {})
// this.$emit('update:coverImageField', queryParams.coverImageField)
// this.$emit('update:groupingField', queryParams.groupingField)
// this.$emit('update:showSystemFields', queryParams.showSystemFields)
// if (queryParams.showFields) {
// this.$emit('update:showFields', queryParams.showFields)
// } else {
// this.$emit('mapFieldsAndShowFields')
// }
this.$emit('loadTableData')
},
hideMiniSponsorCard() {
@ -707,6 +670,7 @@ export default {
}
this.createViewType = type
this.showCreateView = true
this.$tele.emit(`view:create:trigger:${type}`)
},
isCentrallyAligned(col) {
return ![
@ -730,30 +694,41 @@ export default {
},
async saveShareLinkPassword() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [
{ dbAlias: this.nodes.dbAlias },
'updateSharedViewLinkPassword',
{
id: this.shareLink.id,
password: this.shareLink.password
}
])
await this.$api.dbViewShare.update(this.shareLink.id, {
password: this.shareLink.password
})
// await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { dbAlias: this.nodes.dbAlias },
// 'updateSharedViewLinkPassword',
// {
// id: this.shareLink.id,
// password: this.shareLink.password
// }
// ])
this.$toast.success('Successfully updated').goAway(3000)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
this.$tele.emit('share-view:enable-pwd')
},
async loadViews() {
this.viewsList = await this.sqlOp(
{
dbAlias: this.nodes.dbAlias
},
'xcVirtualTableList',
{
tn: this.table
}
)
// this.viewsList = await this.sqlOp(
// {
// dbAlias: this.nodes.dbAlias
// },
// 'xcVirtualTableList',
// {
// tn: this.table
// }
// )
// this.selectedViewIdLocal = this.viewsList && this.viewsList[0] && this.viewsList[0].id
// this.viewsList = []
const views = (await this.$api.dbView.list(this.meta.id)).list
this.$emit('update:views', views)
},
// async onViewChange() {
// let query_params = {}
@ -773,7 +748,7 @@ export default {
// this.$emit('loadTableData');
// },
copyapiUrlToClipboard() {
this.$clipboard(this.currentApiUrl)
copyTextToClipboard(this.currentApiUrl)
this.clipboardSuccessHandler()
},
async updateViewName(view, index) {
@ -781,7 +756,7 @@ export default {
return
}
const oldTitle = view.title
// const oldTitle = view.title
this.$set(view, 'edit', false)
if (view.title_temp === view.title) {
@ -801,16 +776,20 @@ export default {
})
}
this.$set(view, 'title', view.title_temp)
await this.sqlOp({ dbAlias: this.nodes.dbAlias }, 'xcVirtualTableRename', {
id: view.id,
old_title: oldTitle,
title: view.title_temp,
alias: view.alias,
parent_model_title: this.meta.tn
// await this.sqlOp({ dbAlias: this.nodes.dbAlias }, 'xcVirtualTableRename', {
// id: view.id,
// old_title: oldTitle,
// title: view.title_temp,
// alias: view.alias,
// parent_model_title: this.meta.table_name
// })
await this.$api.dbView.update(view.id, {
title: view.title,
order: view.order
})
this.$toast.success('View renamed successfully').goAway(3000)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
},
showRenameTextBox(view, i) {
@ -821,59 +800,67 @@ export default {
input.focus()
input.setSelectionRange(0, input.value.length)
})
this.$tele.emit(`view:rename:trigger:${view.type}`)
},
async deleteView(view) {
try {
await this.sqlOp({ dbAlias: this.nodes.dbAlias }, 'xcVirtualTableDelete', {
id: view.id,
title: view.alias || view.title,
view_name: view.alias || view.title,
parent_model_title: this.table
})
// await this.sqlOp({ dbAlias: this.nodes.dbAlias }, 'xcVirtualTableDelete', {
// id: view.id,
// title: view.alias || view.title,
// view_name: view.alias || view.title,
// parent_model_title: this.table
// })
await this.$api.dbView.delete(view.id)
this.$toast.success('View deleted successfully').goAway(3000)
await this.loadViews()
} catch (e) {
this.$toast.error(e.message).goAway(3000)
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
this.$tele.emit(`view:delete:submit:${view.type}`)
},
async genShareLink() {
const sharedViewUrl = await this.$store.dispatch('sqlMgr/ActSqlOp', [
{ dbAlias: this.nodes.dbAlias },
'createSharedViewLink',
{
model_name: this.table,
// meta: this.meta,
query_params: {
where: this.concatenatedXWhere,
sort: this.sort,
fields: Object.keys(this.showFields)
.filter(f => this.showFields[f])
.join(','),
showFields: this.showFields,
fieldsOrder: this.fieldsOrder,
extraViewParams: this.extraViewParams,
selectedViewId: this.selectedViewId,
columnsWidth: this.columnsWidth
},
view_name: this.selectedView.title,
type: this.selectedView.type,
show_as: this.selectedView.show_as,
password: this.sharedViewPassword
}
])
this.shareLink = sharedViewUrl
// const sharedViewUrl = await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { dbAlias: this.nodes.dbAlias },
// 'createSharedViewLink',
// {
// model_name: this.table,
// // meta: this.meta,
// query_params: {
// where: this.concatenatedXWhere,
// sort: this.sort,
// fields: Object.keys(this.showFields)
// .filter(f => this.showFields[f])
// .join(','),
// showFields: this.showFields,
// fieldsOrder: this.fieldsOrder,
// extraViewParams: this.extraViewParams,
// selectedViewId: this.selectedViewId,
// columnsWidth: this.columnsWidth
// },
// view_name: this.selectedView.title,
// type: this.selectedView.type,
// show_as: this.selectedView.show_as,
// password: this.sharedViewPassword
// }
// ])
const shared = (await this.$api.dbViewShare.create(this.selectedViewId))
// todo: url
this.shareLink = shared
this.showShareModel = true
},
copyView(view, i) {
this.createViewType = view.show_as
this.createViewType = view.type
this.showCreateView = true
this.copyViewRef = view
this.$tele.emit(`view:copy:trigger${view.type}`)
},
async onViewCreate(viewMeta) {
this.copyViewRef = null
await this.loadViews()
this.selectedViewIdLocal = viewMeta.id
// await this.onViewChange();
this.$tele.emit(`view:create:submit:${viewMeta.type}`)
},
clipboard(str) {
const el = document.createElement('textarea')
@ -890,6 +877,7 @@ export default {
copyShareUrlToClipboard() {
this.clipboard(this.sharedViewUrl)
this.clipboardSuccessHandler()
this.$tele.emit('share-view:copy-url')
}
}
}

22
packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue

@ -5,7 +5,7 @@
v-if="hm"
ref="cell"
:row="row"
:value="row[`${hm._tn}List`]"
:value="row[column.title]"
:meta="meta"
:hm="hm"
:nodes="nodes"
@ -27,9 +27,8 @@
ref="cell"
:is-public="isPublic"
:row="row"
:value="row[`${mm._rtn}MMList`]"
:value="row[column.title]"
:meta="meta"
:mm="mm"
:nodes="nodes"
:sql-ui="sqlUi"
:active="active"
@ -51,9 +50,8 @@
:disabled-columns="disabledColumns"
:active="active"
:row="row"
:value="row[`${bt._rtn}Read`]"
:value="row[column.title]"
:meta="meta"
:bt="bt"
:nodes="nodes"
:api="api"
:sql-ui="sqlUi"
@ -71,6 +69,7 @@
:disabled-columns="disabledColumns"
:active="active"
:row="row"
:value="row[column.title]"
:meta="meta"
:metas="metas"
:nodes="nodes"
@ -99,6 +98,7 @@
</template>
<script>
import { UITypes } from 'nocodb-sdk'
import RollupCell from './virtualCell/rollupCell'
import FormulaCell from './virtualCell/formulaCell'
import hasManyCell from './virtualCell/hasManyCell'
@ -150,22 +150,22 @@ export default {
},
computed: {
hm() {
return this.column && this.column.hm
return this.column && this.column.uidt === UITypes.LinkToAnotherRecord && this.column.colOptions.type === 'hm'
},
bt() {
return this.column && this.column.bt
return this.column && (this.column.uidt === UITypes.ForeignKey || this.column.uidt === UITypes.LinkToAnotherRecord) && this.column.colOptions.type === 'bt'
},
mm() {
return this.column && this.column.mm
return this.column && this.column.uidt === UITypes.LinkToAnotherRecord && this.column.colOptions.type === 'mm'
},
lookup() {
return this.column && this.column.lk
return this.column && this.column.uidt === UITypes.Lookup
},
formula() {
return this.column && this.column.formula
return this.column && this.column.uidt === UITypes.Formula
},
rollup() {
return this.column && this.column.rl
return this.column && this.column.uidt === UITypes.Rollup
}
},
methods: {

124
packages/nc-gui/components/project/spreadsheet/components/virtualCell/belongsToCell.vue

@ -31,6 +31,7 @@
v-model="newRecordModal"
:size="10"
:meta="parentMeta"
:column="column"
:primary-col="parentPrimaryCol"
:primary-key="parentPrimaryKey"
:api="parentApi"
@ -41,6 +42,7 @@
:is-public="isPublic"
:tn="bt && bt.rtn"
:password="password"
:row-id="rowId"
@add-new-record="insertAndMapNewParentRecord"
@add="addChildToParent"
/>
@ -85,7 +87,7 @@
:db-alias="nodes.dbAlias"
:has-many="parentMeta.hasMany"
:belongs-to="parentMeta.belongsTo"
:table="parentMeta.tn"
:table="parentMeta.table_name"
:old-row="{...selectedParent}"
:meta="parentMeta"
:sql-ui="sqlUi"
@ -97,7 +99,7 @@
:is-new.sync="isNewParent"
icon-color="warning"
:breadcrumbs="breadcrumbs"
@cancel="selectedParent = null"
@cancel="selectedParent = null; expandFormModal =false"
@input="onParentSave"
/>
</v-dialog>
@ -106,6 +108,7 @@
<script>
// import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import ListItems from '@/components/project/spreadsheet/components/virtualCell/components/listItems'
import ListChildItems from '@/components/project/spreadsheet/components/virtualCell/components/listChildItems'
import ItemChip from '~/components/project/spreadsheet/components/virtualCell/components/itemChip'
@ -123,9 +126,8 @@ export default {
}
},
isForm: Boolean,
value: [Object, Array],
value: [Array, Object],
meta: [Object],
bt: Object,
nodes: [Object],
row: [Object],
api: [Object, Function],
@ -135,7 +137,8 @@ export default {
disabledColumns: Object,
isPublic: Boolean,
metas: Object,
password: String
password: String,
column: Object
},
data: () => ({
newRecordModal: false,
@ -154,42 +157,32 @@ export default {
}),
computed: {
parentMeta() {
return this.metas ? this.metas[this.bt.rtn] : this.$store.state.meta.metas[this.bt.rtn]
return this.metas ? this.metas[this.column.colOptions.fk_related_model_id] : this.$store.state.meta.metas[this.column.colOptions.fk_related_model_id]
},
// todo : optimize
parentApi() {
return this.parentMeta && this.$ncApis.get({
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
table: this.parentMeta.tn
})
// return this.parentMeta && this.parentMeta._tn
// ? ApiFactory.create(this.$store.getters['project/GtrProjectType'],
// this.parentMeta && this.parentMeta._tn, this.parentMeta && this.parentMeta.columns, this, this.parentMeta)
// : null
},
parentId() {
return this.pid ?? (this.value && this.parentMeta && this.parentMeta.columns.filter(c => c.pk).map(c => this.value[c._cn]).join('___'))
return this.pid ?? (this.value && this.parentMeta && this.parentMeta.columns.filter(c => c.pk).map(c => this.value[c.title]).join('___'))
},
rowId() {
return (this.row && this.meta && this.meta.columns.filter(c => c.pk).map(c => this.row[c.title]).join('___'))
},
parentPrimaryCol() {
return this.parentMeta && (this.parentMeta.columns.find(c => c.pv) || {})._cn
return this.parentMeta && (this.parentMeta.columns.find(c => c.pv) || {}).title
},
parentPrimaryKey() {
return this.parentMeta && (this.parentMeta.columns.find(c => c.pk) || {})._cn
return this.parentMeta && (this.parentMeta.columns.find(c => c.pk) || {}).title
},
parentReferenceKey() {
return this.parentMeta && (this.parentMeta.columns.find(c => c.cn === this.bt.rcn) || {})._cn
return this.parentMeta && (this.parentMeta.columns.find(c => c.id === this.column.colOptions.fk_parent_column_id) || {}).title
},
btWhereClause() {
// if parent reference key is pk, then filter out the selected value
// else, filter out the selected value + empty values (as we can't set an empty value)
const prk = this.parentReferenceKey
const isPk = !!(this.parentMeta && (this.parentMeta.columns.find(c => c.pk && c._cn === prk))) || false
let selectedValue = this.meta && this.meta.columns ? this.meta.columns.filter(c => c.cn === this.bt.cn).map(c => this.row[c._cn] || '').join('___') : ''
if (this.parentMeta && (this.parentMeta.columns.find(c => c._cn === prk)).type !== 'string') {
selectedValue = selectedValue || 0
}
return `(${prk},not,${selectedValue})` + (!isPk ? `~and(${prk},not,)` : '')
const selectedValue = this.meta && this.meta.columns ? this.meta.columns.filter(c => c.id === this.column.colOptions.fk_child_column_id).map(c => this.row[c.title] || '').join('___') : ''
return `(${prk},not,${selectedValue})~or(${prk},is,null)`
},
parentQueryParams() {
if (!this.parentMeta) {
@ -197,9 +190,6 @@ export default {
}
// todo: use reduce
return {
hm: (this.parentMeta && this.parentMeta.v && this.parentMeta.v.filter(v => v.hm).map(({ hm }) => hm.tn).join()) || '',
bt: (this.parentMeta && this.parentMeta.v && this.parentMeta.v.filter(v => v.bt).map(({ bt }) => bt.rtn).join()) || '',
mm: (this.parentMeta && this.parentMeta.v && this.parentMeta.v.filter(v => v.mm).map(({ mm }) => mm.rtn).join()) || ''
}
},
parentAvailableColumns() {
@ -210,7 +200,7 @@ export default {
const columns = []
if (this.parentMeta.columns) {
columns.push(...this.parentMeta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn) && !((this.parentMeta.v || []).some(v => v.bt && v.bt.cn === c.cn))))
columns.push(...this.parentMeta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.column_name) && !((this.parentMeta.v || []).some(v => v.bt && v.bt.column_name === c.column_name))))
}
if (this.parentMeta.v) {
columns.push(...this.parentMeta.v.map(v => ({ ...v, virtual: 1 })))
@ -229,7 +219,7 @@ export default {
return Object.values(this.value || this.localState)[1]
}
return null
},
}
},
watch: {
isNew(n, o) {
@ -240,6 +230,9 @@ export default {
}
},
async mounted() {
if (this.isNew && this.value) {
this.localState = this.value
}
if (this.isForm) {
await this.loadParentMeta()
}
@ -259,13 +252,21 @@ export default {
await this.loadParentMeta()
this.newRecordModal = false
this.isNewParent = true
this.selectedParent = {}
this.selectedParent = {
[(this.parentMeta.columns.find(c => c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions &&
this.column.colOptions &&
c.colOptions.fk_child_column_id === this.column.colOptions.fk_child_column_id &&
c.colOptions.fk_parent_column_id === this.column.colOptions.fk_parent_column_id &&
c.colOptions.type === RelationTypes.HAS_MANY
) || {}).title]: [this.row]
}
this.expandFormModal = true
},
async unlink() {
const column = this.meta.columns.find(c => c.cn === this.bt.cn)
const _cn = column._cn
async unlink(parent) {
const column = this.meta.columns.find(c => c.id === this.column.colOptions.fk_child_column_id)
const _cn = column.title
if (this.isNew) {
this.$emit('updateCol', this.row, _cn, null)
this.localState = null
@ -276,8 +277,15 @@ export default {
this.$toast.info('Unlink is not possible, instead map to another parent.').goAway(3000)
return
}
const id = this.meta.columns.filter(c => c.pk).map(c => this.row[c._cn]).join('___')
await this.api.update(id, { [_cn]: null }, this.row)
const id = this.meta.columns.filter(c => c.pk).map(c => this.row[c.title]).join('___')
// todo: audit
await this.$api.data.nestedDelete(
this.meta.id,
id,
this.column.id,
'bt',
parent[this.parentPrimaryKey]
)
this.$emit('loadTableData')
if (this.isForm && this.$refs.childList) {
this.$refs.childList.loadData()
@ -286,8 +294,8 @@ export default {
async showParentListModal() {
this.parentListModal = true
await this.loadParentMeta()
const pid = this.meta.columns.filter(c => c.pk).map(c => this.row[c._cn]).join('___')
const _cn = this.parentMeta.columns.find(c => c.cn === this.hm.cn)._cn
const pid = this.meta.columns.filter(c => c.pk).map(c => this.row[c.title]).join('___')
const _cn = this.parentMeta.columns.find(c => c.column_name === this.hm.column_name).title
this.childList = await this.parentApi.paginatedList({
where: `(${_cn},eq,${pid})`
})
@ -300,7 +308,7 @@ export default {
if (act === 'hideDialog') {
this.dialogShow = false
} else {
const id = this.parentMeta.columns.filter(c => c.pk).map(c => child[c._cn]).join('___')
const id = this.parentMeta.columns.filter(c => c.pk).map(c => child[c.title]).join('___')
await this.parentApi.delete(id)
this.pid = null
this.dialogShow = false
@ -317,15 +325,8 @@ export default {
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.bt.rtn
id: this.column.colOptions.fk_related_model_id
})
// const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.bt.rtn
// }]);
// this.parentMeta = JSON.parse(parentTableData.meta)
}
},
async showNewRecordModal() {
@ -333,28 +334,27 @@ export default {
this.newRecordModal = true
},
async addChildToParent(parent) {
const pkColumns = this.parentMeta.columns.filter(c => c.pk)
const pid = pkColumns.map(c => parent[c._cn]).join('___')
const id = this.meta.columns.filter(c => c.pk).map(c => this.row[c._cn]).join('___')
const _cn = this.meta.columns.find(c => c.cn === this.bt.cn)._cn
// let isNum = false
// if (pkColumns.length === 1) {
// isNum = ['float', 'integer'].includes(this.sqlUi.getAbstractType(pkColumns[0]))
// }
const pid = this._extractRowId(parent, this.parentMeta)
const id = this._extractRowId(this.row, this.meta)
const _cn = this.meta.columns.find(c => c.id === this.column.colOptions.fk_child_column_id).title
if (this.isNew) {
const _rcn = this.parentMeta.columns.find(c => c.id === this.column.colOptions.fk_parent_column_id).title
this.localState = parent
this.$emit('update:localState', this.localState)
this.$emit('updateCol', this.row, _cn, +pid || pid)
this.$emit('updateCol', this.row, _cn, parent[_rcn])
this.newRecordModal = false
return
}
await this.api.update(id, {
[_cn]: parseIfInteger(parent[this.parentReferenceKey])
}, {
[_cn]: this.value && this.value[this.parentPrimaryKey]
})
await this.$api.data.nestedAdd(
this.meta.id,
id,
this.column.id,
'bt',
pid
)
this.pid = pid
this.newRecordModal = false

1
packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/itemChip.vue

@ -7,7 +7,6 @@
:color="isDark ? '' : 'primary lighten-5'"
@click="!readonly && active && $emit('edit',item)"
>
<!-- <span class="name" :title="value">{{ value }}</span>-->
<slot><span class="name" :title="value">{{ value }}</span></slot>
<div v-show="active" v-if="!readonly && _isUIAllowed('table-remove-linked-record')" class="mr-n1 ml-2">
<x-icon

82
packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listChildItems.vue

@ -1,8 +1,7 @@
<template>
<!-- <v-dialog v-model="show" width="600">-->
<v-card width="600" color="">
<v-card-title v-if="!isForm" class="textColor--text mx-2" :class="{'py-2':isForm}">
<span v-if="!isForm">{{ meta ? meta._tn : 'Children' }}</span>
<span v-if="!isForm">{{ meta ? meta.title : 'Children' }}</span>
<v-spacer />
<v-icon small class="mr-1" @click="loadData()">
mdi-reload
@ -19,7 +18,7 @@
>
mdi-link
</v-icon>&nbsp;
Link to '{{ meta._tn }}'
Link to '{{ meta.title }}'
</v-btn>
</v-card-title>
<v-card-text>
@ -38,7 +37,7 @@
>
mdi-link
</v-icon>&nbsp;
Link to '{{ meta._tn }}'
Link to '{{ meta.title }}'
</v-btn>
</div>
<template v-if="isDataAvail">
@ -52,7 +51,7 @@
<div class="remove-child-icon d-flex align-center">
<x-icon
v-if="((isPublic && isForm) || (!isPublic && _isUIAllowed('xcDatatableEditable'))) && !readOnly "
:tooltip="`Unlink this '${meta._tn}' from '${parentMeta._tn}'`"
:tooltip="`Unlink this '${meta.title}' from '${parentMeta.title}'`"
:color="['error','grey']"
small
icon.class="mr-1 mt-n1"
@ -61,8 +60,8 @@
mdi-link-variant-remove
</x-icon>
<x-icon
v-if="!isPublic && !mm && !bt && !readOnly && _isUIAllowed('xcDatatableEditable')"
:tooltip="`Delete row in '${meta._tn}'`"
v-if="!isPublic && type === RelationTypes.HAS_MANY && !readOnly && _isUIAllowed('xcDatatableEditable')"
:tooltip="`Delete row in '${meta.title}'`"
:color="['error','grey']"
small
@click.stop="$emit('delete',ch,i)"
@ -91,10 +90,10 @@
<div v-if="isForm" class="mb-2 d-flex align-center justify-center">
<pagination
v-if="!bt && data && data.count > 1"
v-if="!bt && data && data.pageInfo&& data.pageInfo.totalRows > 1"
v-model="page"
:size="size"
:count="data && data.count"
:count="data && data.pageInfo&& data.pageInfo.totalRows"
@input="loadData"
/>
</div>
@ -102,10 +101,10 @@
</v-card-text>
<v-card-actions v-if="!isForm" class="justify-center flex-column" :class="{'py-0':isForm}">
<pagination
v-if="!bt && data && data.count > 1"
v-if="!bt && data && data.pageInfo&& data.pageInfo.totalRows > 1"
v-model="page"
:size="size"
:count="data && data.count"
:count="data && data.pageInfo&& data.pageInfo.totalRows"
class="mb-3"
@input="loadData"
/>
@ -115,6 +114,7 @@
</template>
<script>
import { RelationTypes } from 'nocodb-sdk'
import Pagination from '@/components/project/spreadsheet/components/pagination'
export default {
@ -123,7 +123,7 @@ export default {
props: {
readOnly: Boolean,
isForm: Boolean,
bt: Object,
bt: [Object],
localState: [Array],
isNew: Boolean,
value: Boolean,
@ -151,6 +151,7 @@ export default {
password: String
},
data: () => ({
RelationTypes,
data: null,
page: 1
}),
@ -178,28 +179,47 @@ export default {
methods: {
async loadData() {
if ((!this.isForm && this.isPublic) && this.$route.params.id) {
this.data = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'sharedViewNestedChildDataGet', {
password: this.password,
limit: this.size,
tn: this.tn,
view_id: this.$route.params.id,
row_id: this.rowId,
offset: this.size * (this.page - 1),
query: this.query,
_cn: this.column._cn,
ptn: this.parentMeta.tn,
ctn: this.meta.tn,
type: this.type
}])
if (this.column && this.column.colOptions && this.rowId) {
this.data = (await this.$api.public.dataNestedList(
this.$route.params.id,
this.rowId,
this.column.colOptions.type,
this.column.fk_column_id || this.column.id, {
limit: this.size,
offset: this.size * (this.page - 1)
}, {}))
}
return
}
if (!this.api || this.isNew) { return }
this.data = await this.api.paginatedList({
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams
})
if (this.isNew) {
return
}
if (this.column && this.column.colOptions) {
this.data = (await this.$api.data.nestedList(
this.column.fk_model_id,
this.rowId,
this.column.id,
this.column.colOptions.type,
{
query: {
limit: this.size,
offset: this.size * (this.page - 1)
// ...this.queryParams
}
}))
} else {
this.data = (await this.$api.data.list(
this.meta.id, {
query: {
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams
}
}))
}
}
}
}

76
packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listItems.vue

@ -49,7 +49,7 @@
@click="$emit('add',ch)"
>
<v-card-text class="primary-value textColor--text text--lighten-2 d-flex">
<span class="font-weight-bold"> {{ ch[primaryCol] }}&nbsp;</span>
<span class="font-weight-bold"> {{ ch[primaryCol] || (ch &&Object.values(ch).slice(0,1).join()) }}&nbsp;</span>
<span
v-if="primaryKey"
class="grey--text caption primary-key "
@ -73,7 +73,7 @@
v-if="data && data.list && data.list.length"
v-model="page"
:size="size"
:count="data.count"
:count="data && data.pageInfo&& data.pageInfo.totalRows"
class="mb-3"
@input="loadData"
/>
@ -111,7 +111,9 @@ export default {
parentId: [String, Number],
parentMeta: [Object],
isPublic: Boolean,
password: String
password: String,
column: Object,
rowId: [Number, String]
},
data: () => ({
data: null,
@ -130,7 +132,7 @@ export default {
hmParentPrimaryValCol() {
return this.hm &&
this.parentMeta &&
this.parentMeta.columns.find(v => v.pv)._cn
this.parentMeta.columns.find(v => v.pv).title
}
},
mounted() {
@ -139,41 +141,43 @@ export default {
methods: {
async loadData() {
if (this.isPublic) {
this.data = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'sharedViewNestedDataGet', {
password: this.password,
limit: this.size,
tn: this.tn,
view_id: this.$route.params.id,
offset: this.size * (this.page - 1),
query: this.query
}])
this.data = (await this.$api.public.dataRelationList(
this.$route.params.id,
this.column.id,
{ password: this.password }, {
query: {
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams
}
}))
} else {
if (!this.api) {
return
}
const isByPass = this.queryParams.isByPass || false
if (isByPass) {
return
}
let where = this.queryParams.where || ''
if (this.query) {
where += (where ? '~and' : '') + `(${this.primaryCol},like,%${this.query}%)`
}
const where = `(${this.primaryCol},like,%${this.query}%)`
if (this.mm) {
this.data = await this.api.paginatedM2mNotChildrenList({
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams,
where
}, this.mm.vtn, this.parentId || -1)
// eslint-disable-next-line no-lonely-if
if (this.column && this.column.colOptions && this.rowId) {
this.data = (await this.$api.data.nestedExcludedList(
this.column.fk_model_id,
this.rowId,
this.column.id,
this.column.colOptions.type,
{
query: {
limit: this.size,
offset: this.size * (this.page - 1),
where: this.query && `(${this.primaryCol},like,${this.query})`
}
}))
} else {
this.data = await this.api.paginatedList({
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams,
where
})
this.data = (await this.$api.data.list(
this.meta.id, {
query: {
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams,
where
}
}))
}
}
}

39
packages/nc-gui/components/project/spreadsheet/components/virtualCell/formulaCell.vue

@ -1,43 +1,44 @@
<template>
<v-tooltip
v-if="column.formula && column.formula.error && column.formula.error.length"
v-if="column && column.colOptions&& column.colOptions.error"
bottom
color="error"
>
<template #activator="{on}">
<span class="caption" v-on="on">ERR<span class="error--text">!</span></span>
</template>
<span class=" font-weight-bold">{{ column.formula.error.join(', ') }}</span>
<span class=" font-weight-bold">{{ column.colOptions.error }}</span>
</v-tooltip>
<div v-else-if="urls" v-html="urls"></div>
<div v-else-if="urls" v-html="urls" />
<div v-else>
{{ row[column._cn] }}
{{ row[column.title] }}
</div>
</template>
<script>
export default {
name: 'FormulaCell',
props: ['column', 'row'],
props: { column: Object, row: Object },
computed: {
urls: function() {
if(this.row[this.column._cn] ) {
let rawText = this.row[this.column._cn].toString();
let matchURLs = rawText.match(/URI::\((.*?)\)/g);
if(matchURLs) {
for(const url of matchURLs) {
let tmpuri = url.match(/URI::\((.*?)\)/)[1];
rawText = rawText.replace(url, '<a href="' + tmpuri + '" target="_blank">' + tmpuri + '</a>');
}
return rawText;
}
}
return false;
urls() {
if (!this.row[this.column.title]) { return }
const rawText = this.row[this.column.title].toString()
let found = false
const out = rawText.match(/URI::\((.*?)\)/g, (_, url) => {
found = true
const a = document.createElement('a')
a.textContent = url
a.setAttribute('href', url)
return a.innerHTML
})
return found && out
}
}
}
</script>
<style scoped>
<style scoped>/**/
</style>

127
packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue

@ -2,7 +2,7 @@
<div class="d-flex d-100 chips-wrapper" :class="{active}">
<template v-if="!isForm">
<div class="chips d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value|| localState">
<template v-if="value||localState">
<item-chip
v-for="(ch,i) in (value|| localState)"
:key="i"
@ -22,6 +22,7 @@
</span>
</template>
</div>
<div
v-if="!isLocked"
class="actions align-center justify-center px-1 flex-shrink-1"
@ -44,14 +45,13 @@
<list-items
v-if="newRecordModal"
v-model="newRecordModal"
:hm="hm"
:tn="hm && hm.tn"
:size="10"
:meta="childMeta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
:api="childApi"
:parent-meta="meta"
:column="column"
:query-params="{
...childQueryParams,
// check if it needs to bypass to
@ -67,6 +67,7 @@
}"
:is-public="isPublic"
:password="password"
:row-id="parentId"
@add-new-record="insertAndAddNewChildRecord"
@add="addChildToParent"
/>
@ -109,7 +110,6 @@
/>
<v-dialog
v-if="selectedChild && !isPublic"
v-model="expandFormModal"
:overlay-opacity="0.8"
width="1000px"
@ -124,10 +124,9 @@
:db-alias="nodes.dbAlias"
:has-many="childMeta.hasMany"
:belongs-to="childMeta.belongsTo"
:table="childMeta.tn"
:table="childMeta.table_name"
:old-row="{...selectedChild}"
:meta="childMeta"
:sql-ui="sqlUi"
:primary-value-column="childPrimaryCol"
:api="childApi"
:available-columns="childAvailableColumns"
@ -137,7 +136,7 @@
:is-new.sync="isNewChild"
:disabled-columns="disabledChildColumns"
:breadcrumbs="breadcrumbs"
@cancel="selectedChild = null"
@cancel="selectedChild = null; expandFormModal =false"
@input="onChildSave"
/>
</v-dialog>
@ -146,6 +145,7 @@
<script>
// import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
import { isSystemColumn, RelationTypes, UITypes } from 'nocodb-sdk'
import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel'
import Pagination from '@/components/project/spreadsheet/components/pagination'
import ListItems from '@/components/project/spreadsheet/components/virtualCell/components/listItems'
@ -177,10 +177,8 @@ export default {
},
value: [Object, Array],
meta: [Object],
hm: Object,
nodes: [Object],
row: [Object],
sqlUi: [Object, Function],
active: Boolean,
isNew: Boolean,
isForm: Boolean,
@ -204,37 +202,30 @@ export default {
}),
computed: {
childMeta() {
return this.metas ? this.metas[this.hm.tn] : this.$store.state.meta.metas[this.hm.tn]
return this.metas
? this.metas[this.column.colOptions.fk_related_model_id]
: this.$store.state.meta.metas[this.column.colOptions.fk_related_model_id]
},
// todo : optimize
childApi() {
return this.childMeta && this.$ncApis.get({
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
table: this.childMeta.tn
})
// return this.childMeta && this.childMeta._tn
// ? ApiFactory.create(this.$store.getters['project/GtrProjectType'],
// this.childMeta && this.childMeta._tn, this.childMeta && this.childMeta.columns, this, this.childMeta)
// : null
},
childPrimaryCol() {
return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {})._cn
return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {}).title
},
primaryCol() {
return this.meta && (this.meta.columns.find(c => c.pv) || {})._cn
return this.meta && (this.meta.columns.find(c => c.pv) || {}).title
},
childPrimaryKey() {
return this.childMeta && (this.childMeta.columns.find(c => c.pk) || {})._cn
return this.childMeta && (this.childMeta.columns.find(c => c.pk) || {}).title
},
childForeignKey() {
return this.childMeta && (this.childMeta.columns.find(c => c.cn === this.hm.cn) || {})._cn
return this.childMeta && (this.childMeta.columns.find(c => c.id === this.column.colOptions.fk_child_column_id) || {}).title
},
childForeignKeyVal() {
return this.meta && this.meta.columns ? this.meta.columns.filter(c => c._cn === this.childForeignKey).map(c => this.row[c._cn] || '').join('___') : ''
return this.meta && this.meta.columns ? this.meta.columns.filter(c => c.title === this.childForeignKey).map(c => this.row[c.title] || '').join('___') : ''
},
isVirtualRelation() {
return (this.childMeta && (!!this.childMeta.columns.find(c => c.cn === this.hm.cn && this.hm.type === 'virtual'))) || false
return this.column && this.column.colOptions.virtual// (this.childMeta && (!!this.childMeta.columns.find(c => c.column_name === this.hm.column_name && this.hm.type === 'virtual'))) || false
},
isByPass() {
if (this.isVirtualRelation) {
@ -242,7 +233,10 @@ export default {
}
// if child fk references a column in parent which is not pk,
// then this column has to be filled
if (((this.meta && this.meta.columns.find(c => !c.pk && c.cn === this.hm.rcn)) || false)) {
// if (((this.meta && this.meta.columns.find(c => !c.pk && c.id === this.hm.rcn)) || false)) {
// return this.childForeignKeyVal === ''
// }
if (((this.meta && this.meta.columns.find(c => !c.pk && c.id === this.column.fk_parent_column_id)) || false)) {
return this.childForeignKeyVal === ''
}
return false
@ -255,12 +249,12 @@ export default {
return this.selectedChild && !this.isPublic ? () => import('@/components/project/spreadsheet/components/expandedForm') : 'span'
},
childAvailableColumns() {
const hideCols = ['created_at', 'updated_at']
// const hideCols = ['created_at', 'updated_at']
if (!this.childMeta) { return [] }
const columns = []
if (this.childMeta.columns) {
columns.push(...this.childMeta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn) && !((this.childMeta.v || []).some(v => v.bt && v.bt.cn === c.cn))))
columns.push(...this.childMeta.columns.filter(c => !isSystemColumn(c)))
}
if (this.childMeta.v) {
columns.push(...this.childMeta.v.map(v => ({ ...v, virtual: 1 })))
@ -271,15 +265,15 @@ export default {
if (!this.childMeta) { return {} }
// todo: use reduce
return {
hm: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.hm).map(({ hm }) => hm.tn).join()) || '',
hm: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.hm).map(({ hm }) => hm.table_name).join()) || '',
bt: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.bt).map(({ bt }) => bt.rtn).join()) || '',
mm: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.mm).map(({ mm }) => mm.rtn).join()) || ''
}
},
parentId() {
return (this.meta && this.meta.columns &&
(this.meta.columns.filter(c => c._cn === this.childForeignKey).map(c => this.row[c._cn] || '').join('___') ||
this.meta.columns.filter(c => c.pk).map(c => this.row[c._cn]).join('___'))) || ''
(this.meta.columns.filter(c => c.title === this.childForeignKey).map(c => this.row[c.title] || '').join('___') ||
this.meta.columns.filter(c => c.pk).map(c => this.row[c.title]).join('___'))) || ''
}
},
watch: {
@ -291,6 +285,10 @@ export default {
},
async mounted() {
await this.loadChildMeta()
if (this.isNew && this.value) {
this.localState = [...this.value]
}
},
created() {
this.loadChildMeta()
@ -315,16 +313,16 @@ export default {
if (act === 'hideDialog') {
this.dialogShow = false
} else {
const id = this.childMeta.columns.filter(c => c.pk).map(c => child[c._cn]).join('___')
const id = this.childMeta.columns.filter(c => c.pk).map(c => child[c.title]).join('___')
try {
await this.childApi.delete(id)
await this.$api.data.delete(this.childMeta.id, id)
this.dialogShow = false
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData()
}
} catch (e) {
this.$toast.error(e.message)
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
}
}
@ -337,14 +335,21 @@ export default {
}
await this.loadChildMeta()
const column = this.childMeta.columns.find(c => c.cn === this.hm.cn)
const column = this.childMeta.columns.find(c => c.id === this.column.colOptions.fk_child_column_id)
if (column.rqd) {
this.$toast.info('Unlink is not possible, instead add to another record.').goAway(3000)
return
}
const _cn = column._cn
const id = this.childMeta.columns.filter(c => c.pk).map(c => child[c._cn]).join('___')
await this.childApi.update(id, { [_cn]: null }, child)
const id = this.childMeta.columns.filter(c => c.pk).map(c => child[c.title]).join('___')
await this.$api.data.nestedDelete(
this.meta.id,
this.parentId,
this.column.id,
'hm',
id
)
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData()
@ -358,16 +363,8 @@ export default {
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.hm.tn
id: this.column.colOptions.fk_related_model_id
})
// const childTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.hm.tn
// }]);
// this.childMeta = JSON.parse(childTableData.meta);
// this.childQueryParams = JSON.parse(childTableData.query_params);
}
},
async showNewRecordModal() {
@ -383,14 +380,15 @@ export default {
return
}
const id = this.childMeta.columns.filter(c => c.pk).map(c => child[c._cn]).join('___')
const _cn = this.childForeignKey
const id = this.childMeta.columns.filter(c => c.pk).map(c => child[c.title]).join('___')
this.newRecordModal = false
await this.childApi.update(id, {
[_cn]: parseIfInteger(this.parentId)
}, {
[_cn]: child[this.childForeignKey]
})
await this.$api.data.nestedAdd(
this.meta.id,
this.parentId,
this.column.id,
'hm',
id
)
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
@ -400,8 +398,8 @@ export default {
async editChild(child) {
await this.loadChildMeta()
this.isNewChild = false
this.selectedChild = child
this.expandFormModal = true
this.selectedChild = child
setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.reload()
}, 500)
@ -411,7 +409,14 @@ export default {
await this.loadChildMeta()
this.isNewChild = true
this.selectedChild = {
[this.childForeignKey]: parseIfInteger(this.parentId)
[this.childForeignKey]: parseIfInteger(this.parentId),
[(this.childMeta.columns.find(c => c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions &&
this.column.colOptions &&
c.colOptions.fk_child_column_id === this.column.colOptions.fk_child_column_id &&
c.colOptions.fk_parent_column_id === this.column.colOptions.fk_parent_column_id &&
c.colOptions.type === RelationTypes.BELONGS_TO
) || {}).title]: this.row
}
this.expandFormModal = true
if (!this.isNew) {
@ -434,13 +439,13 @@ export default {
while (child = this.localState.pop()) {
if (row) {
// todo: use common method
const pid = this.meta.columns.filter(c => c.pk).map(c => row[c._cn]).join('___')
const id = this.childMeta.columns.filter(c => c.pk).map(c => child[c._cn]).join('___')
const _cn = this.childForeignKey
const pid = this.meta.columns.filter(c => c.pk).map(c => row[c.title]).join('___')
const id = this.childMeta.columns.filter(c => c.pk).map(c => child[c.title]).join('___')
const title = this.childForeignKey
await this.childApi.update(id, {
[_cn]: parseIfInteger(pid)
[title]: parseIfInteger(pid)
}, {
[_cn]: child[this.childForeignKey]
[title]: child[this.childForeignKey]
})
} else {
await this.addChildToParent(child)

217
packages/nc-gui/components/project/spreadsheet/components/virtualCell/lookupCell.vue

@ -1,59 +1,77 @@
<template>
<div class="chips-wrapper">
<div class="chips d-flex align-center lookup-items">
<template v-if="localValue">
<item-chip
v-for="(value,i) in localValue"
:key="i"
:active="active"
:value="value"
:readonly="true"
<div class="d-flex flex-wrap wrapper">
<template v-if="lookupColumnMeta">
<template
v-if="isVirtualCol(lookupColumnMeta)"
>
<template
:is="virtualCell"
v-if="lookupColumnMeta.uidt === UITypes.LinkToAnotherRecord &&lookupColumnMeta.colOptions.type === RelationTypes.BELONGS_TO && Array.isArray(value) "
>
<table-cell
:column="lookUpColumn"
:meta="lookUpMeta"
:db-alias="nodes.dbAlias"
:value="value"
<div
:is="virtualCell"
v-for="(v,i) in value"
:key="i"
:is-public="true"
:metas="metas"
:is-locked="true"
:column="lookupColumnMeta"
:row="{[lookupColumnMeta.title]:v}"
:nodes="nodes"
:meta="lookupTableMeta"
:sql-ui="sqlUi"
/>
</item-chip>
</template>
<div
:is="virtualCell"
v-else
:is-public="true"
:metas="metas"
:is-locked="true"
:column="lookupColumnMeta"
:row="{[lookupColumnMeta.title]:value}"
:nodes="nodes"
:meta="lookupTableMeta"
:sql-ui="sqlUi"
/>
</template>
<template v-else>
<template v-if="localValue">
<item-chip
v-for="(value,i) in localValue"
:key="i"
style="margin: 1.5px"
:active="active"
:value="value"
:readonly="true"
>
<table-cell
:is-locked="true"
:column="lookupColumnMeta"
:meta="lookupTableMeta"
:db-alias="nodes.dbAlias"
:value="value"
:sql-ui="sqlUi"
/>
</item-chip>
</template>
</template>
<span
v-if="localValue && localValue.length === 10"
class="caption pointer ml-1 grey--text"
@click="showLookupListModal"
>more...
</span>
</div>
<list-child-items-modal
v-if="lookUpMeta && lookupListModal"
ref="childList"
v-model="lookupListModal"
:is-form="isForm"
:is-new="isNew"
:size="10"
:meta="lookUpMeta"
:parent-meta="meta"
:primary-col="lookUpColumnAlias"
:api="lookupApi"
:password="password"
:read-only="true"
:query-params="queryParams"
:metas="metas"
/>
</template>
</div>
</template>
<script>
// import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
import { isVirtualCol, RelationTypes, UITypes } from 'nocodb-sdk'
import TableCell from '../cell'
import ItemChip from '@/components/project/spreadsheet/components/virtualCell/components/itemChip'
import ListChildItemsModal
from '@/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal'
export default {
name: 'LookupCell',
components: { TableCell, ListChildItemsModal, ItemChip },
components: {
TableCell,
// ListChildItemsModal,
ItemChip
},
props: {
meta: [Object],
metas: [Object],
@ -64,105 +82,69 @@ export default {
sqlUi: [Object, Function],
active: Boolean,
isNew: Boolean,
isForm: Boolean
isForm: Boolean,
value: [Object, Array, String, Number]
},
data: () => ({
lookupListModal: false
UITypes,
lookupListModal: false,
lookupTableMeta: null,
lookupColumnMeta: null,
isVirtualCol,
RelationTypes
}),
computed: {
virtualCell() {
return this.lookupColumnMeta && isVirtualCol(this.lookupColumnMeta) ? () => import('@/components/project/spreadsheet/components/virtualCell') : 'div'
},
// todo : optimize
lookupApi() {
return this.column && this.$ncApis.get({
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
table: this.column.lk.ltn
})
// return this.column && this.$ncApis.get({
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias,
// table: this.column.lk.ltn
// })
},
lookUpMeta() {
return this.metas ? this.metas[this.column.lk.ltn] : this.$store.state.meta.metas[this.column.lk.ltn]
// return this.metas ? this.metas[this.column.lk.ltn] : this.$store.state.meta.metas[this.column.lk.ltn]
},
assocMeta() {
return this.column.lk.type === 'mm' && (this.metas ? this.metas[this.column.lk.vtn] : this.$store.state.meta.metas[this.column.lk.vtn])
// return this.column.lk.type === 'mm' && (this.metas ? this.metas[this.column.lk.vtn] : this.$store.state.meta.metas[this.column.lk.vtn])
},
lookUpColumnAlias() {
if (!this.lookUpMeta || !this.column.lk.lcn) {
return
}
return (this.lookUpMeta.columns.find(cl => cl.cn === this.column.lk.lcn) || {})._cn
return (this.lookUpMeta.columns.find(cl => cl.column_name === this.column.lk.lcn) || {}).title
},
lookUpColumn() {
if (!this.lookUpMeta || !this.column.lk.lcn) {
return
}
return (this.lookUpMeta.columns.find(cl => cl.cn === this.column.lk.lcn) || {})
return (this.lookUpMeta.columns.find(cl => cl.column_name === this.column.lk.lcn) || {})
},
localValueObj() {
if (!this.column || !this.row) {
return null
}
switch (this.column.lk.type) {
case 'mm':
return this.row[`${this.column.lk._ltn}MMList`]
case 'hm':
return this.row[`${this.column.lk._ltn}List`]
case 'bt':
return this.row[`${this.column.lk._ltn}Read`]
default:
return null
}
},
localValue() {
if (!this.localValueObj || !this.lookUpColumnAlias) {
return null
}
if (Array.isArray(this.localValueObj)) {
return this.localValueObj.map(o => o[this.lookUpColumnAlias])
}
return [this.localValueObj[this.lookUpColumnAlias]]
return this.value && (Array.isArray(this.value) ? this.value : [this.value])
},
queryParams() {
switch (this.column.lk.type) {
case 'bt':
return { where: `(${this.lookUpMeta.columns.find(c => c.cn === this.column.lk.rcn)._cn},eq,${this.row[this.meta.columns.find(c => c.cn === this.column.lk.cn)._cn]})` }
case 'hm':
return { where: `(${this.lookUpMeta.columns.find(c => c.cn === this.column.lk.cn)._cn},eq,${this.row[this.meta.columns.find(c => c.cn === this.column.lk.rcn)._cn]})` }
case 'mm':
return this.assocMeta
? {
conditionGraph: {
[this.assocMeta.tn]: {
relationType: 'hm',
[this.assocMeta.columns.find(c => c.cn === this.column.lk.vcn).cn]: {
eq: this.row[this.meta.columns.find(c => c.cn === this.column.lk.cn)._cn]
}
}
}
}
: {}
default:
return {}
}
}
},
created() {
this.loadLookupMeta()
this.loadLookupColumnMeta()
},
methods: {
async loadLookupColumnMeta() {
const relationColumn = this.meta.columns.find(c => c.id === this.column.colOptions.fk_relation_column_id)
this.lookupTableMeta = await this.$store.dispatch('meta/ActLoadMeta', { id: relationColumn.colOptions.fk_related_model_id })
this.lookupColumnMeta = this.lookupTableMeta.columns.find(c => c.id === this.column.colOptions.fk_lookup_column_id)
},
async loadLookupMeta() {
if (!this.metas && !this.lookUpMeta) {
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.column.lk.ltn
})
}
if (!this.metas && this.column.lk.type === 'mm' && !this.assocMeta) {
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.column.lk.vtn
})
}
},
showLookupListModal() {
this.lookupListModal = true
@ -172,24 +154,9 @@ export default {
</script>
<style scoped lang="scss">
.chips-wrapper {
.chips {
max-width: 100%;
&.lookup-items {
.wrapper{
flex-wrap: wrap;
row-gap: 3px;
gap: 3px;
margin: 3px 0;
}
}
&.active {
.chips {
max-width: calc(100% - 44px);
}
}
}
</style>
<!--
/**

144
packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue

@ -40,6 +40,7 @@
v-model="newRecordModal"
:hm="true"
:size="10"
:column="column"
:meta="childMeta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
@ -50,6 +51,7 @@
:is-public="isPublic"
:query-params="childQueryParams"
:password="password"
:row-id="row && row[parentPrimaryKey]"
@add-new-record="insertAndAddNewChildRecord"
@add="addChildToParent"
/>
@ -105,19 +107,17 @@
:db-alias="nodes.dbAlias"
:has-many="childMeta.hasMany"
:belongs-to="childMeta.belongsTo"
:table="childMeta.tn"
:table="childMeta.table_name"
:old-row="{...selectedChild}"
:meta="childMeta"
:sql-ui="sqlUi"
:primary-value-column="childPrimaryCol"
:api="childApi"
:available-columns="childAvailableColumns"
icon-color="warning"
:nodes="nodes"
:query-params="childQueryParams"
:is-new.sync="isNewChild"
:breadcrumbs="breadcrumbs"
@cancel="selectedChild = null"
@cancel="selectedChild = null; expandFormModal =false"
@input="onChildSave"
/>
</v-dialog>
@ -125,12 +125,12 @@
</template>
<script>
// import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel'
import ListItems from '@/components/project/spreadsheet/components/virtualCell/components/listItems'
import ListChildItems from '@/components/project/spreadsheet/components/virtualCell/components/listChildItems'
import listChildItemsModal
from '@/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal'
from '@/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal'
import { parseIfInteger } from '@/helpers'
import ItemChip from '~/components/project/spreadsheet/components/virtualCell/components/itemChip'
@ -187,23 +187,23 @@ export default {
}
},
childMeta() {
return this.metas ? this.metas[this.mm.rtn] : this.$store.state.meta.metas[this.mm.rtn]
return this.metas ? this.metas[this.column.colOptions.fk_related_model_id] : this.$store.state.meta.metas[this.column.colOptions.fk_related_model_id]
},
assocMeta() {
return this.metas ? this.metas[this.mm.vtn] : this.$store.state.meta.metas[this.mm.vtn]
return this.metas ? this.metas[this.column.colOptions.fk_mm_model_id] : this.$store.state.meta.metas[this.column.colOptions.fk_mm_model_id]
},
// todo : optimize
childApi() {
return this.childMeta && this.$ncApis.get({
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
table: this.childMeta.tn
})
// return this.childMeta && this.$ncApis.get({
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias,
// id: this.column.colOptions.fk_related_model_id
// })
//
// return this.childMeta && this.childMeta._tn
// return this.childMeta && this.childMeta.title
// ? ApiFactory.create(
// this.$store.getters['project/GtrProjectType'],
// this.childMeta._tn,
// this.childMeta.title,
// this.childMeta.columns,
// this,
// this.childMeta
@ -212,15 +212,15 @@ export default {
},
// todo : optimize
assocApi() {
return this.childMeta && this.$ncApis.get({
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
table: this.assocMeta.tn
})
// return this.assocMeta && this.assocMeta._tn
// return this.childMeta && this.$ncApis.get({
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias,
// id: this.column.colOptions.fk_mm_model_id
// })
// return this.assocMeta && this.assocMeta.title
// ? ApiFactory.create(
// this.$store.getters['project/GtrProjectType'],
// this.assocMeta._tn,
// this.assocMeta.title,
// this.assocMeta.columns,
// this,
// this.assocMeta
@ -228,33 +228,33 @@ export default {
// : null
},
childPrimaryCol() {
return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {})._cn
return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {}).title
},
childPrimaryKey() {
return this.childMeta && (this.childMeta.columns.find(c => c.pk) || {})._cn
return this.childMeta && (this.childMeta.columns.find(c => c.pk) || {}).title
},
parentPrimaryKey() {
return this.meta && (this.meta.columns.find(c => c.pk) || {})._cn
return this.meta && (this.meta.columns.find(c => c.pk) || {}).title
},
childQueryParams() {
if (!this.childMeta) { return {} }
// todo: use reduce
return {
hm: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.hm).map(({ hm }) => hm.tn).join()) || '',
hm: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.hm).map(({ hm }) => hm.table_name).join()) || '',
bt: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.bt).map(({ bt }) => bt.rtn).join()) || '',
mm: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.mm).map(({ mm }) => mm.rtn).join()) || ''
}
},
conditionGraph() {
if (!this.childMeta || !this.assocMeta) { return null }
return {
[this.assocMeta.tn]: {
relationType: 'hm',
[this.assocMeta.columns.find(c => c.cn === this.mm.vcn).cn]: {
eq: this.row[this.parentPrimaryKey]
}
}
}
// if (!this.childMeta || !this.assocMeta) { return null }
// return {
// [this.assocMeta.table_name]: {
// relationType: 'hm',
// [this.assocMeta.columns.find(c => c.column_name === this.mm.vcn).column_name]: {
// eq: this.row[this.parentPrimaryKey]
// }
// }
// }
},
childAvailableColumns() {
const hideCols = ['created_at', 'updated_at']
@ -262,7 +262,7 @@ export default {
const columns = []
if (this.childMeta.columns) {
columns.push(...this.childMeta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn) && !((this.childMeta.v || []).some(v => v.bt && v.bt.cn === c.cn))))
columns.push(...this.childMeta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.column_name) && !((this.childMeta.v || []).some(v => v.bt && v.bt.column_name === c.column_name))))
}
if (this.childMeta.v) {
columns.push(...this.childMeta.v.map(v => ({ ...v, virtual: 1 })))
@ -285,6 +285,10 @@ export default {
if (this.isForm) {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()])
}
if (this.isNew && this.value) {
this.localState = [...this.value]
}
},
created() {
this.loadChildMeta()
@ -310,15 +314,15 @@ export default {
return
}
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()])
const _pcn = this.meta.columns.find(c => c.cn === this.mm.cn)._cn
const _ccn = this.childMeta.columns.find(c => c.cn === this.mm.rcn)._cn
const apcn = this.assocMeta.columns.find(c => c.cn === this.mm.vcn).cn
const accn = this.assocMeta.columns.find(c => c.cn === this.mm.vrcn).cn
const id = this.assocMeta.columns.filter(c => c.cn === apcn || c.cn === accn).map(c => c.cn === apcn ? this.row[_pcn] : child[_ccn]).join('___')
await this.assocApi.delete(id)
const cid = this.childMeta.columns.filter(c => c.pk).map(c => child[c.title]).join('___')
const pid = this.meta.columns.filter(c => c.pk).map(c => this.row[c.title]).join('___')
await this.$api.data.nestedDelete(
this.meta.id,
pid,
this.column.id,
'mm',
cid
)
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData()
@ -332,7 +336,7 @@ export default {
if (act === 'hideDialog') {
this.dialogShow = false
} else {
const id = this.childMeta.columns.filter(c => c.pk).map(c => child[c._cn]).join('___')
const id = this.childMeta.columns.filter(c => c.pk).map(c => child[c.title]).join('___')
await this.childApi.delete(id)
this.dialogShow = false
this.$emit('loadTableData')
@ -348,7 +352,8 @@ export default {
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.mm.rtn
// tn: this.mm.rtn,
id: this.column.colOptions.fk_related_model_id
})
// const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
@ -365,7 +370,7 @@ export default {
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.mm.vtn
id: this.column.colOptions.fk_mm_model_id
})
// const assocTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
@ -389,16 +394,25 @@ export default {
this.newRecordModal = false
return
}
const cid = this.childMeta.columns.filter(c => c.pk).map(c => child[c._cn]).join('___')
const pid = this.meta.columns.filter(c => c.pk).map(c => this.row[c._cn]).join('___')
const cid = this.childMeta.columns.filter(c => c.pk).map(c => child[c.title]).join('___')
const pid = this.meta.columns.filter(c => c.pk).map(c => this.row[c.title]).join('___')
// const vcidCol = this.assocMeta.columns.find(c => c.id === this.column.colOptions.fk_mm_parent_column_id).title
// const vpidCol = this.assocMeta.columns.find(c => c.id === this.column.colOptions.fk_mm_child_column_id).title
await this.$api.data.nestedAdd(
this.meta.id,
pid,
this.column.id,
'mm',
cid
)
const vcidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vrcn)._cn
const vpidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vcn)._cn
try {
await this.assocApi.insert({
[vcidCol]: parseIfInteger(cid),
[vpidCol]: parseIfInteger(pid)
})
// await this.$api.data.create(this.assocMeta.id, {
// [vcidCol]: parseIfInteger(cid),
// [vpidCol]: parseIfInteger(pid)
// })
this.$emit('loadTableData')
} catch (e) {
@ -416,7 +430,15 @@ export default {
await this.loadChildMeta()
this.isNewChild = true
this.selectedChild = {
[this.childForeignKey]: this.parentId
[this.childForeignKey]: this.parentId,
[(this.childMeta.columns.find(c => c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions &&
this.column.colOptions &&
c.colOptions.fk_child_column_id === this.column.colOptions.fk_parent_column_id &&
c.colOptions.fk_parent_column_id === this.column.colOptions.fk_child_column_id &&
c.colOptions.fk_mm_model_id === this.column.colOptions.fk_mm_model_id &&
c.colOptions.type === RelationTypes.MANY_TO_MANY
) || {}).title]: [this.row]
}
this.expandFormModal = true
setTimeout(() => {
@ -438,11 +460,11 @@ export default {
while (child = this.localState.pop()) {
if (row) {
// todo: use common method
const cid = this.childMeta.columns.filter(c => c.pk).map(c => child[c._cn]).join('___')
const pid = this.meta.columns.filter(c => c.pk).map(c => row[c._cn]).join('___')
const cid = this.childMeta.columns.filter(c => c.pk).map(c => child[c.title]).join('___')
const pid = this.meta.columns.filter(c => c.pk).map(c => row[c.title]).join('___')
const vcidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vrcn)._cn
const vpidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vcn)._cn
const vcidCol = this.assocMeta.columns.find(c => c.id === this.column.colOptions.fk_mm_parent_column_id).title
const vpidCol = this.assocMeta.columns.find(c => c.id === this.column.colOptions.fk_mm_child_column_id).title
await this.assocApi.insert({
[vcidCol]: parseIfInteger(cid),
[vpidCol]: parseIfInteger(pid)

2
packages/nc-gui/components/project/spreadsheet/components/virtualCell/rollupCell.vue

@ -1,6 +1,6 @@
<template>
<span>
{{ row[column._cn] }}
{{ row[column.title] }}
</span>
</template>

158
packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue

@ -37,7 +37,7 @@
</v-icon>
</template>
<span v-on="on">
<span class="name flex-grow-1" style="white-space: nowrap" :title="column._cn" v-html="alias" />
<span class="name flex-grow-1" style="white-space: nowrap" :title="column.title" v-html="alias" />
<span v-if="column.rqd || required" class="error--text text--lighten-1">&nbsp;*</span>
</span>
</template>
@ -57,7 +57,10 @@
</v-icon>
</template>
<v-list dense>
<v-list-item dense @click="editColumnMenu = true">
<v-list-item
dense
@click="editColumnMenu = true"
>
<x-icon small class="mr-1 nc-column-edit" color="primary">
mdi-pencil
</x-icon>
@ -66,16 +69,9 @@
{{ $t('general.edit') }}
</span>
</v-list-item>
<!-- <v-list-item dense @click="setAsPrimaryValue">
<x-icon small class="mr-1" color="primary">mdi-key-star</x-icon>
<v-tooltip bottom>
<template v-slot:activator="{on}">
<span class="caption" v-on="on">Set as Primary value</span>
</template>
<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 @click="columnDeleteDialog = true">
<v-list-item
@click="columnDeleteDialog = true"
>
<x-icon small class="mr-1 nc-column-delete" color="error">
mdi-delete-outline
</x-icon>
@ -99,7 +95,7 @@
<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 />
@ -109,7 +105,12 @@
<!-- Cancel -->
{{ $t('general.cancel') }}
</v-btn>
<v-btn small color="error" @click="deleteColumn">
<v-btn
v-t="['vitual:column:delete']"
small
color="error"
@click="deleteColumn"
>
Confirm
</v-btn>
</v-card-actions>
@ -148,8 +149,8 @@ export default {
}),
computed: {
alias() {
// return this.column.lk ? `${this.column.lk._lcn} <small class="grey--text text--darken-1">(from ${this.column.lk._ltn})</small>` : this.column._cn
return this.column._cn
// return this.column.lk ? `${this.column.lk._lcn} <small class="grey--text text--darken-1">(from ${this.column.lk._ltn})</small>` : this.column.title
return this.column.title
},
type() {
if (this.column.bt) {
@ -165,10 +166,10 @@ export default {
},
childColumn() {
if (this.column.bt) {
return this.column.bt.cn
return this.column.bt.column_name
}
if (this.column.hm) {
return this.column.hm.cn
return this.column.hm.column_name
}
if (this.column.mm) {
return this.column.mm.rcn
@ -177,10 +178,10 @@ export default {
},
childTable() {
if (this.column.bt) {
return this.column.bt.tn
return this.column.bt.table_name
}
if (this.column.hm) {
return this.column.hm.tn
return this.column.hm.table_name
}
if (this.column.mm) {
return this.column.mm.rtn
@ -195,7 +196,7 @@ export default {
return this.column.hm.rtn
}
if (this.column.mm) {
return this.column.mm.tn
return this.column.mm.table_name
}
return ''
},
@ -207,7 +208,7 @@ export default {
return this.column.hm.rcn
}
if (this.column.mm) {
return this.column.mm.cn
return this.column.mm.column_name
}
return ''
},
@ -216,11 +217,11 @@ export default {
return ''
}
if (this.column.hm) {
return `'${this.column.hm._rtn}' has many '${this.column.hm._tn}'`
return `'${this.column.hm._rtn}' has many '${this.column.hm.title}'`
} else if (this.column.mm) {
return `'${this.column.mm._tn}' & '${this.column.mm._rtn}' have <br>many to many relation`
return `'${this.column.mm.title}' & '${this.column.mm._rtn}' have <br>many to many relation`
} else if (this.column.bt) {
return `'${this.column.bt._tn}' belongs to '${this.column.bt._rtn}'`
return `'${this.column.bt.title}' belongs to '${this.column.bt._rtn}'`
} else if (this.column.lk) {
return `'${this.column.lk._lcn}' from '${this.column.lk._ltn}' (${this.column.lk.type})`
} else if (this.column.formula) {
@ -232,114 +233,13 @@ export default {
}
},
methods: {
async deleteRelation() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcRelationColumnDelete', {
type: this.type,
childColumn: this.childColumn,
childTable: this.childTable,
parentTable: this.parentTable,
parentColumn: this.parentColumn,
assocTable: this.column.mm && this.column.mm.vtn
}])
this.$emit('saved')
this.columnDeleteDialog = false
} catch (e) {
console.log(e)
}
},
async deleteLookupColumn() {
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]))
// remove lookup from virtual columns
meta.v = meta.v.filter(cl => cl.cn !== this.column.cn ||
cl.type !== this.column.type ||
cl._cn !== this.column._cn ||
cl.tn !== this.column.tn)
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}])
this.$emit('saved')
this.columnDeleteDialog = false
} catch (e) {
console.log(e)
}
},
async deleteFormulaColumn() {
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]))
// remove formula from virtual columns
meta.v = meta.v.filter(cl => !cl.formula || cl._cn !== this.column._cn)
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}])
this.$emit('saved')
this.columnDeleteDialog = false
} catch (e) {
console.log(e)
}
},
async deleteRollupColumn() {
async deleteColumn() {
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]))
// remove rollup from virtual columns
meta.v = meta.v.filter(cl => !cl.rl || cl._cn !== this.column._cn)
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}])
await this.$api.dbTableColumn.delete(this.meta.id, this.column.id)
this.$emit('saved')
this.columnDeleteDialog = false
} catch (e) {
console.log(e)
}
},
async deleteColumn() {
if (this.column.lk) {
await this.deleteLookupColumn()
} else if (this.column.formula) {
await this.deleteFormulaColumn()
} else if (this.column.rl) {
await this.deleteRollupColumn()
} else {
await this.deleteRelation()
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
}
}

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

Loading…
Cancel
Save