Browse Source

chore: sync with develop

pull/1931/head
Wing-Kam Wong 2 years ago
parent
commit
2139c91268
  1. 9
      .all-contributorsrc
  2. 2
      .github/workflows/publish-docs.yml
  3. 2
      .github/workflows/release-docker.yml
  4. 4
      .github/workflows/release-nightly-dev.yml
  5. 1
      .github/workflows/release-nocodb.yml
  6. 3
      .github/workflows/release-npm.yml
  7. 1
      README.md
  8. 1051
      package-lock.json
  9. 1
      packages/nc-gui/app.html
  10. 2
      packages/nc-gui/components/createOrEditProject.vue
  11. 128
      packages/nc-gui/components/monaco/CustomMonacoEditor.js
  12. 129
      packages/nc-gui/components/monaco/MonacoHandlebarEditor.js
  13. 3
      packages/nc-gui/components/monaco/index.js
  14. 272
      packages/nc-gui/components/project/spreadsheet/components/codeSnippet.vue
  15. 4
      packages/nc-gui/components/project/spreadsheet/components/editColumn.vue
  16. 8
      packages/nc-gui/components/project/spreadsheet/components/moreActions.vue
  17. 333
      packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue
  18. 2
      packages/nc-gui/components/project/spreadsheet/mixins/cell.js
  19. 766
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  20. 7
      packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue
  21. 34
      packages/nc-gui/components/project/tableTabs/webhook/httpWebhook.vue
  22. 539
      packages/nc-gui/components/project/tableTabs/webhook/webhookEditor.vue
  23. 0
      packages/nc-gui/components/project/tableTabs/webhook/webhookEvent.vue
  24. 186
      packages/nc-gui/components/project/tableTabs/webhook/webhookList.vue
  25. 53
      packages/nc-gui/components/project/tableTabs/webhook/webhookModal.vue
  26. 35
      packages/nc-gui/components/project/tableTabs/webhook/webhooksTest.vue
  27. 423
      packages/nc-gui/components/project/tableTabs/webhooks.vue
  28. 17569
      packages/nc-gui/package-lock.json
  29. 2
      packages/nc-gui/package.json
  30. 2
      packages/nc-lib-gui/package.json
  31. 43
      packages/noco-docs/content/en/developer-resources/webhooks.md
  32. 45
      packages/noco-docs/content/en/setup-and-usages/code-snippets.md
  33. 10532
      packages/nocodb-sdk/package-lock.json
  34. 2
      packages/nocodb-sdk/package.json
  35. 5
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  36. 2
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  37. 24960
      packages/nocodb/package-lock.json
  38. 4
      packages/nocodb/package.json
  39. 17
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSqlv2.ts
  40. 6
      packages/nocodb/src/lib/dataMapper/lib/sql/helpers/getAst.ts
  41. 11
      packages/nocodb/src/lib/noco-models/Base.ts
  42. 26
      packages/nocodb/src/lib/noco-models/HookLog.ts
  43. 26
      packages/nocodb/src/lib/noco-models/Model.ts
  44. 17
      packages/nocodb/src/lib/noco-models/ProjectUser.ts
  45. 4
      packages/nocodb/src/lib/noco/meta/api/apiTokenApis.ts
  46. 58
      packages/nocodb/src/lib/noco/meta/api/columnApis.ts
  47. 8
      packages/nocodb/src/lib/noco/meta/api/filterApis.ts
  48. 5
      packages/nocodb/src/lib/noco/meta/api/formViewApis.ts
  49. 2
      packages/nocodb/src/lib/noco/meta/api/formViewColumnApis.ts
  50. 4
      packages/nocodb/src/lib/noco/meta/api/galleryViewApis.ts
  51. 2
      packages/nocodb/src/lib/noco/meta/api/gridViewApis.ts
  52. 3
      packages/nocodb/src/lib/noco/meta/api/gridViewColumnApis.ts
  53. 12
      packages/nocodb/src/lib/noco/meta/api/hookApis.ts
  54. 12
      packages/nocodb/src/lib/noco/meta/api/hookFilterApis.ts
  55. 3
      packages/nocodb/src/lib/noco/meta/api/metaDiffApis.ts
  56. 3
      packages/nocodb/src/lib/noco/meta/api/modelVisibilityApis.ts
  57. 11
      packages/nocodb/src/lib/noco/meta/api/pluginApis.ts
  58. 6
      packages/nocodb/src/lib/noco/meta/api/projectApis.ts
  59. 6
      packages/nocodb/src/lib/noco/meta/api/projectUserApis.ts
  60. 11
      packages/nocodb/src/lib/noco/meta/api/sortApis.ts
  61. 30
      packages/nocodb/src/lib/noco/meta/api/tableApis.ts
  62. 9
      packages/nocodb/src/lib/noco/meta/api/viewApis.ts
  63. 4
      packages/nocodb/src/lib/noco/meta/api/viewColumnApis.ts
  64. 12
      packages/nocodb/src/lib/noco/meta/helpers/apiMetrics.ts
  65. 4
      packages/nocodb/src/lib/noco/meta/helpers/populateSamplePayload.ts
  66. 41
      packages/nocodb/src/lib/noco/meta/helpers/webhookHelpers.ts
  67. 2
      scripts/cypress/integration/common/1a_table_operations.js
  68. 45
      scripts/cypress/integration/common/6b_downloadCsv.js
  69. 44
      scripts/cypress/support/commands.js

9
.all-contributorsrc

@ -756,6 +756,15 @@
"contributions": [
"code"
]
},
{
"login": "titouancreach",
"name": "Titouan CREACH",
"avatar_url": "https://avatars.githubusercontent.com/u/3995719?v=4",
"profile": "https://github.com/titouancreach",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

2
.github/workflows/publish-docs.yml

@ -7,6 +7,8 @@ on:
- "packages/noco-docs/**"
release:
types: [ published ]
# Triggered manually
workflow_dispatch:
jobs:
copy-file:

2
.github/workflows/release-docker.yml

@ -54,7 +54,7 @@ jobs:
DOCKER_REPOSITORY=nocodb
DOCKER_BUILD_TAG=${{ github.event.inputs.tag || inputs.tag }}
if [[ ${{ github.event.inputs.targetEnv || inputs.targetEnv }} == 'DEV' ]]; then
if [[ ${{ github.event.inputs.currentVersion || inputs.currentVersion }} != '' ]]; then
if [[ ${{ github.event.inputs.currentVersion || inputs.currentVersion || 'N/A' }} != 'N/A' ]]; then
DOCKER_BUILD_TAG=${{ github.event.inputs.currentVersion || inputs.currentVersion }}-${{ github.event.inputs.tag || inputs.tag }}
fi
if [[ ${{ inputs.isDaily || 'N' }} == 'Y' ]]; then

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

@ -18,19 +18,17 @@ jobs:
# Get current date
CURRENT_DATE=$(date +"%Y%m%d")
CURRENT_TIME=$(date +"%H%M")
TAG_NAME=${CURRENT_DATE}
TAG_NAME=${CURRENT_DATE}-${CURRENT_TIME}
IS_DAILY='Y'
# Get current version
CURRENT_VERSION=$(basename $(curl -fs -o/dev/null -w %{redirect_url} https://github.com/nocodb/nocodb/releases/latest))
# Set the tag
if [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then
TAG_NAME=${CURRENT_DATE}-${CURRENT_TIME}
IS_DAILY='N'
fi
echo "::set-output name=NIGHTLY_BUILD_TAG::${TAG_NAME}"
echo "::set-output name=IS_DAILY::${IS_DAILY}"
echo "::set-output name=CURRENT_VERSION::${CURRENT_VERSION}"
# Verify the tag
- name: verify-tag
run: |
echo ${{ steps.tag-step.outputs.NIGHTLY_BUILD_TAG }}

1
.github/workflows/release-nocodb.yml

@ -98,6 +98,7 @@ jobs:
needs: [release-draft-note, process-input]
uses: ./.github/workflows/release-docker.yml
with:
currentVersion: 'N/A'
tag: ${{ needs.process-input.outputs.target_tag }}
targetEnv: ${{ github.event.inputs.targetEnv || 'PROD' }}
secrets:

3
.github/workflows/release-npm.yml

@ -41,6 +41,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: NPM Setup and Publish with ${{ matrix.node-version }}
# Setup .npmrc file to publish to npm
uses: actions/setup-node@v2

1
README.md

@ -445,6 +445,7 @@ Our mission is to provide the most powerful no-code interface for databases whic
<td align="center"><a href="https://github.com/iamnamananand996"><img src="https://avatars.githubusercontent.com/u/31537362?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Naman Anand</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=iamnamananand996" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/GeoffMaciolek"><img src="https://avatars.githubusercontent.com/u/10995633?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Geo Maciolek</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=GeoffMaciolek" title="Code">💻</a></td>
<td align="center"><a href="http://blog.mukyu.tw/"><img src="https://avatars.githubusercontent.com/u/6008539?v=4?s=100" width="100px;" alt=""/><br /><sub><b>神楽坂帕琪</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=mudream4869" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/titouancreach"><img src="https://avatars.githubusercontent.com/u/3995719?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Titouan CREACH</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=titouancreach" title="Code">💻</a></td>
</tr>
</table>

1051
package-lock.json generated

File diff suppressed because it is too large Load Diff

1
packages/nc-gui/app.html

@ -5,6 +5,7 @@
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}
<a href="https://nocodb.com" style="display:none">Created with NocoDB</a>
</body>
<script>
setTimeout(() => {

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

@ -1541,6 +1541,8 @@ export default {
const con = projectJson.envs._noco.db[0]
if (con.client === 'pg' || con.client === 'mssql') {
con.searchPath = [this.schema]
} else if ('searchPath' in con) {
delete con.searchPath
}
const inflection = (con.meta && con.meta.inflection) || {}

128
packages/nc-gui/components/monaco/CustomMonacoEditor.js

@ -0,0 +1,128 @@
/* eslint-disable */
// import assign from "nano-assign";
// import sqlAutoCompletions from "./sqlAutoCompletions";
// import {ext} from "vee-validate/dist/rules.esm";
export default {
name: "CustomMonacoEditor",
props: {
value: {
default: "",
type: String
},
theme: {
type: String,
default: "vs-dark"
},
lang: {type:String, default: 'typescript'},
readOnly:Boolean,
minimap:Boolean,
},
model: {
event: "change"
},
watch: {
value(newVal) {
if (newVal !== this.editor.getValue()) {
if (typeof newVal === 'object') {
this.editor.setValue(JSON.stringify(newVal, 0, 2));
} else {
this.editor.setValue(newVal);
}
}
}
},
mounted() {
this.$nextTick(() => {
if (this.amdRequire) {
this.amdRequire(["vs/editor/editor.main"], () => {
this.initMonaco(window.monaco);
});
} else {
// ESM format so it can't be resolved by commonjs `require` in eslint
// eslint-disable import/no-unresolved
const monaco = require("monaco-editor");
// monaco.editor.defineTheme('monokai', require('./Cobalt.json'))
// monaco.editor.setTheme('monokai')
this.monaco = monaco;
this.initMonaco(monaco);
}
});
},
unmounted() {
},
beforeDestroy() {
this.editor && this.editor.dispose();
},
methods: {
resizeLayout() {
this.editor.layout();
},
initMonaco(monaco) {
const code = this.value;
const model = monaco.editor.createModel(code, this.lang ||"json");
this.editor = monaco.editor.create(this.$el, {
model: model,
theme: this.theme, minimap: {
enabled: this.minimap
}
});
this.editor.onDidChangeModelContent(event => {
const value = this.editor.getValue();
if (this.value !== value) {
this.$emit("change", value, event);
}
});
this.editor.updateOptions({ readOnly: this.readOnly })
},
getMonaco() {
return this.editor;
},
getMonacoModule() {
return this.monaco;
},
},
render(h) {
return h("div");
},
created() {
},
destroyed() {
}
};
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

129
packages/nc-gui/components/monaco/MonacoHandlebarEditor.js

@ -0,0 +1,129 @@
/* eslint-disable */
// import assign from "nano-assign";
// import sqlAutoCompletions from "./sqlAutoCompletions";
// import {ext} from "vee-validate/dist/rules.esm";
export default {
name: "MonacoHandlebarEditor",
props: {
value: {
default: "",
type: String
},
theme: {
type: String,
default: "vs-dark"
},
},
model: {
event: "change"
},
watch: {
value(newVal) {
if (newVal !== this.editor.getValue()) {
if (typeof newVal === 'object') {
this.editor.setValue(JSON.stringify(newVal, 0, 2));
} else {
this.editor.setValue(newVal);
}
}
}
},
mounted() {
this.$nextTick(() => {
if (this.amdRequire) {
this.amdRequire(["vs/editor/editor.main"], () => {
this.initMonaco(window.monaco);
});
} else {
// ESM format so it can't be resolved by commonjs `require` in eslint
// eslint-disable import/no-unresolved
const monaco = require("monaco-editor");
// monaco.editor.defineTheme('monokai', require('./Cobalt.json'))
// monaco.editor.setTheme('monokai')
this.monaco = monaco;
// this.completionItemProvider = monaco.languages.registerCompletionItemProvider("sql", {
// async provideCompletionItems(model, position) {
// // console.log(sqlAutoCompletions(monaco).actions[0])
// console.log(model === vm.editor,model,vm.editor)
// return model === vm.editor.getModel() ? {suggestions: await vm.getLiveSuggestionsList(model, position)} : {};
// }
// });
this.initMonaco(monaco);
}
});
},
unmounted() {
},
beforeDestroy() {
this.editor && this.editor.dispose();
},
methods: {
initMonaco(monaco) {
const typescriptCode = this.value;
const model = monaco.editor.createModel(typescriptCode);
this.editor = monaco.editor.create(this.$el, {
model: model,
theme: this.theme
});
this.editor.onDidChangeModelContent(event => {
const value = this.editor.getValue();
if (this.value !== value) {
this.$emit("change", value, event);
}
});
},
getMonaco() {
return this.editor;
},
getMonacoModule() {
return this.monaco;
},
},
render(h) {
return h("div");
},
created() {
},
destroyed() {
}
};
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

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

@ -1,10 +1,11 @@
import MonacoEditor from './MonacoEditor'
import MonacoJsonEditor from './MonacoJsonEditor'
import MonacoSingleLineEditor from './MonacoSingleLineEditor'
import MonacoHandlebarEditor from './MonacoHandlebarEditor'
export default MonacoEditor
export { MonacoJsonEditor, MonacoSingleLineEditor }
export { MonacoJsonEditor, MonacoSingleLineEditor, MonacoHandlebarEditor }
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.component(MonacoEditor.name, MonacoEditor)

272
packages/nc-gui/components/project/spreadsheet/components/codeSnippet.vue

@ -0,0 +1,272 @@
<template>
<div class="nc-container" :class="{active:modal}" @click="modal=false">
<div class="nc-snippet elevation-3 pa-4" @click.stop>
<h3 class="font-weight-medium mb-4">
Code Snippet
</h3>
<v-icon class="nc-snippet-close" @click="modal=false">
mdi-close
</v-icon>
<div v-if="modal">
<v-tabs v-model="tab" height="30" show-arrows @change="client=null">
<v-tab
v-for="{lang} in langs"
:key="lang"
v-t="['c:snippet:tab', {lang}]"
class="caption"
>
{{ lang }}
</v-tab>
</v-tabs>
<div class="nc-snippet-wrapper mt-4">
<div class="nc-snippet-actions d-flex">
<v-btn
v-t="['c:snippet:copy', {client: langs[tab].clients && (client || langs[tab].clients[0]), lang: langs[tab ||0].lang}]"
color="primary"
class="rounded caption "
@click="copyToClipboard"
>
<v-icon small>
mdi-clipboard-outline
</v-icon>
Copy To Clipboard
</v-btn>
<div
v-if="langs[tab].clients"
class=" ml-2 d-flex align-center"
>
<v-menu bottom offset-y>
<template #activator="{on}">
<v-btn class="caption text-uppercase" color="primary" v-on="on">
{{ client || langs[tab].clients[0] }}
<v-icon small>
mdi-chevron-down
</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item
v-for="c in langs[tab].clients"
:key="c"
dense
@click="client = c"
>
<v-list-item-title class="text-uppercase">
{{ c }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</div>
<custom-monaco-editor
hide-line-num
:theme="$store.state.windows.darkTheme ? 'vs-dark' : 'vs-light'"
style="min-height:500px;max-width: 100%"
:value="code"
read-only
/>
</div>
</div>
</div>
</div>
</template>
<script>
import HTTPSnippet from 'httpsnippet'
import CustomMonacoEditor from '~/components/monaco/CustomMonacoEditor'
import { copyTextToClipboard } from '~/helpers/xutils'
export default {
name: 'CodeSnippet',
components: { CustomMonacoEditor },
props: {
meta: Object,
view: Object,
filters: [Object, Array],
sorts: [Object, Array],
fileds: [Object, Array],
queryParams: Object,
value: Boolean
},
data: () => ({
tab: 0,
client: null,
langs: [
{
lang: 'shell',
clients: ['curl', 'wget']
},
{
lang: 'javascript',
clients: ['axios', 'fetch', 'jquery', 'xhr']
},
{
lang: 'node',
clients: ['axios', 'fetch', 'request', 'native', 'unirest']
},
{
lang: 'nocodb-sdk',
clients: ['javascript', 'node']
},
{
lang: 'php'
},
{
lang: 'python',
clients: ['python3',
'requests']
},
{
lang: 'ruby'
},
{
lang: 'java'
},
{
lang: 'c'
}
]
}),
computed: {
modal: {
get() {
return this.value
},
set(v) {
this.$emit('input', v)
}
},
apiUrl() {
return new URL(`/api/v1/db/data/noco/${this.projectId}/${this.meta.title}/views/${this.view.title}`, (this.$store.state.project.projectInfo && this.$store.state.project.projectInfo.ncSiteUrl) || '/').href
},
snippet() {
return new HTTPSnippet({
method: 'GET',
headers: [
{ name: 'xc-auth', value: this.$store.state.users.token, comment: 'JWT Auth token' }
],
url: this.apiUrl,
queryString: Object.entries(this.queryParams || {}).map(([name, value]) => {
return {
name, value: String(value)
}
})
})
},
code() {
if (this.langs[this.tab].lang === 'nocodb-sdk') {
return `${
this.client === 'node'
? 'const { Api } require("nocodb-sdk");'
: 'import { Api } from "nocodb-sdk";'
}
const api = new Api({
baseURL: ${JSON.stringify(this.apiUrl)},
headers: {
"xc-auth": ${JSON.stringify(this.$store.state.users.token)}
}
})
api.dbViewRow.list(
"noco",
${JSON.stringify(this.projectName)},
${JSON.stringify(this.meta.title)},
${JSON.stringify(this.view.title)}, ${JSON.stringify(this.queryParams, null, 4)}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.error(error);
});`
}
return this.snippet.convert(this.langs[this.tab].lang, this.client || (this.langs[this.tab].clients && this.langs[this.tab].clients[0]), {})
}
},
mounted() {
(document.querySelector('[data-app]') || this.$root.$el).append(this.$el)
},
destroyed() {
this.$el.parentNode && this.$el.parentNode.removeChild(this.$el)
},
methods: {
copyToClipboard() {
copyTextToClipboard(this.code)
this.$toast.success('Code copied to clipboard successfully.').goAway(3000)
}
}
}
</script>
<style scoped lang="scss">
.nc-snippet-wrapper {
position: relative;
border: 1px solid #7773;
border-radius: 4px;
overflow: hidden;
}
.nc-snippet-actions {
position: absolute;
right: 10px;
bottom: 10px;
z-index: 99999;
}
.nc-container {
position: fixed;
pointer-events: none;
width: 100vw;
height: 100vh;
z-index: 9999;
right: 0;
top: 0;
.nc-snippet {
background-color: var(--v-backgroundColorDefault-base);
height: 100%;
width: max(50%, 700px);
position: absolute;
bottom: 0;
top: 0;
right: min(-50%, -700px);
transition: .3s right;
}
&.active {
pointer-events: all;
& > .nc-snippet {
right: 0
}
}
.nc-snippet-close {
position: absolute;
right: 16px;
top: 16px;
}
}
::v-deep {
.v-tabs {
height: 100%;
.v-tabs-items {
height: calc(100% - 30px);
.v-window__container {
height: 100%;
}
}
}
.v-slide-group__prev--disabled {
display: none
}
}
</style>

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

@ -693,6 +693,10 @@ export default {
);
} catch (e) {
console.log(e);
this.$toast
.error("Failed to save column: " + e)
.goAway(3000);
throw e;
}
this.$emit("close");

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

@ -82,7 +82,7 @@
v-if="_isUIAllowed('webhook') && !isView"
v-t="['c:actions:webhook']"
dense
@click="$emit('webhook')"
@click="webhookModal = true"
>
<v-list-item-title>
<v-icon small class="mr-1" color="">
@ -104,6 +104,7 @@
:parsed-csv="parsedCsv"
@import="importData"
/>
<webhook-modal v-model="webhookModal" :meta="meta" />
</div>
</template>
@ -115,10 +116,12 @@ import DropOrSelectFileModal from '~/components/import/dropOrSelectFileModal'
import ColumnMappingModal from '~/components/project/spreadsheet/components/importExport/columnMappingModal'
import CSVTemplateAdapter from '~/components/import/templateParsers/CSVTemplateAdapter'
import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes'
import WebhookModal from '~/components/project/tableTabs/webhook/webhookModal'
export default {
name: 'ExportImport',
components: {
WebhookModal,
ColumnMappingModal,
DropOrSelectFileModal
},
@ -135,7 +138,8 @@ export default {
return {
importModal: false,
columnMappingModal: false,
parsedCsv: {}
parsedCsv: {},
webhookModal: false
}
},

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

@ -61,7 +61,9 @@
>
{{ viewIcons[view.type].icon }}
</v-icon>
<v-icon v-else color="primary" small> mdi-table </v-icon>
<v-icon v-else color="primary" small>
mdi-table
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<v-tooltip bottom>
@ -77,7 +79,7 @@
@click.stop
@keydown.enter.stop="updateViewName(view, i)"
@blur="updateViewName(view, i)"
/>
>
<template v-else>
<span v-on="on">{{
view.alias || view.title
@ -183,7 +185,9 @@
@click="openCreateViewDlg(viewTypes.GRID)"
>
<v-list-item-icon class="mr-n1">
<v-icon color="blue" x-small> mdi-grid-large </v-icon>
<v-icon color="blue" x-small>
mdi-grid-large
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<span class="font-weight-regular">
@ -192,7 +196,9 @@
</span>
</v-list-item-title>
<v-spacer />
<v-icon class="mr-1" small> mdi-plus </v-icon>
<v-icon class="mr-1" small>
mdi-plus
</v-icon>
</v-list-item>
</template>
<!-- Add Grid View -->
@ -207,7 +213,9 @@
@click="openCreateViewDlg(viewTypes.GALLERY)"
>
<v-list-item-icon class="mr-n1">
<v-icon color="orange" x-small> mdi-camera-image </v-icon>
<v-icon color="orange" x-small>
mdi-camera-image
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<span class="font-weight-regular">
@ -217,7 +225,9 @@
</v-list-item-title>
<v-spacer />
<v-icon class="mr-1" small> mdi-plus </v-icon>
<v-icon class="mr-1" small>
mdi-plus
</v-icon>
</v-list-item>
</template>
<!-- Add Gallery View -->
@ -251,7 +261,9 @@
</v-list-item-title>
<v-spacer />
<v-icon class="mr-1" small> mdi-plus </v-icon>
<v-icon class="mr-1" small>
mdi-plus
</v-icon>
</v-list-item>
</template>
<!-- Add Form View -->
@ -261,6 +273,20 @@
</template>
</div>
<div>
<v-btn
v-t="['c:snippet:open']"
color="primary"
class="caption d-100"
@click="codeSnippetModal=true"
>
<v-icon small class="mr-2">
mdi-xml
</v-icon> Get API Snippet
</v-btn>
<code-snippet v-model="codeSnippetModal" :query-params="queryParams" :meta="meta" :view="selectedView" />
</div>
<div
v-if="time - $store.state.windows.miniSponsorCard > 15 * 60 * 1000"
class="pa-2 sponsor-wrapper"
@ -270,10 +296,10 @@
</v-icon>
<!-- <extras />-->
<v-divider/>
<extras class="pl-1"/>
<v-divider />
<extras class="pl-1" />
<!-- <sponsor-mini nav />-->
<!-- <sponsor-mini nav />-->
</div>
<!--<div class="text-center">
<v-hover >
@ -490,17 +516,18 @@
</template>
<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";
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'
import CodeSnippet from '~/components/project/spreadsheet/components/codeSnippet'
export default {
name: "SpreadsheetNavDrawer",
components: { SponsorMini, Extras, CreateViewDialog, draggable },
name: 'SpreadsheetNavDrawer',
components: { CodeSnippet, SponsorMini, Extras, CreateViewDialog, draggable },
props: {
extraViewParams: Object,
showAdvanceOptions: Boolean,
@ -509,7 +536,7 @@ export default {
primaryValueColumn: [Number, String],
toggleDrawer: {
type: Boolean,
default: false,
default: false
},
nodes: Object,
table: String,
@ -523,7 +550,7 @@ export default {
sortList: [Object, Array],
load: {
default: true,
type: Boolean,
type: Boolean
},
currentApiUrl: String,
fieldsOrder: Array,
@ -533,22 +560,24 @@ export default {
groupingField: String,
// showSystemFields: Boolean,
views: Array,
queryParams: Object
},
data: () => ({
codeSnippetModal: false,
drag: false,
dragOptions: {
animation: 200,
group: "description",
group: 'description',
disabled: false,
ghostClass: "ghost",
ghostClass: 'ghost'
},
time: Date.now(),
sponsorMiniVisible: true,
enableDummyFeat: false,
searchQueryVal: "",
searchQueryVal: '',
showShareLinkPassword: false,
passwordProtect: false,
sharedViewPassword: "",
sharedViewPassword: '',
overAdvShieldIcon: false,
overShieldIcon: false,
viewIcons,
@ -558,120 +587,120 @@ export default {
showCreateView: false,
loading: false,
viewTypeAlias: {
[ViewTypes.GRID]: "grid",
[ViewTypes.FORM]: "form",
[ViewTypes.GALLERY]: "gallery",
},
[ViewTypes.GRID]: 'grid',
[ViewTypes.FORM]: 'form',
[ViewTypes.GALLERY]: 'gallery'
}
}),
computed: {
viewsList: {
set(v) {
this.$emit("update:views", v);
this.$emit('update:views', v)
},
get() {
return this.views;
},
return this.views
}
},
viewTypes() {
return ViewTypes;
return ViewTypes
},
newViewParams() {
if (!this.showFields) {
return {};
return {}
}
const showFields = { ...this.showFields };
const showFields = { ...this.showFields }
Object.keys(showFields).forEach((k) => {
showFields[k] = true;
});
return { showFields };
showFields[k] = true
})
return { showFields }
},
selectedViewIdLocal: {
set(val) {
const view = (this.views || []).find((v) => v.id === val);
const view = (this.views || []).find(v => v.id === val)
this.$router.push({
query: {
...this.$route.query,
view: view && view.id,
},
});
view: view && view.id
}
})
},
get() {
let id;
let 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;
const view = this.views.find(v => v.id === this.$route.query.view)
id = (view && view.id) || ((this.views && this.views[0]) || {}).id
}
return id;
},
return id
}
},
sharedViewUrl() {
let viewType;
let viewType
switch (this.shareLink.type) {
case this.viewTypes.FORM:
viewType = "form";
break;
viewType = 'form'
break
case this.viewTypes.KANBAN:
viewType = "kanban";
break;
viewType = 'kanban'
break
default:
viewType = "view";
viewType = 'view'
}
return `${this.dashboardUrl}#/nc/${viewType}/${this.shareLink.uuid}`;
},
return `${this.dashboardUrl}#/nc/${viewType}/${this.shareLink.uuid}`
}
},
watch: {
async load(v) {
if (v) {
await this.loadViews();
this.onViewIdChange(this.selectedViewIdLocal);
await this.loadViews()
this.onViewIdChange(this.selectedViewIdLocal)
}
},
selectedViewIdLocal(id) {
this.onViewIdChange(id);
},
this.onViewIdChange(id)
}
},
async created() {
if (this.load) {
await this.loadViews();
await this.loadViews()
}
this.onViewIdChange(this.selectedViewIdLocal);
this.onViewIdChange(this.selectedViewIdLocal)
},
methods: {
async onMove(event) {
if (this.viewsList.length - 1 === event.moved.newIndex) {
this.$set(
this.viewsList[event.moved.newIndex],
"order",
'order',
this.viewsList[event.moved.newIndex - 1].order + 1
);
)
} else if (event.moved.newIndex === 0) {
this.$set(
this.viewsList[event.moved.newIndex],
"order",
'order',
this.viewsList[1].order / 2
);
)
} else {
this.$set(
this.viewsList[event.moved.newIndex],
"order",
'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,
});
order: this.viewsList[event.moved.newIndex].order
})
this.$e("a:view:reorder");
this.$e('a:view:reorder')
},
onViewIdChange(id) {
const selectedView = this.views && this.views.find((v) => v.id === id);
const selectedView = this.views && this.views.find(v => v.id === id)
// const queryParams = {}
this.$emit("update:selectedViewId", id);
this.$emit("update:selectedView", selectedView);
this.$emit('update:selectedViewId', id)
this.$emit('update:selectedView', selectedView)
// if (selectedView.type === 'table') {
// return;
// }
@ -694,52 +723,52 @@ export default {
// } else {
// this.$emit('mapFieldsAndShowFields')
// }
this.$emit("loadTableData");
this.$emit('loadTableData')
},
hideMiniSponsorCard() {
this.$store.commit("windows/MutMiniSponsorCard", Date.now());
this.$store.commit('windows/MutMiniSponsorCard', Date.now())
},
openCreateViewDlg(type) {
const mainView = this.viewsList.find(
(v) => v.type === "table" || v.type === "view"
);
v => v.type === 'table' || v.type === 'view'
)
try {
this.copyViewRef = this.copyViewRef || {
query_params: JSON.stringify({
...this.newViewParams,
fieldsOrder: JSON.parse(mainView.query_params).fieldsOrder,
}),
};
fieldsOrder: JSON.parse(mainView.query_params).fieldsOrder
})
}
} catch {}
this.createViewType = type;
this.showCreateView = true;
this.$e("c:view:create", { view: type });
this.createViewType = type
this.showCreateView = true
this.$e('c:view:create', { view: type })
},
isCentrallyAligned(col) {
return ![
"SingleLineText",
"LongText",
"Attachment",
"Date",
"Time",
"Email",
"URL",
"DateTime",
"CreateTime",
"LastModifiedTime",
].includes(col.uidt);
'SingleLineText',
'LongText',
'Attachment',
'Date',
'Time',
'Email',
'URL',
'DateTime',
'CreateTime',
'LastModifiedTime'
].includes(col.uidt)
},
onPasswordProtectChange() {
if (!this.passwordProtect) {
this.shareLink.password = null;
this.saveShareLinkPassword();
this.shareLink.password = null
this.saveShareLinkPassword()
}
},
async saveShareLinkPassword() {
try {
await this.$api.dbViewShare.update(this.shareLink.id, {
password: this.shareLink.password,
});
password: this.shareLink.password
})
// await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { dbAlias: this.nodes.dbAlias },
@ -749,14 +778,14 @@ export default {
// password: this.shareLink.password
// }
// ])
this.$toast.success("Successfully updated").goAway(3000);
this.$toast.success('Successfully updated').goAway(3000)
} catch (e) {
this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000);
.goAway(3000)
}
this.$e("a:view:share:enable-pwd");
this.$e('a:view:share:enable-pwd')
},
async loadViews() {
// this.viewsList = await this.sqlOp(
@ -772,8 +801,8 @@ export default {
// this.viewsList = []
const views = (await this.$api.dbView.list(this.meta.id)).list;
this.$emit("update:views", views);
const views = (await this.$api.dbView.list(this.meta.id)).list
this.$emit('update:views', views)
},
// async onViewChange() {
// let query_params = {}
@ -793,27 +822,27 @@ export default {
// this.$emit('loadTableData');
// },
copyapiUrlToClipboard() {
copyTextToClipboard(this.currentApiUrl);
this.clipboardSuccessHandler();
copyTextToClipboard(this.currentApiUrl)
this.clipboardSuccessHandler()
},
async updateViewName(view, index) {
if (!view.edit) {
return;
return
}
// const oldTitle = view.title
this.$set(view, "edit", false);
this.$set(view, 'edit', false)
if (view.title_temp === view.title) {
return;
return
}
if (
this.viewsList.some(
(v, i) => i !== index && (v.alias || v.title) === view.title_temp
)
) {
this.$toast.info("View name should be unique").goAway(3000);
return;
this.$toast.info('View name should be unique').goAway(3000)
return
}
try {
// if (this.selectedViewIdLocal === view.id) {
@ -824,39 +853,39 @@ export default {
// }
// })
// }
this.$set(view, "title", view.title_temp);
this.$set(view, 'title', view.title_temp)
await this.$api.dbView.update(view.id, {
title: view.title,
order: view.order,
});
this.$toast.success("View renamed successfully").goAway(3000);
order: view.order
})
this.$toast.success('View renamed successfully').goAway(3000)
} catch (e) {
this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000);
.goAway(3000)
}
},
showRenameTextBox(view, i) {
this.$set(view, "edit", true);
this.$set(view, "title_temp", view.title);
this.$set(view, 'edit', true)
this.$set(view, 'title_temp', view.title)
this.$nextTick(() => {
const input = this.$refs[`input${i}`][0];
input.focus();
input.setSelectionRange(0, input.value.length);
});
this.$e("c:view:rename", { view: view.type });
const input = this.$refs[`input${i}`][0]
input.focus()
input.setSelectionRange(0, input.value.length)
})
this.$e('c:view:rename', { view: view.type })
},
async deleteView(view) {
try {
await this.$api.dbView.delete(view.id);
this.$toast.success("View deleted successfully").goAway(3000);
await this.loadViews();
await this.$api.dbView.delete(view.id)
this.$toast.success('View deleted successfully').goAway(3000)
await this.loadViews()
} catch (e) {
this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000);
.goAway(3000)
}
this.$e("a:view:delete", { view: view.type });
this.$e('a:view:delete', { view: view.type })
},
async genShareLink() {
// const sharedViewUrl = await this.$store.dispatch('sqlMgr/ActSqlOp', [
@ -883,44 +912,44 @@ export default {
// password: this.sharedViewPassword
// }
// ])
const shared = await this.$api.dbViewShare.create(this.selectedViewId);
const shared = await this.$api.dbViewShare.create(this.selectedViewId)
// todo: url
this.shareLink = shared;
this.showShareModel = true;
this.shareLink = shared
this.showShareModel = true
},
copyView(view, i) {
this.createViewType = view.type;
this.showCreateView = true;
this.copyViewRef = view;
this.$e("c:view:copy", { view: view.type });
this.createViewType = view.type
this.showCreateView = true
this.copyViewRef = view
this.$e('c:view:copy', { view: view.type })
},
async onViewCreate(viewMeta) {
this.copyViewRef = null;
await this.loadViews();
this.selectedViewIdLocal = viewMeta.id;
this.copyViewRef = null
await this.loadViews()
this.selectedViewIdLocal = viewMeta.id
// await this.onViewChange();
this.$e("a:view:create", { view: viewMeta.type });
this.$e('a:view:create', { view: viewMeta.type })
},
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);
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)
},
clipboardSuccessHandler() {
this.$toast.info("Copied to clipboard").goAway(1000);
this.$toast.info('Copied to clipboard').goAway(1000)
},
copyShareUrlToClipboard() {
this.clipboard(this.sharedViewUrl);
this.clipboardSuccessHandler();
this.$e("c:view:share:copy-url");
},
},
};
this.clipboard(this.sharedViewUrl)
this.clipboardSuccessHandler()
this.$e('c:view:share:copy-url')
}
}
}
</script>
<style scoped lang="scss">

2
packages/nc-gui/components/project/spreadsheet/mixins/cell.js

@ -45,7 +45,7 @@ export default {
return this.abstractType === 'datetime' || this.uiDatatype === 'DateTime'
},
isJSON() {
return this.abstractType === 'json' || this.uiDatatype === 'JSON'
return this.uiDatatype === 'JSON'
},
isEnum() {
return this.uiDatatype === 'SingleSelect'

766
packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue

File diff suppressed because it is too large Load Diff

7
packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue

@ -729,9 +729,12 @@ export default {
},
onClickOutside() {
if (
this.meta.columns &&
(this.meta.columns &&
this.meta.columns[this.selected.col] &&
this.meta.columns[this.selected.col].virtual
this.meta.columns[this.selected.col].virtual) ||
(this.availableColumns &&
this.availableColumns[this.editEnabled.col] &&
this.availableColumns[this.editEnabled.col].uidt === "JSON")
) {
return;
}

34
packages/nc-gui/components/project/tableTabs/webhook/httpWebhook.vue

@ -23,6 +23,9 @@
class="req-tabs"
height="24"
>
<v-tab v-ge="['api-client','body']" class="caption">
<span class="text-capitalize">Body</span>
</v-tab>
<v-tab v-ge="['api-client','params']" class="caption">
<span class="text-capitalize"> Params&nbsp;<b
v-if="paramsCount"
@ -37,12 +40,19 @@
headersCount
}})</b></span>
</v-tab>
<v-tab v-ge="['api-client','body']" class="caption">
<span class="text-capitalize">Body</span>
</v-tab>
<v-tab v-ge="['api-client','auth']" class="caption">
<span class="text-capitalize">Auth</span>
</v-tab>
<v-tab-item>
<monaco-handlebar-editor
v-model="api.body"
style="height: 250px"
class="editor card text-left"
theme="vs-dark"
lang="json"
:options="{validate:true,documentFormattingEdits:true,foldingRanges:true}"
/>
</v-tab-item>
<v-tab-item>
<params
v-model="api.parameters"
@ -55,23 +65,13 @@
:env.sync="selectedEnv"
/>
</v-tab-item>
<v-tab-item>
<monaco-json-editor
v-model="api.body"
style="height: 250px"
class="editor card text-left"
theme="vs-dark"
lang="json"
:options="{validate:true,documentFormattingEdits:true,foldingRanges:true}"
/>
</v-tab-item>
<v-tab-item>
<monaco-json-editor
<monaco-handlebar-editor
v-model="api.auth"
style="height: 250px"
class="editor card text-left"
theme="vs-dark"
lang="json"
:options="{validate:true,documentFormattingEdits:true,foldingRanges:true}"
/>
<span class="caption grey--text">For more about auth option refer <a href="https://github.com/axios/axios#request-config" target="_blank">axios docs</a>.</span>
@ -84,7 +84,7 @@
import params from '../../../apiClient/params'
import headers from '../../../apiClient/headers'
import { MonacoJsonEditor } from '../../../monaco/index'
import { MonacoHandlebarEditor } from '../../../monaco/index'
export default {
tab: 0,
@ -92,7 +92,7 @@ export default {
components: {
params,
headers,
MonacoJsonEditor
MonacoHandlebarEditor
},
props: {
value: Object

539
packages/nc-gui/components/project/tableTabs/webhook/webhookEditor.vue

@ -0,0 +1,539 @@
<template>
<v-form v-if="hook" ref="form" v-model="valid" class="mx-4" lazy-validation>
<v-card-title>
<a class="pointer mr-1" @click="$emit('backToList')">
<v-icon>mdi-arrow-left-bold</v-icon>
</a>
<v-spacer />
{{ meta.title }} : {{ hook.title || 'Webhook' }}
<v-spacer />
<div style="width: 24px;height: 24px" />
</v-card-title>
<div class="mx-4 d-flex m-2">
<v-spacer />
<v-btn
outlined
tooltip="Save"
small
:disabled="loading || !valid || !hook.event"
@click.prevent="$refs.webhookTest.testWebhook()"
>
Test webhook
</v-btn>
<v-btn
outlined
tooltip="Save"
color="primary"
small
:disabled="loading || !valid || !hook.event"
@click.prevent="saveHooks"
>
<v-icon small left>
save
</v-icon>
<!-- Save -->
{{ $t("general.save") }}
</v-btn>
</div>
<v-card-text>
<v-text-field
v-model="hook.title"
class="caption"
outlined
dense
:label="$t('general.title')"
required
:rules="[(v) => !!v || `${$t('general.required')}`]"
/>
<webhook-event
:event.sync="hook.event"
:operation.sync="hook.operation"
/>
<v-card class="mb-8 nc-filter-wrapper">
<v-card-text>
<v-checkbox
v-model="hook.condition"
dense
hide-details
class="mt-1"
label="On Condition"
/>
<column-filter
v-if="hook.condition"
:key="key"
ref="filter"
v-model="filters"
:meta="meta"
:field-list="fieldList"
dense
style="max-width: 100%"
:hook-id="hook.id"
web-hook
/>
</v-card-text>
</v-card>
<v-select
v-model="hook.notification.type"
outlined
dense
:label="$t('general.notification')"
required
:items="notificationList"
:rules="[(v) => !!v || `${$t('general.required')}`]"
class="caption"
:prepend-inner-icon="notificationIcon[hook.notification.type]"
@change="onNotTypeChange"
>
<template #item="{ item }">
<v-list-item-icon>
<v-icon small>
{{ notificationIcon[item] }}
</v-icon>
</v-list-item-icon>
<v-list-item-title>
{{ item }}
</v-list-item-title>
</template>
</v-select>
<template v-if="hook.notification.type === 'URL'">
<http-webhook v-model="notification" />
</template>
<template v-if="hook.notification.type === 'Slack'">
<v-combobox
v-if="slackChannels"
v-model="notification.channels"
:rules="[(v) => !!v || `${$t('general.required')}`]"
:items="slackChannels"
item-text="channel"
label="Select Slack channels"
multiple
outlined
dense
class="caption"
/>
</template>
<template v-if="hook.notification.type === 'Microsoft Teams'">
<v-combobox
v-if="teamsChannels"
v-model="notification.channels"
:rules="[(v) => !!v || `${$t('general.required')}`]"
:items="teamsChannels"
item-text="channel"
label="Select Teams channels"
multiple
outlined
dense
class="caption"
/>
</template>
<template v-if="hook.notification.type === 'Discord'">
<v-combobox
v-if="discordChannels"
v-model="notification.channels"
:rules="[(v) => !!v || `${$t('general.required')}`]"
:items="discordChannels"
item-text="channel"
label="Select Discord channels"
multiple
outlined
dense
class="caption"
/>
</template>
<template v-if="hook.notification.type === 'Mattermost'">
<v-combobox
v-if="mattermostChannels"
v-model="notification.channels"
:rules="[(v) => !!v || `${$t('general.required')}`]"
:items="mattermostChannels"
item-text="channel"
label="Select Mattermost channels"
multiple
outlined
dense
class="caption"
/>
</template>
<template v-if="inputs[hook.notification.type] && notification">
<template v-for="input in inputs[hook.notification.type]">
<v-textarea
v-if="input.type === 'LongText'"
:key="input.key"
v-model="notification[input.key]"
class="caption"
dense
outlined
:label="input.label"
:rules="[
(v) =>
!input.required || !!v || `${$t('general.required')}`,
]"
/>
<v-text-field
v-else
:key="input.key"
v-model="notification[input.key]"
class="caption"
dense
outlined
:label="input.label"
:rules="[
(v) =>
!input.required || !!v || `${$t('general.required')}`,
]"
/>
</template>
</template>
</v-card-text>
<v-card-text>
<span class="caption grey--text">
<em>Available context variables are
<strong>data and user</strong></em>
<v-tooltip top>
<template #activator="{ on }">
<v-icon
small
color="grey"
class="ml-2"
v-on="on"
>mdi-information</v-icon>
</template>
<span class="caption">
<strong>data</strong> : Row data <br>
<strong>user</strong> : User information<br>
</span>
</v-tooltip>
<br>
<a
href="https://docs.nocodb.com/developer-resources/webhooks/"
>
<!--Document Reference-->
{{ $t("labels.docReference") }}
</a>
</span>
<webhooks-test
ref="webhookTest"
class="mt-3"
:model-id="meta.id"
hide-test-btn
:hook="{
...hook,
filters,
notification: {
...hook.notification,
payload: notification,
},
}"
/>
</v-card-text>
</v-form>
<span v-else />
</template>
<script>
import WebhooksTest from '~/components/project/tableTabs/webhook/webhooksTest'
import WebhookEvent from '~/components/project/tableTabs/webhook/webhookEvent'
import HttpWebhook from '~/components/project/tableTabs/webhook/httpWebhook'
import ColumnFilter from '~/components/project/spreadsheet/components/columnFilter'
export default {
name: 'WebhookEditor',
components: { ColumnFilter, HttpWebhook, WebhookEvent, WebhooksTest },
props: {
meta: Object
},
data: () => ({
notification: {},
hook: {
notification: {
type: 'URL'
}
},
valid: false,
apps: {},
slackChannels: null,
teamsChannels: null,
discordChannels: null,
mattermostChannels: null,
enableCondition: false,
notificationList: [
'URL',
'Email',
'Slack',
'Microsoft Teams',
'Discord',
'Mattermost',
'Twilio',
'Whatsapp Twilio'
],
filters: [],
notificationIcon: {
URL: 'mdi-link',
Email: 'mdi-email',
Slack: 'mdi-slack',
'Microsoft Teams': 'mdi-microsoft-teams',
Discord: 'mdi-discord',
Mattermost: 'mdi-chat',
'Whatsapp Twilio': 'mdi-whatsapp',
Twilio: 'mdi-cellphone-message'
},
inputs: {
Email: [
{
key: 'to',
label: 'To Address',
placeholder: 'To Address',
type: 'SingleLineText',
required: true
},
{
key: 'subject',
label: 'Subject',
placeholder: 'Subject',
type: 'SingleLineText',
required: true
},
{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
],
Slack: [
{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
],
'Microsoft Teams': [
{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
],
Discord: [
{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
],
Mattermost: [
{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
],
Twilio: [
{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
},
{
key: 'to',
label: 'Comma separated Mobile #',
placeholder: 'Comma separated Mobile #',
type: 'LongText',
required: true
}
],
'Whatsapp Twilio': [
{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
},
{
key: 'to',
label: 'Comma separated Mobile #',
placeholder: 'Comma separated Mobile #',
type: 'LongText',
required: true
}
]
}
}),
created() {
this.loadPluginList()
},
methods: {
async loadPluginList() {
try {
// 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.reduce((o, p) => {
p.tags = p.tags ? p.tags.split(',') : []
p.parsedInput = p.input && JSON.parse(p.input)
o[p.title] = p
return o
}, {})
} catch (e) {}
},
addNewHook() {
this.onEventChange()
this.$refs.form.resetValidation()
},
async onNotTypeChange() {
this.notification = {}
if (this.hook.notification.type === 'Slack') {
this.slackChannels =
(this.apps && this.apps.Slack && this.apps.Slack.parsedInput) || []
}
if (this.hook.notification.type === 'Microsoft Teams') {
this.teamsChannels =
(this.apps &&
this.apps['Microsoft Teams'] &&
this.apps['Microsoft Teams'].parsedInput) ||
[]
}
if (this.hook.notification.type === 'Discord') {
this.discordChannels =
(this.apps && this.apps.Discord && this.apps.Discord.parsedInput) ||
[]
}
if (this.hook.notification.type === 'Mattermost') {
this.mattermostChannels =
(this.apps &&
this.apps.Mattermost &&
this.apps.Mattermost.parsedInput) ||
[]
}
if (this.hook.notification.type === 'URL') {
this.notification = this.notification || {}
this.$set(this.notification, 'body', '{{ json data }}')
}
this.$nextTick(() => this.$refs.form.validate())
},
async onEventChange() {
const { notification: { payload, type } = {}, ...hook } = this.hook
this.hook = {
...hook,
notification: {
type
}
}
// this.enableCondition = !!(this.hook && this.hook.condition && Object.keys(this.hook.condition).length)
await this.onNotTypeChange()
this.notification = payload
if (this.hook.notification.type === 'Slack') {
this.notification.webhook_url =
this.notification.webhook_url &&
this.notification.webhook_url.map(v =>
this.slackChannels.find(s => v.webhook_url === s.webhook_url)
)
}
if (this.hook.notification.type === 'Microsoft Teams') {
this.notification.webhook_url =
this.notification.webhook_url &&
this.notification.webhook_url.map(v =>
this.teamsChannels.find(s => v.webhook_url === s.webhook_url)
)
}
if (this.hook.notification.type === 'Discord') {
this.notification.webhook_url =
this.notification.webhook_url &&
this.notification.webhook_url.map(v =>
this.discordChannels.find(s => v.webhook_url === s.webhook_url)
)
}
if (this.hook.notification.type === 'Mattermost') {
this.notification.webhook_url =
this.notification.webhook_url &&
this.notification.webhook_url.map(v =>
this.mattermostChannels.find(s => v.webhook_url === s.webhook_url)
)
}
if (this.hook.notification.type === 'URL') {
this.notification = this.notification || {}
this.$set(this.notification, 'body', this.notification.body || '{{ json data }}')
}
},
async saveHooks() {
if (!this.$refs.form.validate() || !this.valid || !this.hook.event) {
return
}
this.loading = true
try {
let res
if (this.hook.id) {
res = await this.$api.dbTableWebhook.update(this.hook.id, {
...this.hook,
notification: {
...this.hook.notification,
payload: this.notification
}
})
} else {
res = await this.$api.dbTableWebhook.create(this.meta.id, {
...this.hook,
notification: {
...this.hook.notification,
payload: this.notification
}
})
}
if (!this.hook.id && res) {
this.hook.id = res.id
}
if (this.$refs.filter) {
await this.$refs.filter.applyChanges(false, {
hookId: this.hook.id
})
}
this.$toast
.success('Webhook details updated successfully')
.goAway(3000)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
this.loading = false
this.$e('a:webhook:add', {
operation: this.hook.operation,
condition: this.hook.condition,
notification: this.hook.notification.type
})
}
}
}
</script>
<style scoped>
/deep/ .nc-filter-wrapper label {
font-size: 0.75rem !important;
}
</style>

0
packages/nc-gui/components/project/tableTabs/webhookEvent.vue → packages/nc-gui/components/project/tableTabs/webhook/webhookEvent.vue

186
packages/nc-gui/components/project/tableTabs/webhook/webhookList.vue

@ -0,0 +1,186 @@
<template>
<div>
<v-card-title>
Webhooks
<v-spacer />
<v-btn
outlined
tooltip="Save"
small
@click.prevent="$emit('add')"
>
Create webhook
</v-btn>
</v-card-title>
<div v-if="hooks " class="pa-4">
<template v-if=" hooks.length">
<v-card v-for="(hook,i) in hooks" :key="hook.id" class="elevation-0 backgroundColor nc-hook" @click="$emit('edit', hook)">
<div class="pa-4 ">
<h4 class="nc-text">
{{ hook.title }}
</h4>
<div class="d-flex">
<!--Title-->
<span class="caption textColor1--text">{{ $t("general.event") }} : {{ hook.event }} {{
hook.operation
}}</span>
<v-spacer />
<!--Notify Via-->
<span class="caption textColor1--text">{{
$t("labels.notifyVia")
}} : {{ hook.notification && hook.notification.type }}
</span>
</div>
</div>
<v-icon class="nc-hook-delete-icon" small @click.stop="deleteHook(hook,i)">
mdi-delete-outline
</v-icon>
</v-card>
</template>
<div v-else class="pa-4 backgroundColor caption textColor--text text--lighten-3">
Webhooks list is empty, create new webhook by clicking 'Create webhook' button.
</div>
</div>
<!-- <v-simple-table dense>
<template #default>
<thead>
<tr>
<th>
&lt;!&ndash;Title&ndash;&gt;
{{ $t("general.title") }}
</th>
<th>
&lt;!&ndash;Event&ndash;&gt;
{{ $t("general.event") }}
</th>
<th>
&lt;!&ndash;Condition&ndash;&gt;
{{ $t("general.condition") }}
</th>
<th>
&lt;!&ndash;Notify Via&ndash;&gt;
{{ $t("labels.notifyVia") }}
</th>
<th>
&lt;!&ndash;Action&ndash;&gt;
{{ $t("labels.action") }}
</th>
</tr>
</thead>
<tbody>
<template v-if="hooks && hooks.length">
<tr v-for="(item, i) in hooks" :key="i">
<td>{{ item.title }}</td>
<td>{{ item.event }} {{ item.operation }}</td>
<td>
<v-icon v-if="item.condition" color="success" small>
mdi-check-bold
</v-icon>
</td>
<td>
{{ item.notification && item.notification.type }}
</td>
<td>
<x-icon
small
color="error"
@click.stop="deleteHook(item, i)"
>
mdi-delete
</x-icon>
&lt;!&ndash; <x-icon small :color="loading || !valid || !hook.event ? 'grey' : 'primary'"
@click.stop="(!loading && valid && hook.event) && saveHooks()">save
</x-icon>&ndash;&gt;
</td>
</tr>
</template>
<tr>
<td colspan="6" class="text-center py-5">
&lt;!&ndash;:tooltip="$t('tooltip.saveChanges')"&ndash;&gt;
<x-btn
v-ge="['hooks', 'add new']"
outlined
color="primary"
small
@click.prevent="$emit('add')"
>
<v-icon small left>
mdi-plus
</v-icon>
&lt;!&ndash;Add New Webhook&ndash;&gt;
{{ $t("activity.addWebhook") }}
</x-btn>
</td>
</tr>
</tbody>
</template>
</v-simple-table>-->
</div>
</template>
<script>
export default {
name: 'WebhookList',
props: { meta: Object },
data: () => ({
hooks: null, loading: false
}),
mounted() {
this.loadHooksList()
},
methods: {
async loadHooksList() {
this.key++
this.loading = true
const hooks = await this.$api.dbTableWebhook.list(this.meta.id)
this.hooks = hooks.list.map((h) => {
h.notification = h.notification && JSON.parse(h.notification)
return h
})
this.loading = false
},
async deleteHook(item, i) {
try {
if (item.id) {
await this.$api.dbTableWebhook.delete(item.id)
this.hooks.splice(i, 1)
} else {
this.hooks.splice(i, 1)
}
this.$toast.success('Hook deleted successfully').goAway(3000)
if (!this.hooks.length) {
this.hook = null
}
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
this.$e('a:webhook:delete')
}
}
}
</script>
<style scoped lang="scss">
.nc-hook {
position: relative;
.nc-hook-delete-icon {
position: absolute;
opacity: 0;
transition: .3s opacity;
right: 16px;
top: 16px
}
&:hover .nc-hook-delete-icon {
opacity: 1;
}
}
</style>

53
packages/nc-gui/components/project/tableTabs/webhook/webhookModal.vue

@ -0,0 +1,53 @@
<template>
<v-dialog v-model="webhookModal" width="min(700px,90%)" overlay-opacity=".9">
<v-card
v-if="webhookModal"
width="100%"
min-height="350px"
class="pa-4"
>
<webhook-editor v-if="editOrAdd" ref="editor" :meta="meta" @backToList="editOrAdd = false" />
<webhook-list v-else :meta="meta" @edit="editHook" @add="editOrAdd = true" />
</v-card>
</v-dialog>
</template>
<script>
import WebhookList from '~/components/project/tableTabs/webhook/webhookList'
import WebhookEditor from '~/components/project/tableTabs/webhook/webhookEditor'
export default {
name: 'WebhookModal',
components: { WebhookEditor, WebhookList },
props: {
meta: Object,
value: Boolean
},
data: () => ({
editOrAdd: false,
activePage: 'role'
}),
computed: {
webhookModal: {
get() {
return this.value
},
set(v) {
this.$emit('input', v)
}
}
},
methods: {
editHook(hook) {
this.editOrAdd = true
this.$nextTick(() => {
this.$refs.editor.hook = { ...hook }
this.$refs.editor.onEventChange()
})
}
}
}
</script>
<style scoped lang="scss">
</style>

35
packages/nc-gui/components/project/tableTabs/webhooksTest.vue → packages/nc-gui/components/project/tableTabs/webhook/webhooksTest.vue

@ -1,8 +1,14 @@
<template>
<div>
<h5>Sample payload</h5>
<monaco-json-object-editor v-model="sampleData" read-only style="min-height: 300px" class="caption mb-2" />
<v-btn small @click="testWebhook">
<h5 @click="isVisible=!isVisible">
Sample payload <v-icon x-small>
mdi-chevron-{{ isVisible? 'up' : 'down' }}
</v-icon>
</h5>
<div :class="{active:isVisible}" class="nc-sample-data">
<monaco-json-object-editor v-model="sampleData" read-only style="min-height: 300px" class="caption mb-2 " />
</div>
<v-btn v-if="!hideTestBtn" small @click="testWebhook">
Test webhook
</v-btn>
</div>
@ -16,10 +22,12 @@ export default {
components: { MonacoJsonObjectEditor },
props: {
modelId: String,
hook: Object
hook: Object,
hideTestBtn: Boolean
},
data: () => ({
sampleData: null
sampleData: null,
isVisible: false
}),
watch: {
async 'hook.operation'() {
@ -38,15 +46,15 @@ export default {
},
async testWebhook() {
try {
const res = await this.$api.dbTableWebhook.test(this.modelId, {
await this.$api.dbTableWebhook.test(this.modelId, {
hook: this.hook,
payload: this.sampleData
})
this.$toast.success('Webhook tested successfully').goAway(3000)
} catch (_e) {
const e = await this._extractSdkResponseError(_e)
this.$toast.error(e.message).goAway(3000)
} catch (e) {
const msg = await this._extractSdkResponseErrorMsg(e)
this.$toast.error(msg).goAway(3000)
}
}
}
@ -61,6 +69,15 @@ export default {
/deep/ label {
font-size: 0.75rem !important
}
.nc-sample-data{
overflow-y: hidden;
height:0;
transition: .3s height;
}
.nc-sample-data.active{
height:300px
}
</style>
<!--
/**

423
packages/nc-gui/components/project/tableTabs/webhooks.vue

@ -24,7 +24,9 @@
small
>
<template #divider>
<v-icon small color="grey lighten-2"> forward </v-icon>
<v-icon small color="grey lighten-2">
forward
</v-icon>
</template>
</v-breadcrumbs>
</v-toolbar-title>
@ -32,7 +34,9 @@
<!--tooltip="Close webhooks modal"-->
<x-btn outlined small @click.prevent="$emit('close')">
<v-icon small left> mdi-close-circle-outline </v-icon>
<v-icon small left>
mdi-close-circle-outline
</v-icon>
<!-- Close -->
{{ $t("general.close") }}
</x-btn>
@ -45,7 +49,9 @@
small
@click.prevent="loadHooksList"
>
<v-icon small left> mdi-reload </v-icon>
<v-icon small left>
mdi-reload
</v-icon>
<!-- Reload -->
{{ $t("general.reload") }}
</x-btn>
@ -58,7 +64,9 @@
small
@click.prevent="addNewHook"
>
<v-icon small left> mdi-plus </v-icon>
<v-icon small left>
mdi-plus
</v-icon>
<!--Add New-->
{{ $t("activity.addWebhook") }}
</x-btn>
@ -149,7 +157,9 @@
small
@click.prevent="addNewHook"
>
<v-icon small left> mdi-plus </v-icon>
<v-icon small left>
mdi-plus
</v-icon>
<!--Add New Webhook-->
{{ $t("activity.addWebhook") }}
</x-btn>
@ -175,7 +185,9 @@
:disabled="loading || !valid || !hook.event"
@click.prevent="saveHooks"
>
<v-icon small left> save </v-icon>
<v-icon small left>
save
</v-icon>
<!-- Save -->
{{ $t("general.save") }}
</x-btn>
@ -341,22 +353,23 @@
<v-card-text>
<span class="caption grey--text">
<em
>Available context variables are
<strong>data and user</strong></em
>
<em>Available context variables are
<strong>data and user</strong></em>
<v-tooltip top>
<template #activator="{ on }">
<v-icon small color="grey" class="ml-2" v-on="on"
>mdi-information</v-icon
>
<v-icon
small
color="grey"
class="ml-2"
v-on="on"
>mdi-information</v-icon>
</template>
<span class="caption">
<strong>data</strong> : Row data <br />
<strong>user</strong> : User information<br />
<strong>data</strong> : Row data <br>
<strong>user</strong> : User information<br>
</span>
</v-tooltip>
<br />
<br>
<a
href="https://docs.nocodb.com/developer-resources/webhooks/"
>
@ -386,22 +399,22 @@
</template>
<script>
import HttpWebhook from "./webhook/httpWebhook";
import ColumnFilter from "~/components/project/spreadsheet/components/columnFilter";
import HttpWebhook from './webhook/httpWebhook'
import ColumnFilter from '~/components/project/spreadsheet/components/columnFilter'
// import FormInput from '~/components/project/appStore/FormInput'
import WebhookEvent from "~/components/project/tableTabs/webhookEvent";
import WebhooksTest from "~/components/project/tableTabs/webhooksTest";
import WebhookEvent from '~/components/project/tableTabs/webhook/webhookEvent'
import WebhooksTest from '~/components/project/tableTabs/webhook/webhooksTest'
export default {
name: "Webhooks",
name: 'Webhooks',
components: {
WebhooksTest,
HttpWebhook,
WebhookEvent,
// FormInput,
ColumnFilter,
ColumnFilter
},
props: ["nodes"],
props: ['nodes'],
data: () => ({
key: 0,
apps: {},
@ -416,147 +429,147 @@ export default {
meta: null,
loading: false,
notificationList: [
"Email",
"Slack",
"Microsoft Teams",
"Discord",
"Mattermost",
"Twilio",
"Whatsapp Twilio",
"URL",
'URL',
'Email',
'Slack',
'Microsoft Teams',
'Discord',
'Mattermost',
'Twilio',
'Whatsapp Twilio'
],
filters: [],
hook: null,
notification: {},
notificationIcon: {
URL: "mdi-link",
Email: "mdi-email",
Slack: "mdi-slack",
"Microsoft Teams": "mdi-microsoft-teams",
Discord: "mdi-discord",
Mattermost: "mdi-chat",
"Whatsapp Twilio": "mdi-whatsapp",
Twilio: "mdi-cellphone-message",
URL: 'mdi-link',
Email: 'mdi-email',
Slack: 'mdi-slack',
'Microsoft Teams': 'mdi-microsoft-teams',
Discord: 'mdi-discord',
Mattermost: 'mdi-chat',
'Whatsapp Twilio': 'mdi-whatsapp',
Twilio: 'mdi-cellphone-message'
},
urlRules: [
(v) =>
!v || !v.trim() || /^https?:\/\/.{1,}/.test(v) || "Not a valid URL",
v =>
!v || !v.trim() || /^https?:\/\/.{1,}/.test(v) || 'Not a valid URL'
],
fieldList: [],
inputs: {
Email: [
{
key: "to",
label: "To Address",
placeholder: "To Address",
type: "SingleLineText",
required: true,
key: 'to',
label: 'To Address',
placeholder: 'To Address',
type: 'SingleLineText',
required: true
},
{
key: "subject",
label: "Subject",
placeholder: "Subject",
type: "SingleLineText",
required: true,
key: 'subject',
label: 'Subject',
placeholder: 'Subject',
type: 'SingleLineText',
required: true
},
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
],
Slack: [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
],
"Microsoft Teams": [
'Microsoft Teams': [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
],
Discord: [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
],
Mattermost: [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
],
Twilio: [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
},
{
key: "to",
label: "Comma separated Mobile #",
placeholder: "Comma separated Mobile #",
type: "LongText",
required: true,
},
key: 'to',
label: 'Comma separated Mobile #',
placeholder: 'Comma separated Mobile #',
type: 'LongText',
required: true
}
],
"Whatsapp Twilio": [
'Whatsapp Twilio': [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
},
{
key: "to",
label: "Comma separated Mobile #",
placeholder: "Comma separated Mobile #",
type: "LongText",
required: true,
},
],
},
key: 'to',
label: 'Comma separated Mobile #',
placeholder: 'Comma separated Mobile #',
type: 'LongText',
required: true
}
]
}
}),
async created() {
await this.loadMeta();
await this.loadHooksList();
await this.loadMeta()
await this.loadHooksList()
// todo: load only necessary plugins
await this.loadPluginList();
this.selectedHook = 0;
this.onEventChange();
await this.loadPluginList()
this.selectedHook = 0
this.onEventChange()
},
methods: {
async loadPluginList() {
try {
// const plugins = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginList'])
const plugins = (await this.$api.plugin.list()).list;
const plugins = (await this.$api.plugin.list()).list
// plugins.push(...plugins.splice(0, 3))
this.apps = plugins.reduce((o, p) => {
p.tags = p.tags ? p.tags.split(",") : [];
p.parsedInput = p.input && JSON.parse(p.input);
o[p.title] = p;
return o;
}, {});
p.tags = p.tags ? p.tags.split(',') : []
p.parsedInput = p.input && JSON.parse(p.input)
o[p.title] = p
return o
}, {})
} catch (e) {}
},
checkConditionAvail() {
@ -567,36 +580,36 @@ export default {
// this.hook.condition = []
},
async onNotTypeChange() {
this.notification = {};
if (this.hook.notification.type === "Slack") {
this.notification = {}
if (this.hook.notification.type === 'Slack') {
// const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
// title: 'Slack'
// }])
// this.slackChannels = JSON.parse(plugin.input) || []
this.slackChannels =
(this.apps && this.apps.Slack && this.apps.Slack.parsedInput) || [];
(this.apps && this.apps.Slack && this.apps.Slack.parsedInput) || []
}
if (this.hook.notification.type === "Microsoft Teams") {
if (this.hook.notification.type === 'Microsoft Teams') {
// const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
// title: 'Microsoft Teams'
// }])
// this.teamsChannels = JSON.parse(plugin.input) || []
this.teamsChannels =
(this.apps &&
this.apps["Microsoft Teams"] &&
this.apps["Microsoft Teams"].parsedInput) ||
[];
this.apps['Microsoft Teams'] &&
this.apps['Microsoft Teams'].parsedInput) ||
[]
}
if (this.hook.notification.type === "Discord") {
if (this.hook.notification.type === 'Discord') {
// const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
// title: 'Discord'
// }])
this.discordChannels =
(this.apps && this.apps.Discord && this.apps.Discord.parsedInput) ||
[];
[]
}
if (this.hook.notification.type === "Mattermost") {
if (this.hook.notification.type === 'Mattermost') {
// const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
// title: 'Mattermost'
// }])
@ -605,64 +618,64 @@ export default {
(this.apps &&
this.apps.Mattermost &&
this.apps.Mattermost.parsedInput) ||
[];
[]
}
},
async onEventChange() {
this.key++;
this.key++
if (!this.hooks || !this.hooks.length) {
return;
return
}
const { notification: { payload, type } = {}, ...hook } =
this.hooks[this.selectedHook] || {};
this.hooks[this.selectedHook] || {}
this.hook = {
...hook,
notification: {
type,
},
};
type
}
}
// this.enableCondition = !!(this.hook && this.hook.condition && Object.keys(this.hook.condition).length)
await this.onNotTypeChange();
this.notification = payload;
if (this.hook.notification.type === "Slack") {
await this.onNotTypeChange()
this.notification = payload
if (this.hook.notification.type === 'Slack') {
this.notification.webhook_url =
this.notification.webhook_url &&
this.notification.webhook_url.map((v) =>
this.slackChannels.find((s) => v.webhook_url === s.webhook_url)
);
this.notification.webhook_url.map(v =>
this.slackChannels.find(s => v.webhook_url === s.webhook_url)
)
}
if (this.hook.notification.type === "Microsoft Teams") {
if (this.hook.notification.type === 'Microsoft Teams') {
this.notification.webhook_url =
this.notification.webhook_url &&
this.notification.webhook_url.map((v) =>
this.teamsChannels.find((s) => v.webhook_url === s.webhook_url)
);
this.notification.webhook_url.map(v =>
this.teamsChannels.find(s => v.webhook_url === s.webhook_url)
)
}
if (this.hook.notification.type === "Discord") {
if (this.hook.notification.type === 'Discord') {
this.notification.webhook_url =
this.notification.webhook_url &&
this.notification.webhook_url.map((v) =>
this.discordChannels.find((s) => v.webhook_url === s.webhook_url)
);
this.notification.webhook_url.map(v =>
this.discordChannels.find(s => v.webhook_url === s.webhook_url)
)
}
if (this.hook.notification.type === "Mattermost") {
if (this.hook.notification.type === 'Mattermost') {
this.notification.webhook_url =
this.notification.webhook_url &&
this.notification.webhook_url.map((v) =>
this.mattermostChannels.find((s) => v.webhook_url === s.webhook_url)
);
this.notification.webhook_url.map(v =>
this.mattermostChannels.find(s => v.webhook_url === s.webhook_url)
)
}
if (this.hook.notification.type === "URL") {
if (this.hook.notification.type === 'URL') {
// eslint-disable-next-line no-self-assign
this.notification.api = this.notification.api;
this.notification.api = this.notification.api
}
},
async saveHooks() {
if (!this.$refs.form.validate() || !this.valid || !this.hook.event) {
return;
return
}
this.loading = true;
this.loading = true
try {
// const res = await this.$store.dispatch('sqlMgr/ActSqlOp', [
// {
@ -679,66 +692,66 @@ export default {
// }
// }
// ])
let res;
let res
if (this.hook.id) {
res = await this.$api.dbTableWebhook.update(this.hook.id, {
...this.hook,
notification: {
...this.hook.notification,
payload: this.notification,
},
});
payload: this.notification
}
})
} else {
res = await this.$api.dbTableWebhook.create(this.meta.id, {
...this.hook,
notification: {
...this.hook.notification,
payload: this.notification,
},
});
payload: this.notification
}
})
}
if (!this.hook.id && res) {
this.hook.id = res.id;
this.hook.id = res.id
}
if (this.$refs.filter) {
await this.$refs.filter.applyChanges(false, {
hookId: this.hook.id,
});
hookId: this.hook.id
})
}
this.$toast
.success("Webhook details updated successfully")
.goAway(3000);
.success('Webhook details updated successfully')
.goAway(3000)
} catch (e) {
this.$toast.error(e.message).goAway(3000);
this.$toast.error(e.message).goAway(3000)
}
this.loading = false;
await this.loadHooksList();
this.loading = false
await this.loadHooksList()
this.$e("a:webhook:add", {
this.$e('a:webhook:add', {
operation: this.hook.operation,
condition: this.hook.condition,
notification: this.hook.notification.type,
});
notification: this.hook.notification.type
})
},
async loadMeta() {
this.loadingMeta = true;
this.loadingMeta = true
// const tableMeta = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.nodes.table_name
// }] )
this.meta = await this.$store.dispatch("meta/ActLoadMeta", {
table_name: this.nodes.table_name,
}); // JSON.parse(tableMeta.meta)
this.fieldList = this.meta.columns.map((c) => c.column_name);
this.loadingMeta = false;
this.meta = await this.$store.dispatch('meta/ActLoadMeta', {
table_name: this.nodes.table_name
}) // JSON.parse(tableMeta.meta)
this.fieldList = this.meta.columns.map(c => c.column_name)
this.loadingMeta = false
},
async loadHooksList() {
this.key++;
this.loading = true;
this.key++
this.loading = true
// const hooks = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
@ -746,49 +759,49 @@ export default {
// tn: this.nodes.table_name
// }])
const hooks = await this.$api.dbTableWebhook.list(this.meta.id);
const hooks = await this.$api.dbTableWebhook.list(this.meta.id)
this.hooks = hooks.list.map((h) => {
h.notification = h.notification && JSON.parse(h.notification);
h.notification = h.notification && JSON.parse(h.notification)
// h.condition = h.condition && JSON.parse(h.condition)
return h;
});
this.loading = false;
return h
})
this.loading = false
},
addNewHook() {
this.key++;
this.selectedHook = this.hooks.length;
this.key++
this.selectedHook = this.hooks.length
this.hooks.push({
notification: {
// type:'Email'
},
});
this.onEventChange();
this.$refs.form.resetValidation();
}
})
this.onEventChange()
this.$refs.form.resetValidation()
this.$e("c:webhook:add", { count: this.hooks.length });
this.$e('c:webhook:add', { count: this.hooks.length })
},
async deleteHook(item, i) {
try {
if (item.id) {
await this.$api.dbTableWebhook.delete(item.id);
this.hooks.splice(i, 1);
await this.$api.dbTableWebhook.delete(item.id)
this.hooks.splice(i, 1)
} else {
this.hooks.splice(i, 1);
this.hooks.splice(i, 1)
}
this.$toast.success("Hook deleted successfully").goAway(3000);
this.$toast.success('Hook deleted successfully').goAway(3000)
if (!this.hooks.length) {
this.hook = null;
this.hook = null
}
} catch (e) {
this.$toast.error(e.message).goAway(3000);
this.$toast.error(e.message).goAway(3000)
}
this.$e("a:webhook:delete");
},
},
};
this.$e('a:webhook:delete')
}
}
}
</script>
<style scoped>

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

File diff suppressed because it is too large Load Diff

2
packages/nc-gui/package.json

@ -23,6 +23,7 @@
"debounce": "^1.2.0",
"file-saver": "^2.0.5",
"fix-path": "^3.0.0",
"httpsnippet": "^2.0.0",
"inflection": "^1.12.0",
"jsep": "^0.4.0",
"material-design-icons-iconfont": "^5.0.1",
@ -65,6 +66,7 @@
"@intlify/eslint-plugin-vue-i18n": "^0.11.1",
"@nuxtjs/eslint-config": "^6.0.1",
"@nuxtjs/vuetify": "^1.11.2",
"@types/httpsnippet": "^1.23.1",
"babel-eslint": "^10.1.0",
"eslint": "^7.31.0",
"monaco-editor-webpack-plugin": "^1.9.1",

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

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.90.8",
"version": "0.90.11",
"description": "> TODO: description",
"author": "“pranavxc” <pranavxc@gmail.com>",
"homepage": "https://gitlab.com/xgenecloud-ts/xgenecloud-ts#readme",

43
packages/noco-docs/content/en/developer-resources/webhooks.md

@ -6,6 +6,23 @@ category: "Developer Resources"
menuTitle: "Webhooks"
---
## Overview
Some types of notifications can be triggered by a webhook after a particular event.
- Open a Project, Select a table and Click 'More' > 'Webhooks'.
![Screenshot 2022-02-22 at 11 16 18 AM](https://user-images.githubusercontent.com/86527202/155085373-f9b438ed-98c3-4fb1-9209-1bb52736a35d.png)
- Click 'Create webhook'
![image](https://user-images.githubusercontent.com/35857179/166660074-0a896ec9-9cd8-403e-a713-61c2cefbae28.png)
- Configure the webhook
![image](https://user-images.githubusercontent.com/35857179/166660248-a3c81a34-4334-48c2-846a-65759d761559.png)
## Triggers
Webhooks allows user to trigger on certain operations on following database operations
@ -95,16 +112,10 @@ Detailed procedure for discord webhook described [here](https://support.discord.
### 3. Configure
- Open project, associated table.
- Open project and choose a table.
- Click 'More' > 'Webhooks'.
![Screenshot 2022-02-22 at 11 16 18 AM](https://user-images.githubusercontent.com/86527202/155085373-f9b438ed-98c3-4fb1-9209-1bb52736a35d.png)
- Click 'Add New Webhook'
![Screenshot 2022-02-22 at 11 18 04 AM](https://user-images.githubusercontent.com/86527202/155085629-2c4260c6-6d0c-490d-bd6e-092cbb9faeb5.png)
- Configure
- Click 'Create webhook'
- Configure webhook
- **Title**: Name of your choice to identify this Webhook.
- **Event**: Trigger event. Choose between.
- After Insert: Trigger event for new ROW insertion.
@ -160,16 +171,10 @@ Detailed procedure for discord webhook described [here](https://support.discord.
### 3. Configure
- Open project, associated table.
- Click 'More' > 'Webhooks'.
![Screenshot 2022-02-22 at 11 16 18 AM](https://user-images.githubusercontent.com/86527202/155085373-f9b438ed-98c3-4fb1-9209-1bb52736a35d.png)
- Click 'Add New Webhook'
![Screenshot 2022-02-22 at 11 18 04 AM](https://user-images.githubusercontent.com/86527202/155085629-2c4260c6-6d0c-490d-bd6e-092cbb9faeb5.png)
- Configure
- Open project and choose a table.
- Click 'More' > 'Webhooks'.
- Click 'Create webhook'
- Configure webhook
- **Title**: Name of your choice to identify this Webhook.
- **Event**: Trigger event. Choose between.
- After Insert: Trigger event for new ROW insertion.

45
packages/noco-docs/content/en/setup-and-usages/code-snippets.md

@ -0,0 +1,45 @@
---
title: 'Code Snippets'
description: 'Code Snippets'
position: 540
category: 'Product'
menuTitle: 'Code Snippets'
---
## Overview
Open a Project, Select a Table and Click `Get API Snippet` on the bottom right area.
<img width="1335" alt="image" src="https://user-images.githubusercontent.com/35857179/166663362-43ffe3cc-1053-4bf6-a65e-e4b8bae69fcb.png">
A modal box will be shown with sample code snippet for List API.
![image](https://user-images.githubusercontent.com/35857179/166663478-3f802012-7bdc-4265-9ffe-6e51c4bcf4cd.png)
## Supported Snippet
### Shell
- cURL
- wget
### Javascript
- Axios
- Fetch
- jQuery
- XHR
### Node
- Axios
- Fetch
- Request
- Native
- Unirest
### NocoDB SDK
- Javascript
- Node
### PHP
### Python
- http.client
- request
### Ruby
### Java
### C

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

File diff suppressed because it is too large Load Diff

2
packages/nocodb-sdk/package.json

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

5
packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts

@ -337,7 +337,7 @@ export class MssqlUi {
return '';
case 'nvarchar':
return '';
return 255;
case 'real':
return '';
@ -391,6 +391,9 @@ export class MssqlUi {
static getDefaultLengthIsDisabled(type) {
switch (type) {
case 'nvarchar':
return false;
case 'bigint':
case 'binary':
case 'bit':

2
packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts

@ -870,7 +870,7 @@ export class PgUi {
return 'eg: ';
case 'character varying':
return 'eg: ';
return "eg: 'sample text'";
case 'tinyint':
return 'eg: ';

24960
packages/nocodb/package-lock.json generated

File diff suppressed because it is too large Load Diff

4
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.90.8",
"version": "0.90.11",
"description": "NocoDB",
"main": "dist/bundle.js",
"repository": "https://github.com/nocodb/nocodb",
@ -151,7 +151,7 @@
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.46",
"nc-lib-gui": "0.90.8",
"nc-lib-gui": "0.90.11",
"nc-plugin": "^0.1.1",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",

17
packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSqlv2.ts

@ -236,7 +236,6 @@ class BaseModelSqlv2 {
const aliasColObjMap = await this.model.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
// todo: replace with view id
if (!ignoreFilterSort && this.viewId) {
await conditionV2(
[
@ -320,7 +319,7 @@ class BaseModelSqlv2 {
const childQb = this.dbDriver.queryBuilder().from(
this.dbDriver
.union(
.unionAll(
ids.map(p => {
const query = qb
.clone()
@ -396,7 +395,6 @@ class BaseModelSqlv2 {
);
return children.map(({ count }) => count);
// return _.groupBy(children, cn);
} catch (e) {
console.log(e);
throw e;
@ -523,11 +521,12 @@ class BaseModelSqlv2 {
model: childTable
});
const rtn = childTable.table_name;
const rtnId = childTable.id;
const qb = this.dbDriver(rtn).join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`);
await childModel.selectObject({ qb });
const finalQb = this.dbDriver.union(
const finalQb = this.dbDriver.unionAll(
parentIds.map(id => {
const query = qb
.clone()
@ -551,7 +550,10 @@ class BaseModelSqlv2 {
const children = await finalQb;
const proto = await (
await Model.getBaseModelSQL({ table_name: rtn, dbDriver: this.dbDriver })
await Model.getBaseModelSQL({
id: rtnId,
dbDriver: this.dbDriver
})
).getProto();
const gs = _.groupBy(
children.map(c => {
@ -582,6 +584,7 @@ class BaseModelSqlv2 {
model: childTable
});
const rtn = childTable.table_name;
const rtnId = childTable.id;
const qb = this.dbDriver(rtn)
.join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`)
@ -600,7 +603,7 @@ class BaseModelSqlv2 {
const children = await qb;
const proto = await (
await Model.getBaseModelSQL({ table_name: rtn, dbDriver: this.dbDriver })
await Model.getBaseModelSQL({ id: rtnId, dbDriver: this.dbDriver })
).getProto();
return children.map(c => {
@ -631,7 +634,7 @@ class BaseModelSqlv2 {
.count(`${vtn}.${vcn}`, { as: 'count' });
// await childModel.selectObject({ qb });
const children = await this.dbDriver.union(
const children = await this.dbDriver.unionAll(
parentIds.map(id => {
const query = qb
.clone()

6
packages/nocodb/src/lib/dataMapper/lib/sql/helpers/getAst.ts

@ -19,8 +19,10 @@ const getAst = async ({
if (!model.columns?.length) await model.getColumns();
if (extractOnlyPrimaries) {
return {
[model.primaryKey.title]: 1,
[model.primaryValue.title]: 1
...(model.primaryKeys
? model.primaryKeys.reduce((o, pk) => ({ ...o, [pk.title]: 1 }), {})
: {}),
...(model.primaryValue ? { [model.primaryValue.title]: 1 } : {})
};
}

11
packages/nocodb/src/lib/noco-models/Base.ts

@ -108,13 +108,20 @@ export default class Base implements BaseType {
return config;
}
// todo: construct with props
return JSON.parse(
const config = JSON.parse(
CryptoJS.AES.decrypt(
this.config,
Noco.getConfig()?.auth?.jwt?.secret
).toString(CryptoJS.enc.Utf8)
);
// todo: update sql-client args
if (config?.client === 'sqlite3') {
config.connection.filename =
config.connection.filename || config.connection?.connection.filename;
}
return config;
}
getProject(ncMeta = Noco.ncMeta): Promise<Project> {
return Project.get(this.project_id, ncMeta);

26
packages/nocodb/src/lib/noco-models/HookLog.ts

@ -66,14 +66,14 @@ export default class HookLog implements HookLogType {
public static async insert(
hookLog: Partial<
(HookLog | HookLogType) & {
HookLog & {
created_at?;
updated_at?;
}
>,
ncMeta = Noco.ncMeta
) {
const insertObj = extractProps(hookLog, [
const insertObj: any = extractProps(hookLog, [
'base_id',
'project_id',
'fk_hook_id',
@ -98,24 +98,12 @@ export default class HookLog implements HookLogType {
insertObj.base_id = hook.base_id;
}
return await ncMeta.metaInsert2(null, null, MetaTable.HOOK_LOGS, insertObj);
if (typeof insertObj.notification === 'object') {
insertObj.notification = JSON.stringify(insertObj.notification);
}
// todo: redis cache ??
// await NocoCache.appendToList(
// CacheScope.HOOK,
// [insertObj.fk_mo_id],
// `${CacheScope.HOOK}:${id}`
// );
insertObj.execution_time = parseInt(insertObj.execution_time) || 0;
// return this.get(id, ncMeta);
return await ncMeta.metaInsert2(null, null, MetaTable.HOOK_LOGS, insertObj);
}
// static async delete(hookId: any, ncMeta = Noco.ncMeta) {
// await NocoCache.deepDel(
// CacheScope.HOOK,
// `${CacheScope.HOOK}:${hookId}`,
// CacheDelDirection.CHILD_TO_PARENT
// );
// return await ncMeta.metaDelete(null, null, MetaTable.HOOKS, hookId);
// }
}

26
packages/nocodb/src/lib/noco-models/Model.ts

@ -317,37 +317,13 @@ export default class Model implements TableType {
public static async getBaseModelSQL(
args: {
id?: string;
table_name?: string;
viewId?: string;
dbDriver: XKnex;
model?: Model;
},
ncMeta = Noco.ncMeta
): Promise<BaseModelSqlv2> {
const model =
args?.model ||
(await this.getByIdOrName(
{
id: args.id,
table_name: args.table_name
},
ncMeta
));
// if (
// this.baseModels?.[model.base_id]?.[model.db_alias]?.[args.table_name || args.id]
// ) {
// return this.baseModels[model.base_id][model.db_alias][args.table_name || args.id];
// }
// this.baseModels[model.base_id] = this.baseModels[model.base_id] || {};
// this.baseModels[model.base_id][model.db_alias] =
// this.baseModels[model.base_id][model.db_alias] || {};
// return (this.baseModels[model.base_id][model.db_alias][
// args.table_name || args.id
// ] = new BaseModelSqlv2({
// dbDriver: args.dbDriver,
// model
// }));
const model = args?.model || (await this.get(args.id, ncMeta));
return new BaseModelSqlv2({
dbDriver: args.dbDriver,

17
packages/nocodb/src/lib/noco-models/ProjectUser.ts

@ -6,6 +6,7 @@ import {
} from '../utils/globals';
import Noco from '../noco/Noco';
import NocoCache from '../noco-cache/NocoCache';
import User from './User';
export default class ProjectUser {
project_id: string;
@ -133,6 +134,22 @@ export default class ProjectUser {
// set cache
await NocoCache.set(key, o);
}
// update user cache
const user = await User.get(userId);
if (user) {
const email = user.email;
for (const key of [
`${CacheScope.USER}:${email}`,
`${CacheScope.USER}:${email}___${projectId}`
]) {
const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
if (o) {
o.roles = roles;
// set cache
await NocoCache.set(key, o);
}
}
}
// set meta
return await ncMeta.metaUpdate(
null,

4
packages/nocodb/src/lib/noco/meta/api/apiTokenApis.ts

@ -2,6 +2,7 @@ import { Request, Response, Router } from 'express';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import ApiToken from '../../../noco-models/ApiToken';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function apiTokenList(_req: Request, res: Response) {
res.json(await ApiToken.list());
@ -19,14 +20,17 @@ const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/projects/:projectId/api-tokens',
metaApiMetrics,
ncMetaAclMw(apiTokenList, 'apiTokenList')
);
router.post(
'/api/v1/db/meta/projects/:projectId/api-tokens',
metaApiMetrics,
ncMetaAclMw(apiTokenCreate, 'apiTokenCreate')
);
router.delete(
'/api/v1/db/meta/projects/:projectId/api-tokens/:token',
metaApiMetrics,
ncMetaAclMw(apiTokenDelete, 'apiTokenDelete')
);

58
packages/nocodb/src/lib/noco/meta/api/columnApis.ts

@ -31,6 +31,7 @@ import { NcError } from '../helpers/catchError';
import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue';
import NcConnectionMgrv2 from '../../common/NcConnectionMgrv2';
import { metaApiMetrics } from '../helpers/apiMetrics';
const randomID = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 10);
@ -725,11 +726,9 @@ export async function columnDelete(req: Request, res: Response<TableType>) {
},
true
);
const columnsInRelatedTable: Column[] = await relationColOpt
.getRelatedTable()
.then(m => m.getColumns());
let columnInRelatedTable: Column;
for (const c of columnsInRelatedTable) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
@ -737,26 +736,63 @@ export async function columnDelete(req: Request, res: Response<TableType>) {
LinkToAnotherRecordColumn
>();
if (
colOpt.type === 'mm' &&
colOpt.fk_parent_column_id === childColumn.id &&
colOpt.fk_child_column_id === parentColumn.id &&
colOpt.type === 'mm' &&
colOpt.fk_mm_model_id === mmTable.id &&
colOpt.fk_mm_parent_column_id === mmChildCol.id &&
colOpt.fk_mm_child_column_id === mmParentCol.id
) {
columnInRelatedTable = c;
await Column.delete(c.id);
break;
}
}
await Column.delete(relationColOpt.fk_column_id);
await Column.delete(columnInRelatedTable.id);
// delete bt columns in m2m table
await mmTable.getColumns();
for (const c of mmTable.columns) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt = await c.getColOptions<
LinkToAnotherRecordColumn
>();
if (colOpt.type === 'bt') {
await Column.delete(c.id);
}
}
// delete hm columns in parent table
await parentTable.getColumns();
for (const c of parentTable.columns) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt = await c.getColOptions<
LinkToAnotherRecordColumn
>();
if (colOpt.fk_related_model_id === mmTable.id) {
await Column.delete(c.id);
}
}
// delete hm columns in child table
await childTable.getColumns();
for (const c of childTable.columns) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt = await c.getColOptions<
LinkToAnotherRecordColumn
>();
if (colOpt.fk_related_model_id === mmTable.id) {
await Column.delete(c.id);
}
}
// retrieve columns in m2m table again
await mmTable.getColumns();
// ignore deleting table if it have more than 2 columns
// ignore deleting table if it has more than 2 columns
// the expected 2 columns would be table1_id & table2_id
if (mmTable.columns.length === 2) {
await sqlMgr.sqlOpPlus(base, 'tableDelete', mmTable);
await mmTable.delete();
}
}
break;
@ -867,7 +903,6 @@ const deleteHmOrBtRelation = async (
const columnsInRelatedTable: Column[] = await relationColOpt
.getRelatedTable()
.then(m => m.getColumns());
let columnInRelatedTable: Column;
const relType = relationColOpt.type === 'bt' ? 'hm' : 'bt';
for (const c of columnsInRelatedTable) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
@ -877,14 +912,13 @@ const deleteHmOrBtRelation = async (
colOpt.fk_child_column_id === childColumn.id &&
colOpt.type === relType
) {
columnInRelatedTable = c;
await Column.delete(c.id, ncMeta);
break;
}
}
// delete virtual columns
await Column.delete(relationColOpt.fk_column_id, ncMeta);
await Column.delete(columnInRelatedTable.id, ncMeta);
if (!ignoreFkDelete) {
const cTable = await Model.getWithInfo({
@ -922,18 +956,22 @@ const deleteHmOrBtRelation = async (
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/tables/:tableId/columns/',
metaApiMetrics,
ncMetaAclMw(columnAdd, 'columnAdd')
);
router.patch(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnUpdate, 'columnUpdate')
);
router.delete(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnDelete, 'columnDelete')
);
router.post(
'/api/v1/db/meta/columns/:columnId/primary',
metaApiMetrics,
ncMetaAclMw(columnSetAsPrimary, 'columnSetAsPrimary')
);
export default router;

8
packages/nocodb/src/lib/noco/meta/api/filterApis.ts

@ -12,6 +12,7 @@ import Project from '../../../noco-models/Project';
import Filter from '../../../noco-models/Filter';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore
export async function filterGet(req: Request, res: Response, next) {
@ -129,10 +130,12 @@ export async function hookFilterCreate(req: Request<any, any, TableReq>, res) {
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/views/:viewId/filters',
metaApiMetrics,
ncMetaAclMw(filterList, 'filterList')
);
router.post(
'/api/v1/db/meta/views/:viewId/filters',
metaApiMetrics,
ncMetaAclMw(filterCreate, 'filterCreate')
);
@ -142,23 +145,28 @@ router.get(
);
router.post(
'/api/v1/db/meta/hooks/:hookId/filters',
metaApiMetrics,
ncMetaAclMw(hookFilterCreate, 'filterCreate')
);
router.get(
'/api/v1/db/meta/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterGet, 'filterGet')
);
router.patch(
'/api/v1/db/meta/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterUpdate, 'filterUpdate')
);
router.delete(
'/api/v1/db/meta/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterDelete, 'filterDelete')
);
router.get(
'/api/v1/db/meta/filters/:filterParentId/children',
metaApiMetrics,
ncMetaAclMw(filterChildrenRead, 'filterChildrenRead')
);
export default router;

5
packages/nocodb/src/lib/noco/meta/api/formViewApis.ts

@ -12,6 +12,7 @@ import View from '../../../noco-models/View';
import FormView from '../../../noco-models/FormView';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore
export async function formViewGet(req: Request, res: Response<FormType>) {
@ -41,18 +42,22 @@ export async function formViewDelete(req: Request, res: Response, next) {}
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/tables/:tableId/forms',
metaApiMetrics,
ncMetaAclMw(formViewCreate, 'formViewCreate')
);
router.get(
'/api/v1/db/meta/forms/:formViewId',
metaApiMetrics,
ncMetaAclMw(formViewGet, 'formViewGet')
);
router.patch(
'/api/v1/db/meta/forms/:formViewId',
metaApiMetrics,
ncMetaAclMw(formViewUpdate, 'formViewUpdate')
);
router.delete(
'/api/v1/db/meta/forms/:formViewId',
metaApiMetrics,
ncMetaAclMw(formViewDelete, 'formViewDelete')
);
export default router;

2
packages/nocodb/src/lib/noco/meta/api/formViewColumnApis.ts

@ -2,6 +2,7 @@ import { Request, Response, Router } from 'express';
import FormViewColumn from '../../../noco-models/FormViewColumn';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function columnUpdate(req: Request, res: Response) {
Tele.emit('evt', { evt_type: 'formViewColumn:updated' });
@ -11,6 +12,7 @@ export async function columnUpdate(req: Request, res: Response) {
const router = Router({ mergeParams: true });
router.patch(
'/api/v1/db/meta/form-columns/:formViewColumnId',
metaApiMetrics,
ncMetaAclMw(columnUpdate, 'columnUpdate')
);
export default router;

4
packages/nocodb/src/lib/noco/meta/api/galleryViewApis.ts

@ -4,6 +4,7 @@ import View from '../../../noco-models/View';
import GalleryView from '../../../noco-models/GalleryView';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function galleryViewGet(req: Request, res: Response<GalleryType>) {
res.json(await GalleryView.get(req.params.galleryViewId));
}
@ -27,14 +28,17 @@ export async function galleryViewUpdate(req, res) {
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/tables/:tableId/galleries',
metaApiMetrics,
ncMetaAclMw(galleryViewCreate, 'galleryViewCreate')
);
router.patch(
'/api/v1/db/meta/galleries/:galleryViewId',
metaApiMetrics,
ncMetaAclMw(galleryViewUpdate, 'galleryViewUpdate')
);
router.get(
'/api/v1/db/meta/galleries/:galleryViewId',
metaApiMetrics,
ncMetaAclMw(galleryViewGet, 'galleryViewGet')
);
export default router;

2
packages/nocodb/src/lib/noco/meta/api/gridViewApis.ts

@ -11,6 +11,7 @@ import Project from '../../../noco-models/Project';
import View from '../../../noco-models/View';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore
export async function gridViewCreate(req: Request<any, any>, res) {
@ -27,6 +28,7 @@ export async function gridViewCreate(req: Request<any, any>, res) {
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/tables/:tableId/grids/',
metaApiMetrics,
ncMetaAclMw(gridViewCreate, 'gridViewCreate')
);
export default router;

3
packages/nocodb/src/lib/noco/meta/api/gridViewColumnApis.ts

@ -2,6 +2,7 @@ import { Request, Response, Router } from 'express';
import GridViewColumn from '../../../noco-models/GridViewColumn';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function columnList(req: Request, res: Response) {
res.json(await GridViewColumn.list(req.params.gridViewId));
@ -15,10 +16,12 @@ export async function gridColumnUpdate(req: Request, res: Response) {
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/grids/:gridViewId/grid-columns',
metaApiMetrics,
ncMetaAclMw(columnList, 'columnList')
);
router.patch(
'/api/v1/db/meta/grid-columns/:gridViewColumnId',
metaApiMetrics,
ncMetaAclMw(gridColumnUpdate, 'gridColumnUpdate')
);
export default router;

12
packages/nocodb/src/lib/noco/meta/api/hookApis.ts

@ -8,6 +8,7 @@ import Model from '../../../noco-models/Model';
import populateSamplePayload from '../helpers/populateSamplePayload';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function hookList(
req: Request<any, any, any>,
@ -60,7 +61,8 @@ export async function hookTest(req: Request<any, any>, res: Response) {
model,
data,
user,
(hook as any)?.filters
(hook as any)?.filters,
true
);
Tele.emit('evt', { evt_type: 'webhooks:tested' });
@ -71,32 +73,38 @@ export async function tableSampleData(req: Request, res: Response) {
const model = await Model.getByIdOrName({ id: req.params.tableId });
res // todo: pagination
.json(await populateSamplePayload(model, true, req.params.operation));
.json(await populateSamplePayload(model, false, req.params.operation));
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/tables/:tableId/hooks',
metaApiMetrics,
ncMetaAclMw(hookList, 'hookList')
);
router.post(
'/api/v1/db/meta/tables/:tableId/hooks/test',
metaApiMetrics,
ncMetaAclMw(hookTest, 'hookTest')
);
router.post(
'/api/v1/db/meta/tables/:tableId/hooks',
metaApiMetrics,
ncMetaAclMw(hookCreate, 'hookCreate')
);
router.delete(
'/api/v1/db/meta/hooks/:hookId',
metaApiMetrics,
ncMetaAclMw(hookDelete, 'hookDelete')
);
router.patch(
'/api/v1/db/meta/hooks/:hookId',
metaApiMetrics,
ncMetaAclMw(hookUpdate, 'hookUpdate')
);
router.get(
'/api/v1/db/meta/tables/:tableId/hooks/samplePayload/:operation',
metaApiMetrics,
catchError(tableSampleData)
);
export default router;

12
packages/nocodb/src/lib/noco/meta/api/hookFilterApis.ts

@ -12,6 +12,7 @@ import Project from '../../../noco-models/Project';
import Filter from '../../../noco-models/Filter';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore
export async function filterGet(req: Request, res: Response, next) {
@ -108,25 +109,34 @@ export async function filterDelete(req: Request, res: Response, next) {
}
const router = Router({ mergeParams: true });
router.get('/hooks/:hookId/filters/', ncMetaAclMw(filterList, 'filterList'));
router.get(
'/hooks/:hookId/filters/',
metaApiMetrics,
ncMetaAclMw(filterList, 'filterList')
);
router.post(
'/hooks/:hookId/filters/',
metaApiMetrics,
ncMetaAclMw(filterCreate, 'filterCreate')
);
router.get(
'/hooks/:hookId/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterGet, 'filterGet')
);
router.patch(
'/hooks/:hookId/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterUpdate, 'filterUpdate')
);
router.delete(
'/hooks/:hookId/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterDelete, 'filterDelete')
);
router.get(
'/hooks/:hookId/filters/:filterParentId/children',
metaApiMetrics,
ncMetaAclMw(filterChildrenRead, 'filterChildrenRead')
);
export default router;

3
packages/nocodb/src/lib/noco/meta/api/metaDiffApis.ts

@ -16,6 +16,7 @@ import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue';
import { Tele } from 'nc-help';
import getColumnUiType from '../helpers/getColumnUiType';
import { metaApiMetrics } from '../helpers/apiMetrics';
export enum MetaDiffType {
TABLE_NEW = 'TABLE_NEW',
@ -849,10 +850,12 @@ export async function extractAndGenerateManyToManyRelations(
const router = Router();
router.get(
'/api/v1/db/meta/projects/:projectId/meta-diff',
metaApiMetrics,
ncMetaAclMw(metaDiff, 'metaDiff')
);
router.post(
'/api/v1/db/meta/projects/:projectId/meta-diff',
metaApiMetrics,
ncMetaAclMw(metaDiffSync, 'metaDiffSync')
);
export default router;

3
packages/nocodb/src/lib/noco/meta/api/modelVisibilityApis.ts

@ -4,6 +4,7 @@ import { Router } from 'express';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import Project from '../../../noco-models/Project';
import { metaApiMetrics } from '../helpers/apiMetrics';
async function xcVisibilityMetaSetAll(req, res) {
Tele.emit('evt', { evt_type: 'uiAcl:updated' });
for (const d of req.body) {
@ -108,6 +109,7 @@ export async function xcVisibilityMetaGet(
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/projects/:projectId/visibility-rules',
metaApiMetrics,
ncMetaAclMw(async (req, res) => {
res.json(
await xcVisibilityMetaGet(
@ -120,6 +122,7 @@ router.get(
);
router.post(
'/api/v1/db/meta/projects/:projectId/visibility-rules',
metaApiMetrics,
ncMetaAclMw(xcVisibilityMetaSetAll, 'modelVisibilitySet')
);
export default router;

11
packages/nocodb/src/lib/noco/meta/api/pluginApis.ts

@ -5,6 +5,7 @@ import { PluginType } from 'nocodb-sdk';
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function pluginList(_req: Request, res: Response) {
res.json(new PagedResponseImpl(await Plugin.list()));
@ -34,21 +35,29 @@ export async function isPluginActive(req: Request, res: Response) {
}
const router = Router({ mergeParams: true });
router.get('/api/v1/db/meta/plugins', ncMetaAclMw(pluginList, 'pluginList'));
router.get(
'/api/v1/db/meta/plugins',
metaApiMetrics,
ncMetaAclMw(pluginList, 'pluginList')
);
router.post(
'/api/v1/db/meta/plugins/test',
metaApiMetrics,
ncMetaAclMw(pluginTest, 'pluginTest')
);
router.get(
'/api/v1/db/meta/plugins/:pluginId',
metaApiMetrics,
ncMetaAclMw(pluginRead, 'pluginRead')
);
router.patch(
'/api/v1/db/meta/plugins/:pluginId',
metaApiMetrics,
ncMetaAclMw(pluginUpdate, 'pluginUpdate')
);
router.get(
'/api/v1/db/meta/plugins/:pluginTitle/status',
metaApiMetrics,
ncMetaAclMw(isPluginActive, 'isPluginActive')
);
export default router;

6
packages/nocodb/src/lib/noco/meta/api/projectApis.ts

@ -22,6 +22,7 @@ import { NcError } from '../helpers/catchError';
import getColumnUiType from '../helpers/getColumnUiType';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue';
import { extractAndGenerateManyToManyRelations } from './metaDiffApis';
import { metaApiMetrics } from '../helpers/apiMetrics';
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4);
@ -392,22 +393,27 @@ export async function projectInfoGet(req, res) {
export default router => {
router.get(
'/api/v1/db/meta/projects/:projectId/info',
metaApiMetrics,
ncMetaAclMw(projectInfoGet, 'projectInfoGet')
);
router.get(
'/api/v1/db/meta/projects/:projectId',
metaApiMetrics,
ncMetaAclMw(projectGet, 'projectGet')
);
router.delete(
'/api/v1/db/meta/projects/:projectId',
metaApiMetrics,
ncMetaAclMw(projectDelete, 'projectDelete')
);
router.post(
'/api/v1/db/meta/projects',
metaApiMetrics,
ncMetaAclMw(projectCreate, 'projectCreate')
);
router.get(
'/api/v1/db/meta/projects',
metaApiMetrics,
ncMetaAclMw(projectList, 'projectList')
);
};

6
packages/nocodb/src/lib/noco/meta/api/projectUserApis.ts

@ -14,6 +14,7 @@ import * as ejs from 'ejs';
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2';
import Noco from '../../Noco';
import { PluginCategory } from 'nocodb-sdk';
import { metaApiMetrics } from '../helpers/apiMetrics';
async function userList(req, res) {
res.json({
@ -297,22 +298,27 @@ async function sendInviteEmail(
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/projects/:projectId/users',
metaApiMetrics,
ncMetaAclMw(userList, 'userList')
);
router.post(
'/api/v1/db/meta/projects/:projectId/users',
metaApiMetrics,
ncMetaAclMw(userInvite, 'userInvite')
);
router.patch(
'/api/v1/db/meta/projects/:projectId/users/:userId',
metaApiMetrics,
ncMetaAclMw(projectUserUpdate, 'projectUserUpdate')
);
router.delete(
'/api/v1/db/meta/projects/:projectId/users/:userId',
metaApiMetrics,
ncMetaAclMw(projectUserDelete, 'projectUserDelete')
);
router.post(
'/api/v1/db/meta/projects/:projectId/users/:userId/resend-invite',
metaApiMetrics,
ncMetaAclMw(projectUserInviteResend, 'projectUserInviteResend')
);
export default router;

11
packages/nocodb/src/lib/noco/meta/api/sortApis.ts

@ -11,6 +11,7 @@ import Project from '../../../noco-models/Project';
import Sort from '../../../noco-models/Sort';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore
export async function sortGet(req: Request, res: Response<TableType>) {}
@ -51,19 +52,27 @@ export async function sortDelete(req: Request, res: Response) {
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/views/:viewId/sorts/',
metaApiMetrics,
ncMetaAclMw(sortList, 'sortList')
);
router.post(
'/api/v1/db/meta/views/:viewId/sorts/',
metaApiMetrics,
ncMetaAclMw(sortCreate, 'sortCreate')
);
router.get('/api/v1/db/meta/sorts/:sortId', ncMetaAclMw(sortGet, 'sortGet'));
router.get(
'/api/v1/db/meta/sorts/:sortId',
metaApiMetrics,
ncMetaAclMw(sortGet, 'sortGet')
);
router.patch(
'/api/v1/db/meta/sorts/:sortId',
metaApiMetrics,
ncMetaAclMw(sortUpdate, 'sortUpdate')
);
router.delete(
'/api/v1/db/meta/sorts/:sortId',
metaApiMetrics,
ncMetaAclMw(sortDelete, 'sortDelete')
);
export default router;

30
packages/nocodb/src/lib/noco/meta/api/tableApis.ts

@ -26,6 +26,7 @@ import Column from '../../../noco-models/Column';
import NcConnectionMgrv2 from '../../common/NcConnectionMgrv2';
import getColumnUiType from '../helpers/getColumnUiType';
import LinkToAnotherRecordColumn from '../../../noco-models/LinkToAnotherRecordColumn';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function tableGet(req: Request, res: Response<TableType>) {
const table = await Model.getWithInfo({
id: req.params.tableId
@ -167,15 +168,18 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
res.json(
await Model.insert(project.id, base.id, {
...req.body,
columns: columns.map((c, i) => ({
uidt: c.uidt || getColumnUiType(base, c),
...c,
title:
req.body?.columns?.find(c1 => c.cn === c1.column_name)?.title ||
getColumnNameAlias(c.cn, base),
column_name: c.cn,
order: i + 1
})),
columns: columns.map((c, i) => {
const colMetaFromReq = req.body?.columns?.find(
c1 => c.cn === c1.column_name
);
return {
uidt: colMetaFromReq?.uidt || c.uidt || getColumnUiType(base, c),
...c,
title: colMetaFromReq?.title || getColumnNameAlias(c.cn, base),
column_name: c.cn,
order: i + 1
};
}),
order: +(tables?.pop()?.order ?? 0) + 1
})
);
@ -220,7 +224,7 @@ export async function tableDelete(req: Request, res: Response) {
)
);
NcError.badRequest(
`Table can't be deleted since Table is being referred in following tables : ${referredTables.join(
`Table can't be deleted since Table is being referred in following tables : ${referredTables.join(
', '
)}. Delete LinkToAnotherRecord columns and try again.`
);
@ -261,26 +265,32 @@ export async function tableDelete(req: Request, res: Response) {
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/projects/:projectId/tables',
metaApiMetrics,
ncMetaAclMw(tableList, 'tableList')
);
router.post(
'/api/v1/db/meta/projects/:projectId/tables',
metaApiMetrics,
ncMetaAclMw(tableCreate, 'tableCreate')
);
router.get(
'/api/v1/db/meta/tables/:tableId',
metaApiMetrics,
ncMetaAclMw(tableGet, 'tableGet')
);
router.patch(
'/api/v1/db/meta/tables/:tableId',
metaApiMetrics,
ncMetaAclMw(tableUpdate, 'tableUpdate')
);
router.delete(
'/api/v1/db/meta/tables/:tableId',
metaApiMetrics,
ncMetaAclMw(tableDelete, 'tableDelete')
);
router.post(
'/api/v1/db/meta/tables/:tableId/reorder',
metaApiMetrics,
ncMetaAclMw(tableReorder, 'tableReorder')
);
export default router;

9
packages/nocodb/src/lib/noco/meta/api/viewApis.ts

@ -13,6 +13,7 @@ import View from '../../../noco-models/View';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { xcVisibilityMetaGet } from './modelVisibilityApis';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore
export async function viewGet(req: Request, res: Response<Table>) {}
@ -103,27 +104,33 @@ async function shareViewList(req: Request<any, any>, res) {
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/tables/:tableId/views',
metaApiMetrics,
ncMetaAclMw(viewList, 'viewList')
);
router.patch(
'/api/v1/db/meta/views/:viewId',
metaApiMetrics,
ncMetaAclMw(viewUpdate, 'viewUpdate')
);
router.delete(
'/api/v1/db/meta/views/:viewId',
metaApiMetrics,
ncMetaAclMw(viewDelete, 'viewDelete')
);
router.post(
'/api/v1/db/meta/views/:viewId/show-all',
metaApiMetrics,
ncMetaAclMw(showAllColumns, 'showAllColumns')
);
router.post(
'/api/v1/db/meta/views/:viewId/hide-all',
metaApiMetrics,
ncMetaAclMw(hideAllColumns, 'hideAllColumns')
);
router.get(
'/api/v1/db/meta/tables/:tableId/share',
metaApiMetrics,
ncMetaAclMw(shareViewList, 'shareViewList')
);
router.post(
@ -132,10 +139,12 @@ router.post(
);
router.patch(
'/api/v1/db/meta/views/:viewId/share',
metaApiMetrics,
ncMetaAclMw(shareViewPasswordUpdate, 'shareViewPasswordUpdate')
);
router.delete(
'/api/v1/db/meta/views/:viewId/share',
metaApiMetrics,
ncMetaAclMw(shareViewDelete, 'shareViewDelete')
);

4
packages/nocodb/src/lib/noco/meta/api/viewColumnApis.ts

@ -2,6 +2,7 @@ import { Request, Response, Router } from 'express';
import View from '../../../noco-models/View';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function columnList(req: Request, res: Response) {
res.json(await View.getColumns(req.params.viewId));
@ -33,14 +34,17 @@ export async function columnUpdate(req: Request, res: Response) {
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/views/:viewId/columns/',
metaApiMetrics,
ncMetaAclMw(columnList, 'columnList')
);
router.post(
'/api/v1/db/meta/views/:viewId/columns/',
metaApiMetrics,
ncMetaAclMw(columnAdd, 'columnAdd')
);
router.patch(
'/api/v1/db/meta/views/:viewId/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnUpdate, 'viewColumnUpdate')
);
export default router;

12
packages/nocodb/src/lib/noco/meta/helpers/apiMetrics.ts

@ -3,17 +3,23 @@ import { Tele } from 'nc-help';
const countMap = {};
const metrics = async (req: Request) => {
const metrics = async (req: Request, c = 50) => {
if (!req?.route?.path) return;
const event = `a:api:${req.route.path}:${req.method}`;
countMap[event] = (countMap[event] || 0) + 1;
if (countMap[event] >= 50) {
if (countMap[event] >= c) {
Tele.event({ event });
countMap[event] = 0;
}
};
export default async (req: Request, _res, next) => {
const metaApiMetrics = (req: Request, _res, next) => {
metrics(req, 10).then(() => {});
next();
};
export default (req: Request, _res, next) => {
metrics(req).then(() => {});
next();
};
export { metaApiMetrics };

4
packages/nocodb/src/lib/noco/meta/helpers/populateSamplePayload.ts

@ -33,6 +33,7 @@ export default async function populateSamplePayload(
[UITypes.LinkToAnotherRecord, UITypes.Lookup].includes(column.uidt)
)
continue;
if (operation === 'delete' && model.primaryKey?.title !== column.title)
continue;
@ -55,6 +56,9 @@ async function getSampleColumnValue(column: Column): Promise<any> {
const sampleVal = await populateSamplePayload(
await colOpt.getRelatedTable()
);
if (colOpt.type !== RelationTypes.BELONGS_TO) {
return undefined;
}
return colOpt.type === RelationTypes.BELONGS_TO
? sampleVal
: [sampleVal];

41
packages/nocodb/src/lib/noco/meta/helpers/webhookHelpers.ts

@ -112,26 +112,26 @@ export async function validateCondition(filters: Filter[], data: any) {
return isValid;
}
export async function handleHttpWebHook(apiMeta, apiReq, data) {
export async function handleHttpWebHook(apiMeta, user, data) {
// try {
const req = axiosRequestMake(apiMeta, apiReq, data);
const req = axiosRequestMake(apiMeta, user, data);
await require('axios')(req);
// } catch (e) {
// console.log(e);
// }
}
export function axiosRequestMake(_apiMeta, apiReq, data) {
export function axiosRequestMake(_apiMeta, user, data) {
const apiMeta = { ..._apiMeta };
if (apiMeta.body) {
try {
apiMeta.body = JSON.parse(apiMeta.body, (_key, value) => {
return typeof value === 'string'
? parseBody(value, apiReq, data, apiMeta)
? parseBody(value, user, data, apiMeta)
: value;
});
} catch (e) {
apiMeta.body = parseBody(apiMeta.body, apiReq, data, apiMeta);
apiMeta.body = parseBody(apiMeta.body, user, data, apiMeta);
console.log(e);
}
}
@ -139,11 +139,11 @@ export function axiosRequestMake(_apiMeta, apiReq, data) {
try {
apiMeta.auth = JSON.parse(apiMeta.auth, (_key, value) => {
return typeof value === 'string'
? parseBody(value, apiReq, data, apiMeta)
? parseBody(value, user, data, apiMeta)
: value;
});
} catch (e) {
apiMeta.auth = parseBody(apiMeta.auth, apiReq, data, apiMeta);
apiMeta.auth = parseBody(apiMeta.auth, user, data, apiMeta);
console.log(e);
}
}
@ -152,17 +152,12 @@ export function axiosRequestMake(_apiMeta, apiReq, data) {
params: apiMeta.parameters
? apiMeta.parameters.reduce((paramsObj, param) => {
if (param.name && param.enabled) {
paramsObj[param.name] = parseBody(
param.value,
apiReq,
data,
apiMeta
);
paramsObj[param.name] = parseBody(param.value, user, data, apiMeta);
}
return paramsObj;
}, {})
: {},
url: parseBody(apiMeta.path, apiReq, data, apiMeta),
url: parseBody(apiMeta.path, user, data, apiMeta),
method: apiMeta.method,
data: apiMeta.body,
headers: apiMeta.headers
@ -170,7 +165,7 @@ export function axiosRequestMake(_apiMeta, apiReq, data) {
if (header.name && header.enabled) {
headersObj[header.name] = parseBody(
header.value,
apiReq,
user,
data,
apiMeta
);
@ -188,7 +183,8 @@ export async function invokeWebhook(
_model: Model,
data,
user,
testFilters = null
testFilters = null,
throwErrorOnFailure = false
) {
let hookLog: HookLogType;
const startTime = process.hrtime();
@ -289,16 +285,19 @@ export async function invokeWebhook(
}
} catch (e) {
console.log(e);
hookLog = {
...hook,
error_code: e.error_code,
error_message: e.message,
error: JSON.stringify(e)
};
if (throwErrorOnFailure) throw e;
} finally {
hookLog.execution_time = parseHrtimeToMilliSeconds(
process.hrtime(startTime)
);
if (hookLog) HookLog.insert({ ...hookLog, test_call: !!testFilters });
}
hookLog.execution_time = parseHrtimeToMilliSeconds(process.hrtime(startTime));
if (hookLog) await HookLog.insert({ ...hookLog, test_call: !!testFilters });
}
export function _transformSubmittedFormDataForEmail(
@ -342,6 +341,6 @@ export function _transformSubmittedFormDataForEmail(
}
function parseHrtimeToMilliSeconds(hrtime) {
const seconds = (hrtime[0] + hrtime[1] / 1e6).toFixed(3);
return seconds;
const milliseconds = (hrtime[0] + hrtime[1] / 1e6).toFixed(3);
return milliseconds;
}

2
scripts/cypress/integration/common/1a_table_operations.js

@ -12,7 +12,7 @@ export const genTest = (apiType, dbType) => {
});
after(() => {
cy.get(".mdi-close").click({ multiple: true });
cy.get(".mdi-close").click({ multiple: true, force: true });
});
const name = "tablex";

45
scripts/cypress/integration/common/6b_downloadCsv.js

@ -23,28 +23,39 @@ export const genTest = (apiType, dbType) => {
mainPage.hideField("LastUpdate");
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
// let storedRecords = [
// `Country,CityList`,
// `Afghanistan,Kabul`,
// `Algeria,"Batna, Bchar, Skikda"`,
// `American Samoa,Tafuna`,
// `Angola,"Benguela, Namibe"`,
// ];
let storedRecords = [
`Country,CityList`,
`Afghanistan,Kabul`,
`Algeria,"Batna, Bchar, Skikda"`,
`American Samoa,Tafuna`,
`Angola,"Benguela, Namibe"`,
['Country','CityList'],
['Afghanistan','Kabul'],
['Algeria','Skikda', 'Bchar', 'Batna'],
['American Samoa','Tafuna'],
['Angola','Benguela', 'Namibe'],
];
if (isPostgres()) {
// order of second entry is different
storedRecords = [
`Country,CityList`,
`Afghanistan,Kabul`,
`Algeria,"Skikda, Bchar, Batna"`,
`American Samoa,Tafuna`,
`Angola,"Benguela, Namibe"`,
];
}
// if (isPostgres()) {
// // order of second entry is different
// storedRecords = [
// `Country,CityList`,
// `Afghanistan,Kabul`,
// `Algeria,"Skikda, Bchar, Batna"`,
// `American Samoa,Tafuna`,
// `Angola,"Benguela, Namibe"`,
// ];
// }
for (let i = 0; i < storedRecords.length - 1; i++) {
cy.log(retrievedRecords[i]);
expect(retrievedRecords[i]).to.be.equal(storedRecords[i]);
for(let j=0; j<storedRecords[i].length; j++)
expect(retrievedRecords[i]).to.have.string(storedRecords[i][j])
// often, the order in which records "Skikda, Bchar, Batna" appear, used to toggle
// hence verifying record contents separately
// expect(retrievedRecords[i]).to.be.equal(storedRecords[i]);
}
};

44
scripts/cypress/support/commands.js

@ -162,14 +162,16 @@ Cypress.Commands.add("openTableTab", (tn, rc) => {
.click({ force: true });
cy.get(".nc-project-tree")
.contains(tn, { timeout: 6000 })
.contains(tn)
.should('exist')
.first()
.click({ force: true });
cy.get(`.project-tab`).contains(tn, { timeout: 10000 }).should("exist");
cy.get(`.project-tab`).contains(tn).should("exist");
cy.get(".nc-project-tree")
.find(".v-list-item__title:contains(Tables)", { timeout: 10000 })
.find(".v-list-item__title:contains(Tables)")
.should('exist')
.first()
.click({ force: true });
@ -183,7 +185,7 @@ Cypress.Commands.add("openTableTab", (tn, rc) => {
Cypress.Commands.add("closeTableTab", (tn) => {
cy.task("log", `[closeTableTab] ${tn}`);
cy.get(`.project-tab`).contains(tn, { timeout: 10000 }).should("exist");
cy.get(`.project-tab`).contains(tn).should("exist");
cy.get(`[href="#table||||${tn}"]`).find("button.mdi-close").click();
});
@ -221,7 +223,7 @@ Cypress.Commands.add("openOrCreateGqlProject", (_args) => {
cy.contains("GRAPHQL APIs").closest("label").click();
cy.get(".database-field input").click().clear().type("sakila");
cy.contains("Test Database Connection").click();
cy.contains("Ok & Save Project", { timeout: 3000 }).click();
cy.contains("Ok & Save Project").should('exist').click();
}
}
});
@ -280,11 +282,13 @@ Cypress.Commands.add("createTable", (name) => {
Cypress.Commands.add("deleteTable", (name, dbType) => {
cy.get(".nc-project-tree")
.find(".v-list-item__title:contains(Tables)", { timeout: 10000 })
.find(".v-list-item__title:contains(Tables)")
.should('exist')
.first()
.click();
cy.get(".nc-project-tree")
.contains(name, { timeout: 6000 })
.contains(name)
.should('exist')
.first()
.click({ force: true });
cy.get(`.project-tab:contains(${name}):visible`).should("exist");
@ -301,13 +305,15 @@ Cypress.Commands.add("deleteTable", (name, dbType) => {
Cypress.Commands.add("renameTable", (oldName, newName) => {
// expand project tree
cy.get(".nc-project-tree")
.find(".v-list-item__title:contains(Tables)", { timeout: 10000 })
.find(".v-list-item__title:contains(Tables)")
.should('exist')
.first()
.click();
// right click on project table name
cy.get(".nc-project-tree")
.contains(oldName, { timeout: 6000 })
.contains(oldName)
.should('exist')
.first()
.rightclick();
@ -328,18 +334,23 @@ Cypress.Commands.add("renameTable", (oldName, newName) => {
// close expanded project tree
cy.get(".nc-project-tree")
.find(".v-list-item__title:contains(Tables)", { timeout: 10000 })
.find(".v-list-item__title:contains(Tables)")
.should('exist')
.first()
.click();
cy.wait(8000)
});
Cypress.Commands.add("createColumn", (table, columnName) => {
cy.get(".nc-project-tree")
.find(".v-list-item__title:contains(Tables)", { timeout: 10000 })
.find(".v-list-item__title:contains(Tables)")
.should('exist')
.first()
.click();
cy.get(".nc-project-tree")
.contains(table, { timeout: 6000 })
.contains(table)
.should('exist')
.first()
.click({ force: true });
cy.get(`.project-tab:contains(${table}):visible`).should("exist");
@ -366,7 +377,7 @@ Cypress.Commands.add("openViewsTab", (vn, rc) => {
cy.task("log", `[openViewsTab] ${vn} ${rc}`);
cy.get(".nc-project-tree")
.find(".v-list-item__title:contains(Tables)", { timeout: 10000 })
.find(".v-list-item__title:contains(Tables)")
.should("exist")
.first()
.click({ force: true });
@ -376,10 +387,11 @@ Cypress.Commands.add("openViewsTab", (vn, rc) => {
.first()
.click({ force: true });
cy.get(`.project-tab`).contains(vn, { timeout: 10000 }).should("exist");
cy.get(`.project-tab`).contains(vn).should("exist");
cy.get(".nc-project-tree")
.find(".v-list-item__title:contains(Tables)", { timeout: 10000 })
.find(".v-list-item__title:contains(Tables)")
.should('exist')
.first()
.click({ force: true });
@ -391,7 +403,7 @@ Cypress.Commands.add("openViewsTab", (vn, rc) => {
Cypress.Commands.add("closeViewsTab", (vn) => {
cy.task("log", `[closeViewsTab] ${vn}`);
cy.get(`.project-tab`).contains(vn, { timeout: 10000 }).should("exist");
cy.get(`.project-tab`).contains(vn).should("exist");
cy.get(`[href="#view||||${vn}"]`)
.find("button.mdi-close")
.click({ force: true });

Loading…
Cancel
Save