Browse Source

Feat - Next release improvements and bug fixes (#2120)

* refactor: include log lev3l in progress

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: migration logs classification

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: escape `?` in query

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: handle leading/trailing whitespace in table name

re #2073

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: replace knex.raw

replace `knex.raw` with `knex.from` since response is different for each client

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: created time & modified time handling as dateTime datatype

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix:  at import issue and data list api corrections

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: richtext migration support

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: filter to ignore dateTime datatype along with date datatype

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: replace all occurance of . from column name

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* refactoring

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: correction in read api

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: keep correct dtxp value

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: replace ? with _ during column name processing

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* refactoring

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: allow singleLineText to text type instead of tinytext

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* chore: start scripts for pg

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* refactor: thumbnail size increased by 3x

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: exclude whitespace from table name and single select rendering correction

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: execute without extracting raw query in pg

Signed-off-by: Pranav C <pranavxc@gmail.com>

* chore: upgrade nc-help

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: replace special characters in column name with an _

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: handle duplicate table name

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: replace , in select options with a .

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: for title, trim only spaces

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: multiselect and single select import and rendering

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: unique column name generator

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: mmlist query correction

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: use common function for column/table name generation

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: type correction

Signed-off-by: Pranav C <pranavxc@gmail.com>

* chore: upgrade nc-help

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: form view field alias & help text migration

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: handle column name referenced by $

* refactor: rename system field, change its position during creation

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: replace . in column name with _

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: skip rollup for checkbox

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: replace title with id's in viewRowData APIs

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* chore: code cleanup and we are hiring button

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: ignore escaping . in alias

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: headercell overflow

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: support presence of existing tables during migration

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* enhancement: add reach out here link

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: skip default value configuration during import

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* enhancement: webhook prefill default values

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: add missing component properties

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: missing gallery view cover image

re # 2099

Signed-off-by: Pranav C <pranavxc@gmail.com>

* cache: fix view:[object Object]

* fix: hide websocket button and other buttons from shared form view

re # 2107

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: hide virtual columns which are not relevant in expanded form(add)

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: handle view cache based on returned value

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: add galleryViewGet permission for roles

Signed-off-by: Pranav C <pranavxc@gmail.com>

* chore: upgrade nc-help

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: add beta label

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: add beta label

Signed-off-by: Pranav C <pranavxc@gmail.com>

* test/cypress: fix- corrections for baseShare UI change

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* test/cypress: fix view menu count

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: disable default autocomplete

Signed-off-by: Pranav C <pranavxc@gmail.com>

* chore: update docs link

Signed-off-by: Pranav C <pranavxc@gmail.com>

Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com>
Co-authored-by: Wing-Kam Wong <wingkwong.code@gmail.com>
pull/2124/head
Pranav C 3 years ago committed by GitHub
parent
commit
5d44848cae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      .run/Run NocoDB Sqlite.run.xml
  2. 3
      packages/nc-gui/components/ProjectTabs.vue
  3. 29
      packages/nc-gui/components/ProjectTreeView.vue
  4. 24
      packages/nc-gui/components/import/ImportFromAirtable.vue
  5. 24
      packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue
  6. 15
      packages/nc-gui/components/project/spreadsheet/components/CodeSnippet.vue
  7. 16
      packages/nc-gui/components/project/spreadsheet/components/ExpandedForm.vue
  8. 220
      packages/nc-gui/components/project/spreadsheet/components/FieldsMenu.vue
  9. 118
      packages/nc-gui/components/project/spreadsheet/components/SpreadsheetNavDrawer.vue
  10. 9
      packages/nc-gui/components/project/spreadsheet/components/VirtualHeaderCell.vue
  11. 2
      packages/nc-gui/components/project/spreadsheet/components/cell/EnumCell.vue
  12. 5
      packages/nc-gui/components/project/spreadsheet/components/cell/SetListCell.vue
  13. 8
      packages/nc-gui/components/project/spreadsheet/components/editableCell/EditableAttachmentCell.vue
  14. 6
      packages/nc-gui/components/project/spreadsheet/components/editableCell/EnumListEditableCell.vue
  15. 9
      packages/nc-gui/components/project/spreadsheet/components/editableCell/SetListEditableCell.vue
  16. 10
      packages/nc-gui/components/project/spreadsheet/views/GalleryView.vue
  17. 3
      packages/nc-gui/components/project/tableTabs/webhook/HttpWebhook.vue
  18. 7
      packages/nc-gui/components/project/tableTabs/webhook/WebhookEditor.vue
  19. 13
      packages/nc-gui/components/project/tableTabs/webhook/WebhookSlider.vue
  20. 9
      packages/nc-gui/components/utils/DlgTableCreate.vue
  21. 7
      packages/nc-gui/helpers/weAreHiring.js
  22. 4
      packages/nc-gui/layouts/default.vue
  23. 15
      packages/nc-gui/pages/projects/index.vue
  24. 4
      packages/noco-docs/nuxt.config.js
  25. 37
      packages/noco-docs/plugins/nc.js
  26. 3
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  27. 4
      packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts
  28. 1612
      packages/nocodb/package-lock.json
  29. 4
      packages/nocodb/package.json
  30. 59
      packages/nocodb/src/example/dockerRunPG.ts
  31. 62
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSqlv2.ts
  32. 12
      packages/nocodb/src/lib/noco-jobs/JobsMgr.ts
  33. 7
      packages/nocodb/src/lib/noco-models/Model.ts
  34. 5
      packages/nocodb/src/lib/noco-models/View.ts
  35. 73
      packages/nocodb/src/lib/noco/Noco.ts
  36. 5
      packages/nocodb/src/lib/noco/meta/api/columnApis.ts
  37. 36
      packages/nocodb/src/lib/noco/meta/api/swagger/redocHtml.ts
  38. 33
      packages/nocodb/src/lib/noco/meta/api/swagger/swaggerHtml.ts
  39. 473
      packages/nocodb/src/lib/noco/meta/api/sync/helpers/job.ts
  40. 5
      packages/nocodb/src/lib/noco/meta/api/sync/importApis.ts
  41. 13
      packages/nocodb/src/lib/noco/meta/api/tableApis.ts
  42. 0
      packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts
  43. 2
      packages/nocodb/src/lib/noco/meta/helpers/webhookHelpers.ts
  44. 5
      packages/nocodb/src/lib/utils/projectAcl.ts
  45. 13
      packages/nocodb/src/lib/utils/weAreHiring.ts
  46. 2
      scripts/cypress/integration/common/5a_user_role.js
  47. 2
      scripts/cypress/integration/common/5b_preview_role.js
  48. 2
      scripts/cypress/integration/common/6g_base_share.js
  49. 5
      scripts/cypress/integration/spec/roleValidation.spec.js

15
.run/Run NocoDB Sqlite.run.xml

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

3
packages/nc-gui/components/ProjectTabs.vue

@ -54,6 +54,7 @@
:key="`${pid}||${(tab._nodes && tab._nodes).type || ''}||${
(tab._nodes && tab._nodes.dbAlias) || ''
}||${tab.name}`"
class="nc-main-tab-item"
:value="`${(tab._nodes && tab._nodes.type) || ''}||${
(tab._nodes && tab._nodes.dbAlias) || ''
}||${tab.name}`"
@ -670,7 +671,7 @@ export default {
margin-left: 0 !important;
}
/deep/ .v-window-item:not(.v-window-item--active){
/deep/ .nc-main-tab-item:not(.v-window-item--active){
display:none;
}
</style>

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

@ -65,7 +65,7 @@
type="list-item,list-item-three-line@3,list-item@2,list-item-three-line@3"
/>
<v-treeview
<!-- <v-treeview
v-else-if="isTreeView"
v-model="tree"
class="mt-5 project-tree nc-project-tree"
@ -96,9 +96,9 @@
<v-icon size="16">
mdi-database
</v-icon>
<!-- <img-->
<!-- class="grey lighten-3"-->
<!-- :width="16" :src="`/db-icons/${dbIcons[item._nodes.dbConnection.client]}`"/>-->
&lt;!&ndash; <img&ndash;&gt;
&lt;!&ndash; class="grey lighten-3"&ndash;&gt;
&lt;!&ndash; :width="16" :src="`/db-icons/${dbIcons[item._nodes.dbConnection.client]}`"/>&ndash;&gt;
</template>
<template v-else>
<v-icon
@ -130,7 +130,7 @@
<span>{{ item.tooltip || item.name }}</span>
</v-tooltip>
</template>
</v-treeview>
</v-treeview>-->
<v-container v-else fluid class="px-1 pt-0">
<v-list
height="30"
@ -159,7 +159,7 @@
<template #activator>
<v-list-item-icon>
<v-icon
v-if="open && icons[item._nodes.type].openIcon"
v-if="icons[item._nodes.type].openIcon"
small
style="cursor: auto"
:color="icons[item._nodes.type].openColor"
@ -436,7 +436,7 @@
>
<v-list-item-icon>
<v-icon
v-if="open && icons[item._nodes.type].openIcon"
v-if="icons[item._nodes.type].openIcon"
small
style="cursor: auto"
:color="icons[item._nodes.type].openColor"
@ -613,7 +613,9 @@
<span class="font-weight-regular caption">{{
$t("title.audit")
}}</span>
</v-list-item-title>
</v-list-item-title
</v-list-item
>
</v-list-item>
</template>
<!-- Meta Management -->
@ -857,7 +859,6 @@ export default {
},
loadingProjects: true,
caseInsensitive: true,
open: [],
search: null,
menuVisible: false,
quickImportDialog: false,
@ -1197,31 +1198,31 @@ export default {
const currentlyOpened = JSON.parse(JSON.stringify(this.open));
currentlyOpened.push(item._nodes.key);
this.activeListItem = item._nodes.key;
this.open = currentlyOpened;
// this.open = currentlyOpened;
} else if (item._nodes.type === "viewDir" && !open) {
await this.loadViews(item);
const currentlyOpened = JSON.parse(JSON.stringify(this.open));
currentlyOpened.push(item._nodes.key);
this.activeListItem = item._nodes.key;
this.open = currentlyOpened;
// this.open = currentlyOpened;
} else if (item._nodes.type === "functionDir" && !open) {
await this.loadFunctions(item);
const currentlyOpened = JSON.parse(JSON.stringify(this.open));
currentlyOpened.push(item._nodes.key);
this.activeListItem = item._nodes.key;
this.open = currentlyOpened;
// this.open = currentlyOpened;
} else if (item._nodes.type === "procedureDir" && !open) {
await this.loadProcedures(item);
const currentlyOpened = JSON.parse(JSON.stringify(this.open));
currentlyOpened.push(item._nodes.key);
this.activeListItem = item._nodes.key;
this.open = currentlyOpened;
// this.open = currentlyOpened;
} else if (item._nodes.type === "sequenceDir" && !open) {
await this.loadSequences(item);
const currentlyOpened = JSON.parse(JSON.stringify(this.open));
currentlyOpened.push(item._nodes.key);
this.activeListItem = item._nodes.key;
this.open = currentlyOpened;
// this.open = currentlyOpened;
} else if (item._nodes.type === "env") {
return;
} else {

24
packages/nc-gui/components/import/ImportFromAirtable.vue

@ -14,13 +14,18 @@
<v-divider />
<div class="h-100" style="width: 100%">
<div>
<v-card v-if="step === 1" class="py-6 elevation-0" height="500">
<v-card
:class="{'pb-4 mt-4' : step === 2, 'py-6': step === 1}"
class=" elevation-0"
min-height="500"
>
<template v-if="step === 1">
<div class="d-flex flex-column justify-center align-center pt-2 pb-6">
<span class="subtitle-1 font-weight-medium" @dblclick="$set(syncSource.details,'syncViews',true)">
Credentials
</span>
<a href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free" class="caption grey--text" target="_blank">Where to find this?</a>
<a href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials-for-importing-to-nocodb" class="caption grey--text" target="_blank">Where to find this?</a>
</div>
<v-form v-model="valid">
@ -32,6 +37,7 @@
label="Api Key"
class="caption nc-input-api-key"
:type="isPasswordVisible ? 'text':'password'"
autocomplete="off"
:rules="[v=> !!v || 'Api Key is required']"
>
<template #append="">
@ -61,11 +67,9 @@
Import
</v-btn>
</v-card-actions>
</v-card>
<v-card
v-if="step === 2"
class="pb-4 mt-4 elevation-0"
</template>
<template
v-else-if="step === 2"
>
<v-card-title class=" justify-center">
<span class="subtitle-1 font-weight-medium">Logs</span>
@ -108,6 +112,12 @@
Go to dashboard
</v-btn>
</div>
</template>
<div class="text-center pa-4 pb-0">
<a class="caption grey--text" href="https://github.com/nocodb/nocodb/issues/2052" target="_blank">Questions / Help - reach out here</a>
<br>
<span class="caption grey--text"> This feature is currently in beta and more information can be found <a href="https://github.com/nocodb/nocodb/discussions/2122" class="caption grey--text" target="_blank">here</a>.</span>
</div>
</v-card>
</div>
</div>

24
packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue

@ -1102,8 +1102,8 @@ export default {
const insertedData = await this.$api.dbViewRow.create(
'noco',
this.projectName,
this.meta.title,
this.selectedView.title,
this.meta.id,
this.selectedView.id,
insertObj
)
@ -1192,8 +1192,8 @@ export default {
const newData = await this.$api.dbViewRow.update(
'noco',
this.projectName,
this.meta.title,
this.selectedView.title,
this.meta.id,
this.selectedView.id,
id,
{
[column.title]: rowObj[column.title]
@ -1255,8 +1255,8 @@ export default {
await this.$api.dbViewRow.delete(
'noco',
this.projectName,
this.meta.title,
this.selectedView.title,
this.meta.id,
this.selectedView.id,
id
)
}
@ -1292,8 +1292,8 @@ export default {
await this.$api.dbViewRow.delete(
'noco',
this.projectName,
this.meta.title,
this.selectedView.title,
this.meta.id,
this.selectedView.id,
id
)
}
@ -1426,8 +1426,8 @@ export default {
const { list, pageInfo } = await this.$api.dbViewRow.list(
'noco',
this.projectName,
this.meta.title,
this.selectedView.title,
this.meta.id,
this.selectedView.id,
this.listQueryParams
)
@ -1685,8 +1685,8 @@ export default {
const { count } = await this.$api.dbViewRow.count(
'noco',
this.$store.getters['project/GtrProjectName'],
this.meta.title,
this.selectedView.title
this.meta.id,
this.selectedView.id
)
this.count = count
}

15
packages/nc-gui/components/project/spreadsheet/components/CodeSnippet.vue

@ -1,6 +1,7 @@
<template>
<div class="nc-container" :class="{active:modal}" @click="modal=false">
<div class="nc-snippet elevation-3 pa-4" @click.stop>
<div class="nc-snippet elevation-3 pa-4 d-flex flex-column" @click.stop>
<div>
<h3 class="font-weight-medium mb-4">
Code Snippet
</h3>
@ -71,6 +72,18 @@
</div>
</div>
</div>
<v-spacer />
<v-btn
v-t="['e:hiring']"
color="primary"
outlined
class="caption my-2 mx-auto"
href="https://angel.co/company/nocodb"
target="_blank"
>
🚀 We are Hiring! 🚀
</v-btn>
</div>
</div>
</template>

16
packages/nc-gui/components/project/spreadsheet/components/ExpandedForm.vue

@ -400,20 +400,19 @@ export default {
return !!Object.keys(this.changedColumns).length
},
fields() {
let fields
if (this.availableColumns) {
return this.availableColumns
}
//
// const hideCols = ['created_at', 'updated_at']
if (this.showSystemFields) {
return this.meta.columns || []
fields = this.availableColumns
} else if (this.showSystemFields) {
fields = this.meta.columns || []
} else {
return (
fields = (
this.meta.columns.filter(
c => !isSystemColumn(c)
) || []
)
}
return this.isNew ? fields.filter(f => ![UITypes.Formula, UITypes.Rollup, UITypes.Lookup].includes(f.uidt)) : fields
},
isChanged() {
return Object.values(this.changedColumns).some(Boolean)
@ -559,7 +558,8 @@ export default {
value: updatedObj[key],
prev_value: this.oldRow[key]
})
.then(() => {})
.then(() => {
})
}
} else {
return this.$toast.info('No columns to update').goAway(3000)

220
packages/nc-gui/components/project/spreadsheet/components/FieldsMenu.vue

@ -14,16 +14,20 @@
}"
v-on="on"
>
<v-icon small class="mr-1" color="#777"> mdi-eye-off-outline </v-icon>
<v-icon small class="mr-1" color="#777">
mdi-eye-off-outline
</v-icon>
<!-- Fields -->
{{ $t("objects.fields") }}
<v-icon small color="#777"> mdi-menu-down </v-icon>
<v-icon small color="#777">
mdi-menu-down
</v-icon>
</v-btn>
</v-badge>
</template>
<v-list dense class="pt-0" min-width="280" @click.stop>
<template v-if="isGallery">
<template v-if="isGallery && _isUIAllowed('updateCoverImage')">
<div class="pa-2">
<v-select
v-model="coverImageFieldLoc"
@ -38,7 +42,9 @@
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon"> mdi-image </v-icon>
<v-icon small class="field-icon">
mdi-image
</v-icon>
</template>
</v-select>
</div>
@ -60,7 +66,9 @@
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon"> mdi-select-group </v-icon>
<v-icon small class="field-icon">
mdi-select-group
</v-icon>
</template>
</v-select>
</div>
@ -162,13 +170,13 @@
</template>
<script>
import draggable from "vuedraggable";
import { getSystemColumnsIds } from "nocodb-sdk";
import draggable from 'vuedraggable'
import { getSystemColumnsIds } from 'nocodb-sdk'
export default {
name: "FieldsMenu",
name: 'FieldsMenu',
components: {
draggable,
draggable
},
props: {
coverImageField: String,
@ -182,278 +190,278 @@ export default {
fieldList: [Array, Object],
showSystemFields: {
type: [Boolean, Number],
default: false,
default: false
},
isLocked: Boolean,
isPublic: Boolean,
viewId: String,
viewId: String
},
data: () => ({
fields: [],
fieldFilter: "",
fieldFilter: '',
showFields: {},
fieldsOrderLoc: [],
fieldsOrderLoc: []
}),
computed: {
systemColumnsIds() {
return getSystemColumnsIds(this.meta && this.meta.columns);
return getSystemColumnsIds(this.meta && this.meta.columns)
},
attachmentFields() {
return [
...(this.meta && this.meta.columns
? this.meta.columns.filter((f) => f.uidt === "Attachment")
? this.meta.columns.filter(f => f.uidt === 'Attachment')
: []),
{
alias: "None",
id: null,
},
];
alias: 'None',
id: null
}
]
},
singleSelectFields() {
return [
...(this.meta && this.meta.columns
? this.meta.columns.filter((f) => f.uidt === "SingleSelect")
? this.meta.columns.filter(f => f.uidt === 'SingleSelect')
: []),
{
alias: "None",
id: null,
},
];
alias: 'None',
id: null
}
]
},
coverImageFieldLoc: {
get() {
return this.coverImageField;
return this.coverImageField
},
set(val) {
this.$emit("update:coverImageField", val);
},
this.$emit('update:coverImageField', val)
}
},
groupingFieldLoc: {
get() {
return this.groupingField;
return this.groupingField
},
set(val) {
this.$emit("update:groupingField", val);
},
this.$emit('update:groupingField', val)
}
},
columnMeta() {
return this.meta && this.meta.columns
? this.meta.columns.reduce(
(o, c) => ({
...o,
[c.title]: c,
[c.title]: c
}),
{}
)
: {};
: {}
},
isAnyFieldHidden() {
return this.fields.some(
(f) =>
f =>
!(
!this.showSystemFieldsLoc &&
this.systemColumnsIds.includes(f.fk_column_id)
) && !f.show
); // Object.values(this.showFields).some(v => !v)
) // Object.values(this.showFields).some(v => !v)
},
showSystemFieldsLoc: {
get() {
return this.showSystemFields;
return this.showSystemFields
},
set(v) {
this.$emit("update:showSystemFields", v);
this.$emit('update:showSystemFields', v)
this.showFields = this.fields.reduce(
(o, c) => ({ [c.title]: c.show, ...o }),
{}
);
)
this.$emit(
"update:fieldsOrder",
this.fields.map((c) => c.title)
);
'update:fieldsOrder',
this.fields.map(c => c.title)
)
this.$e("a:fields:system-fields");
},
},
this.$e('a:fields:system-fields')
}
}
},
watch: {
async viewId(v) {
if (v) {
await this.loadFields();
await this.loadFields()
}
},
fieldList(f) {
this.fieldsOrderLoc = [...f];
this.fieldsOrderLoc = [...f]
},
showFields: {
handler(v) {
this.$nextTick(() => {
this.$emit("input", v);
});
this.$emit('input', v)
})
},
deep: true,
deep: true
},
value(v) {
this.showFields = v || [];
this.showFields = v || []
},
fieldsOrder(n, o) {
if ((n && n.join()) !== (o && o.join())) {
this.fieldsOrderLoc = n;
this.fieldsOrderLoc = n
}
this.fieldsOrderLoc = n && n.length ? n : [...this.fieldList];
this.fieldsOrderLoc = n && n.length ? n : [...this.fieldList]
},
fieldsOrderLoc: {
handler(n, o) {
if ((n && n.join()) !== (o && o.join())) {
this.$emit("update:fieldsOrder", n);
this.$emit('update:fieldsOrder', n)
}
},
deep: true,
},
deep: true
}
},
created() {
this.loadFields();
this.showFields = this.value;
this.loadFields()
this.showFields = this.value
this.fieldsOrderLoc =
this.fieldsOrder && this.fieldsOrder.length
? this.fieldsOrder
: [...this.fieldList];
: [...this.fieldList]
},
methods: {
async loadFields() {
let fields = [];
let order = 1;
let fields = []
let order = 1
if (this.viewId) {
const data = await this.$api.dbViewColumn.list(this.viewId);
const data = await this.$api.dbViewColumn.list(this.viewId)
const fieldById = data.reduce(
(o, f) => ({
...o,
[f.fk_column_id]: f,
[f.fk_column_id]: f
}),
{}
);
)
fields = this.meta.columns
.map((c) => ({
.map(c => ({
title: c.title,
fk_column_id: c.id,
...(fieldById[c.id] ? fieldById[c.id] : {}),
order: (fieldById[c.id] && fieldById[c.id].order) || order++,
order: (fieldById[c.id] && fieldById[c.id].order) || order++
}))
.sort((a, b) => a.order - b.order);
.sort((a, b) => a.order - b.order)
} else if (this.isPublic) {
fields = this.meta.columns;
fields = this.meta.columns
}
this.fields = fields;
this.fields = fields
this.$emit(
"input",
'input',
this.fields.reduce(
(o, c) => ({
...o,
[c.title]: c.show,
[c.title]: c.show
}),
{}
)
);
)
this.$emit(
"update:fieldsOrder",
this.fields.map((c) => c.title)
);
'update:fieldsOrder',
this.fields.map(c => c.title)
)
},
async saveOrUpdate(field, i) {
if (!this.isPublic && this._isUIAllowed("fieldsSync")) {
if (!this.isPublic && this._isUIAllowed('fieldsSync')) {
if (field.id) {
await this.$api.dbViewColumn.update(this.viewId, field.id, field);
await this.$api.dbViewColumn.update(this.viewId, field.id, field)
} else {
this.fields[i] = await this.$api.dbViewColumn.create(
this.viewId,
field
);
)
}
}
this.$emit("updated");
this.$emit('updated')
this.$emit(
"input",
'input',
this.fields.reduce(
(o, c) => ({
...o,
[c.title]: c.show,
[c.title]: c.show
}),
{}
)
);
)
this.$emit(
"update:fieldsOrder",
this.fields.map((c) => c.title)
);
'update:fieldsOrder',
this.fields.map(c => c.title)
)
this.$e("a:fields:show-hide");
this.$e('a:fields:show-hide')
},
async showAll() {
if (!this.isPublic) {
await this.$api.dbView.showAllColumn(this.viewId);
await this.$api.dbView.showAllColumn(this.viewId)
}
for (const f of this.fields) {
f.show = true;
f.show = true
}
this.$emit("updated");
this.$emit('updated')
// eslint-disable-next-line no-return-assign,no-sequences
this.showFields = (
this.fieldsOrderLoc || Object.keys(this.showFields)
).reduce((o, k) => ((o[k] = true), o), {});
).reduce((o, k) => ((o[k] = true), o), {})
this.$e("a:fields:show-all");
this.$e('a:fields:show-all')
},
async hideAll() {
if (!this.isPublic) {
await this.$api.dbView.hideAllColumn(this.viewId);
await this.$api.dbView.hideAllColumn(this.viewId)
}
for (const f of this.fields) {
f.show = false;
f.show = false
}
this.$emit("updated");
this.$emit('updated')
this.$nextTick(() => {
this.showFields = (
this.fieldsOrderLoc || Object.keys(this.showFields)
).reduce((o, k) => ((o[k] = false), o), {});
});
).reduce((o, k) => ((o[k] = false), o), {})
})
this.$e("a:fields:hide-all");
this.$e('a:fields:hide-all')
},
onMove(event) {
if (this.fields.length - 1 === event.moved.newIndex) {
this.$set(
this.fields[event.moved.newIndex],
"order",
'order',
this.fields[event.moved.newIndex - 1].order + 1
);
)
} else if (event.moved.newIndex === 0) {
this.$set(
this.fields[event.moved.newIndex],
"order",
'order',
this.fields[1].order / 2
);
)
} else {
this.$set(
this.fields[event.moved.newIndex],
"order",
'order',
(this.fields[event.moved.newIndex - 1].order +
this.fields[event.moved.newIndex + 1].order) /
2
);
)
}
this.saveOrUpdate(
this.fields[event.moved.newIndex],
event.moved.newIndex
);
this.$e("a:fields:reorder");
},
},
};
)
this.$e('a:fields:reorder')
}
}
}
</script>
<style scoped lang="scss">

118
packages/nc-gui/components/project/spreadsheet/components/SpreadsheetNavDrawer.vue

@ -273,7 +273,7 @@
</template>
</div>
<div>
<div v-if="!isSharedBase">
<v-btn
v-t="['c:snippet:open']"
color="primary"
@ -286,7 +286,7 @@
</v-btn>
<code-snippet v-model="codeSnippetModal" :query-params="queryParams" :meta="meta" :view="selectedView" />
</div>
<div>
<div v-if="_isUIAllowed('webhook')" class="mb-2">
<v-btn
v-t="['c:actions:webhook']"
color="primary"
@ -302,7 +302,7 @@
</div>
<div
v-if="time - $store.state.settings.miniSponsorCard > 15 * 60 * 1000"
v-if="!isSharedBase && time - $store.state.settings.miniSponsorCard > 15 * 60 * 1000"
class="py-2 sponsor-wrapper"
>
<v-icon small class="close-icon" @click="hideMiniSponsorCard">
@ -310,7 +310,7 @@
</v-icon>
<!-- <extras />-->
<v-divider class="my-2" />
<v-divider class="mb-2" />
<extras class="pl-1" />
<v-btn
@ -326,113 +326,6 @@
<!-- <sponsor-mini nav />-->
</div>
<!--<div class="text-center">
<v-hover >
<template v-slot:default="{hover}">
<v-btn
:color="hover ?'primary' : 'grey'" class="mb-2" small outlined href="https://github.com/sponsors/nocodb"
target="_blank">
<v-icon small color="red" class="mr-2">mdi-heart-outline</v-icon>
Sponsor Us
</v-btn>
</template>
</v-hover>
</div>
-->
<!-- <div v-if="_isUIAllowed('table-advanced')">
<v-divider />
<v-list
dense
:class="{
'advanced-border': overAdvShieldIcon,
}"
>
<v-list-item dense>
<span
class="body-2 font-weight-medium"
@dblclick="$emit('update:showAdvanceOptions', !showAdvanceOptions)"
>Advanced</span>
<v-tooltip top>
<template #activator="{ on }">
<x-icon
color="pink textColor"
icon-class="ml-2"
small
v-on="on"
@mouseenter="overAdvShieldIcon = true"
@mouseleave="overAdvShieldIcon = false"
>
mdi-shield-lock-outline
</x-icon>
</template>
<span class="caption">
&lt;!&ndash; Only visible to Creator &ndash;&gt;
{{ $t('msg.info.onlyCreator') }}
</span>
</v-tooltip>
</v-list-item>
&lt;!&ndash; <v-tooltip bottom>&ndash;&gt;
&lt;!&ndash; <template v-slot:activator="{on}">&ndash;&gt;
&lt;!&ndash; <v-menu offset-x left>&ndash;&gt;
&lt;!&ndash; <template v-slot:activator="{on}">&ndash;&gt;
&lt;!&ndash;
TODO:
- Add selectedView.show_as === 'kanban' when it is ready
&ndash;&gt;
&lt;!&ndash; <v-list-item
v-show="
selectedView && (selectedView.type === 'view' || selectedView.type === 'table' || selectedView.show_as === 'form' || selectedView.show_as === 'grid' )
"
v-if="_isUIAllowed('shareview')"
@click="genShareLink"
>
<v-icon x-small class="mr-2 nc-share-view">
mdi-open-in-new
</v-icon>
<span class="caption">
&lt;!&ndash; Share View &ndash;&gt;
{{ $t('activity.shareView') }}
</span>
<v-spacer />
<v-menu offset-y>
<template #activator="{ on }">
<v-icon small @click.stop v-on="on">
mdi-dots-vertical
</v-icon>
</template>
<v-list dense>
<v-list-item dense @click="$emit('showAdditionalFeatOverlay', 'shared-views')">
<v-list-item-title>
<span class="font-weight-regular">
&lt;!&ndash; Views List &ndash;&gt;
{{ $t('activity.ListView') }}
</span>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-list-item>&ndash;&gt;
&lt;!&ndash; <v-tooltip bottom>&ndash;&gt;
&lt;!&ndash; <template #activator="{ on }">&ndash;&gt;
&lt;!&ndash; <v-list-item v-on="on" @click="copyapiUrlToClipboard">&ndash;&gt;
&lt;!&ndash; <v-icon x-small class="mr-2">&ndash;&gt;
&lt;!&ndash; mdi-content-copy&ndash;&gt;
&lt;!&ndash; </v-icon>&ndash;&gt;
&lt;!&ndash; &lt;!&ndash; Copy API URL &ndash;&gt;&ndash;&gt;
&lt;!&ndash; <span class="caption">{{ $t('activity.ListView') }}</span>&ndash;&gt;
&lt;!&ndash; </v-list-item>&ndash;&gt;
&lt;!&ndash; </template>&ndash;&gt;
&lt;!&ndash; &lt;!&ndash; Copy API URL &ndash;&gt;&ndash;&gt;
&lt;!&ndash; {{ $t('activity.ListView') }}&ndash;&gt;
&lt;!&ndash; </v-tooltip>&ndash;&gt;
<template v-if="_isUIAllowed('model')">
&lt;!&ndash; <v-divider class="advance-menu-divider" />&ndash;&gt;
<slot />
</template>
</v-list>
</div>-->
</div>
</v-container>
@ -620,6 +513,9 @@ export default {
}
}),
computed: {
isSharedBase() {
return this.$route.params && this.$route.params.shared_base_id
},
viewsList: {
set(v) {
this.$emit('update:views', v)

9
packages/nc-gui/components/project/spreadsheet/components/VirtualHeaderCell.vue

@ -36,10 +36,9 @@
{{ rollupIcon }}
</v-icon>
</template>
<span v-on="on">
<span class="name flex-grow-1" style="white-space: nowrap" :title="column.title" v-html="alias" />
<span v-if="column.rqd || required" class="error--text text--lighten-1">&nbsp;*</span>
</span>
<span class="name" style="white-space: nowrap" :title="column.title" v-on="on" v-html="alias" />
<span v-if="column.rqd || required" class="error--text text--lighten-1" v-on="on">&nbsp;*</span>
</template>
<span class="caption" v-html="tooltipMsg" />
</v-tooltip>
@ -258,9 +257,9 @@ export default {
<style scoped>
.name {
max-width: calc(100% - 40px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
<!--

2
packages/nc-gui/components/project/spreadsheet/components/cell/EnumCell.vue

@ -1,7 +1,7 @@
<template>
<div>
<span
v-for="v in (value||'').split(',')"
v-for="v in [(value||'').replace(/\\'/g, '\'').replace(/^'|'$/g, '')]"
:key="v"
:style="{
background:colors[v]

5
packages/nc-gui/components/project/spreadsheet/components/cell/SetListCell.vue

@ -1,7 +1,7 @@
<template>
<div>
<v-chip
v-for="v in (value || '').split(',')"
v-for="v in selectedValues"
v-show="v || setValues.includes(v)"
:key="v"
small
@ -26,6 +26,9 @@ export default {
return this.column.dtxp.split(',').map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, ''))
}
return []
},
selectedValues() {
return this.value ? this.value.split(',').map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, '')) : []
}
}
}

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

@ -30,7 +30,7 @@
v-if="isImage(item.title)"
lazy-src="https://via.placeholder.com/60.png?text=Loading..."
alt="#"
max-height="33px"
max-height="99px"
contain
:src="item.url || item.data"
v-on="on"
@ -418,15 +418,15 @@ export default {
}
.thumbnail {
height: 33px;
width: 33px;
height: 99px;
width: 99px;
margin: 2px;
border-radius: 4px;
}
.thumbnail img {
/*max-height: 33px;*/
max-width: 33px;
max-width: 99px;
}
.main {

6
packages/nc-gui/components/project/spreadsheet/components/editableCell/EnumListEditableCell.vue

@ -50,7 +50,7 @@ export default {
computed: {
localState: {
get() {
return this.value
return this.value && this.value.replace(/\\'/g, '\'').replace(/^'|'$/g, '')
},
set(val) {
this.$emit('input', val)
@ -59,7 +59,9 @@ export default {
},
enumValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, ''))
return this.column.dtxp
.split(',')
.map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, ''))
}
return []
},

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

@ -1,6 +1,5 @@
<template>
<div>
<v-combobox
v-model="localState"
:items="setValues"
@ -52,7 +51,9 @@ export default {
computed: {
localState: {
get() {
return this.value && this.value.split(',')
return this.value && this.value
.match(/(?:[^',]|\\')+(?='?(?:,|$))/g)
.map(v => v.replace(/\\'/g, '\''))
},
set(val) {
this.$emit('input', val.filter(v => this.setValues.includes(v)).join(','))
@ -61,7 +62,9 @@ export default {
},
setValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, ''))
return this.column.dtxp
.match(/(?:[^']|\\')+(?='?(?:,|$))/g)
.map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, ''))
}
return []
},

10
packages/nc-gui/components/project/spreadsheet/views/GalleryView.vue

@ -139,8 +139,8 @@ export default {
if (this.showSystemFields) {
return this.meta.columns || []
} else {
return this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.column_name) &&
!((this.meta.v || []).some(v => v.bt && v.bt.column_name === c.column_name))
return this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.title) &&
!((this.meta.v || []).some(v => v.bt && v.bt.title === c.title))
) || []
}
}
@ -166,10 +166,10 @@ export default {
},
getCovers(row) {
if (this.attachmentColumn &&
row[this.attachmentColumn.column_name] && row[this.attachmentColumn.column_name][0] &&
row[this.attachmentColumn.column_name]) {
row[this.attachmentColumn.title] && row[this.attachmentColumn.title][0] &&
row[this.attachmentColumn.title]) {
try {
return JSON.parse(row[this.attachmentColumn.column_name])
return JSON.parse(row[this.attachmentColumn.title])
} catch (e) {
}

3
packages/nc-gui/components/project/tableTabs/webhook/HttpWebhook.vue

@ -131,7 +131,8 @@ export default {
response: {},
perf: {},
meta: {}
}
},
tab: 0
}),
computed: {

7
packages/nc-gui/components/project/tableTabs/webhook/WebhookEditor.vue

@ -255,8 +255,13 @@ export default {
meta: Object
},
data: () => ({
notification: {},
loading: false,
notification: {
method: 'POST',
body: '{{ json data }}'
},
hook: {
title: 'Webhook',
notification: {
type: 'URL'
}

13
packages/nc-gui/components/project/tableTabs/webhook/WebhookSlider.vue

@ -1,5 +1,6 @@
<template>
<nc-slider v-model="webhookSlider">
<div style="min-height:calc(100vh - 32px)" class="d-flex flex-column">
<v-card
v-if="webhookSlider"
width="100%"
@ -9,6 +10,18 @@
<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-spacer />
<v-btn
v-t="['e:hiring']"
color="primary"
outlined
class="caption my-2 mx-auto"
href="https://angel.co/company/nocodb"
target="_blank"
>
🚀 We are Hiring! 🚀
</v-btn>
</div>
</nc-slider>
</template>

9
packages/nc-gui/components/utils/DlgTableCreate.vue

@ -197,7 +197,7 @@ export default {
},
watch: {
'table.alias'(v) {
this.$set(this.table, 'name', `${this.projectPrefix || ''}${inflection.underscore(v)}`)
this.$set(this.table, 'name', `${this.projectPrefix || ''}${inflection.underscore(v.replace(/^\s+|\s+$/g, m => new Array(m.length).fill('_').join('')))}`)
}
},
created() {
@ -214,8 +214,8 @@ export default {
methods: {
populateDefaultTitle() {
let c = 1
while (this.tables.some(t => t.title === `sheet${c}`)) { c++ }
this.$set(this.table, 'alias', `sheet${c}`)
while (this.tables.some(t => t.title === `Sheet${c}`)) { c++ }
this.$set(this.table, 'alias', `Sheet${c}`)
},
validateTableName(v) {
return validateTableName(v, this.$store.getters['project/GtrProjectIsGraphql'])
@ -223,6 +223,9 @@ export default {
validateDuplicateAlias(v) {
return (this.tables || []).every(t => t.title !== (v || '')) || 'Duplicate table alias'
},
validateLedingOrTrailingWhiteSpace(v) {
return !/^\s+|\s+$/.test(v) || 'Leading or trailing whitespace not allowed in table name'
},
validateDuplicate(v) {
return (this.tables || []).every(t => t.table_name.toLowerCase() !== (v || '').toLowerCase()) || 'Duplicate table name'
},

7
packages/nc-gui/helpers/weAreHiring.js

@ -0,0 +1,7 @@
export default function weAreHiring() {
const fn = () => {
console.log('%c🚀 We are Hiring!!! 🚀%c\n%cJoin the forces http://careers.nocodb.com', 'color:#1348ba;font-size:3rem;padding:20px;', 'display:none', 'font-size:1.5rem;padding:20px')
}
fn()
setInterval(fn, 300000)
}

4
packages/nc-gui/layouts/default.vue

@ -342,6 +342,7 @@ import Loader from '~/components/Loader'
import PreviewAs from '~/components/PreviewAs'
import ShareOrInviteModal from '~/components/auth/ShareOrInviteModal'
import ImportantAnnouncement from '../components/ImportantAnnouncement.vue'
import weAreHiring from '~/helpers/weAreHiring'
export default {
components: {
@ -448,6 +449,9 @@ export default {
this.selectedEnv = this.$store.getters['project/GtrActiveEnv']
this.loadProjectInfo()
},
created() {
weAreHiring()
},
methods: {
...mapActions({ changeActiveTab: 'tabs/changeActiveTab' }),
...mapMutations({

15
packages/nc-gui/pages/projects/index.vue

@ -538,6 +538,21 @@
</v-list-item-title>
</v-list-item>
</template>
<v-divider />
<v-list-item
v-t="['e:hiring']"
dense
target="_blank"
href="http://careers.nocodb.com"
>
<v-list-item-icon>
<span class="ml-2" style="font-size:20px">🚀</span>
</v-list-item-icon>
<v-list-item-title>
We are Hiring!!!
</v-list-item-title>
</v-list-item>
</v-list>
</div>

4
packages/noco-docs/nuxt.config.js

@ -1,4 +1,5 @@
import theme from '@nuxt/content-theme-docs'
import path from 'path'
export default theme({
docs: {
@ -6,6 +7,9 @@ export default theme({
},
css: [
"./assets/main.css"
],
plugins: [
{src: path.join(__dirname, 'plugins','nc.js'), ssr:false}
]
})

37
packages/noco-docs/plugins/nc.js

@ -0,0 +1,37 @@
console.log('%c🚀 We are Hiring!!! 🚀%c\n%cJoin the forces http://careers.nocodb.com', 'color:#1348ba;font-size:3rem;padding:20px;', 'display:none', 'font-size:1.5rem;padding:20px')
export default () => {
const linkEl = document.createElement('a')
linkEl.setAttribute('href', "http://careers.nocodb.com")
linkEl.setAttribute('target', '_blank')
linkEl.setAttribute('class', 'we-are-hiring')
linkEl.innerHTML = '🚀 We are Hiring!!! 🚀'
const styleEl = document.createElement('style');
styleEl.innerHTML = `
.we-are-hiring {
position: fixed;
bottom: 50px;
right: -250px;
opacity: 0;
background: orange;
border-radius: 4px;
padding: 19px;
z-index: 200;
text-decoration: none;
text-transform: uppercase;
color: black;
transition: 1s opacity, 1s right;
display: block;
font-weight: bold;
}
.we-are-hiring.active {
opacity: 1;
right:25px;
}
`
document.body.appendChild(linkEl, document.body.firstChild)
document.body.appendChild(styleEl, document.body.firstChild)
setTimeout(() => linkEl.classList.add('active'), 2000)
}

3
packages/nocodb-sdk/src/lib/formulaHelpers.ts

@ -84,6 +84,9 @@ export function substituteColumnIdWithAliasInFormula(
c.title === colNameOrId
);
pt.name = column?.title || ptRaw?.name || pt?.name;
if (pt.name[0] != '$' && pt.name[pt.name.length - 1] != '$') {
pt.name = '$' + pt.name + '$';
}
} else if (pt.type === 'BinaryExpression') {
substituteId(pt.left, ptRaw?.left);
substituteId(pt.right, ptRaw?.right);

4
packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts

@ -1,4 +1,4 @@
import UITypes from "../UITypes";
import UITypes from '../UITypes';
import { MssqlUi } from './MssqlUi';
import { MysqlUi } from './MysqlUi';
@ -23,8 +23,6 @@ export class SqlUiFactory {
// if (connectionConfig.meta.dbtype === "vitess")
// return Vitess;
console.log('- - - -In Mysql UI');
return MysqlUi;
}

1612
packages/nocodb/package-lock.json generated

File diff suppressed because it is too large Load Diff

4
packages/nocodb/package.json

@ -70,6 +70,7 @@
"watch:run": "cross-env NC_DISABLE_TELE1=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/example/docker --log-error --project tsconfig.json\"",
"watch:run:cypress": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/example/docker --log-error --project tsconfig.json\"",
"watch:run:mysql": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/example/dockerRunMysql --log-error --project tsconfig.json\"",
"watch:run:pg": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/example/dockerRunPG --log-error --project tsconfig.json\"",
"run": "ts-node src/example/docker",
"watch:try": "nodemon -e ts,js -w ./src -x \"ts-node src/example/try --log-error --project tsconfig.json\"",
"example:docker": "ts-node ./src/example/docker.ts"
@ -119,7 +120,6 @@
"emittery": "^0.7.1",
"express": "^4.17.1",
"express-graphql": "^0.11.0",
"express-status-monitor": "^1.3.3",
"extract-zip": "^2.0.1",
"fast-levenshtein": "^2.0.6",
"fs-extra": "^9.0.1",
@ -152,7 +152,7 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.49",
"nc-help": "0.2.56",
"nc-lib-gui": "0.90.11",
"nc-plugin": "^0.1.1",
"ncp": "^2.0.0",

59
packages/nocodb/src/example/dockerRunPG.ts

@ -0,0 +1,59 @@
import cors from 'cors';
import express from 'express';
import Noco from '../lib/noco/Noco';
process.env.NC_VERSION = '0009044';
const server = express();
server.use(
cors({
exposedHeaders: 'xc-db-response'
})
);
server.set('view engine', 'ejs');
const date = new Date();
const metaDb = `meta_v2_${date.getFullYear()}_${(date.getMonth() + 1)
.toString()
.padStart(2, '0')}_${date
.getDate()
.toString()
.padStart(2, '0')}`;
// process.env[`NC_DB`] = `mysql2://localhost:3306?u=root&p=password&d=${metaDb}`;
// process.env[`NC_DB`] = `pg:/2/localhost:3306?u=root&p=password&d=mar_24`;
process.env[`NC_DB`] = `pg://localhost:5432?u=postgres&p=password&d=${metaDb}`;
// process.env[`NC_TRY`] = 'true';
// process.env[`NC_DASHBOARD_URL`] = '/test';
process.env[`DEBUG`] = 'xc*';
(async () => {
const httpServer = server.listen(process.env.PORT || 8080, () => {
console.log(`App started successfully.\nVisit -> ${Noco.dashboardUrl}`);
});
server.use(await Noco.init({}, httpServer, server));
})().catch(e => console.log(e));
/**
* @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/>.
*
*/

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

@ -93,7 +93,9 @@ class BaseModelSqlv2 {
await this.selectObject({ qb });
const data = await qb.where(this.model.primaryKey.column_name, id).first();
qb.where(this.model.primaryKey.column_name, id);
const data = (await this.extractRawQueryAndExec(qb))?.[0];
if (data) {
const proto = await this.getProto();
@ -239,7 +241,9 @@ class BaseModelSqlv2 {
if (!ignoreFilterSort) applyPaginate(qb, rest);
const proto = await this.getProto();
return (await qb).map(d => {
const data = await this.extractRawQueryAndExec(qb);
return data?.map(d => {
d.__proto__ = proto;
return d;
});
@ -351,7 +355,6 @@ class BaseModelSqlv2 {
async multipleHmList({ colId, ids }, args?: { limit?; offset? }) {
try {
// todo: get only required fields
let fields = '*';
// const { cn } = this.hasManyRelations.find(({ tn }) => tn === child) || {};
const relColumn = (await this.model.getColumns()).find(
@ -367,14 +370,6 @@ class BaseModelSqlv2 {
dbDriver: this.dbDriver
});
await parentTable.getColumns();
// if (fields !== '*' && fields.split(',').indexOf(cn) === -1) {
// fields += ',' + cn;
// }
fields = fields
.split(',')
.map(c => `${chilCol.column_name}.${c}`)
.join(',');
const qb = this.dbDriver(childTable.table_name);
await childModel.selectObject({ qb });
@ -404,8 +399,7 @@ class BaseModelSqlv2 {
.as('list')
);
const children = await childQb;
const children = await this.extractRawQueryAndExec(childQb);
const proto = await (
await Model.getBaseModelSQL({
id: childTable.id,
@ -465,19 +459,8 @@ class BaseModelSqlv2 {
async hmList({ colId, id }, args?: { limit?; offset? }) {
try {
// const {
// where,
// limit,
// offset,
// conditionGraph,
// sort
// // ...restArgs
// } = this.dbModels[child]._getChildListArgs(args);
// let { fields } = restArgs;
// todo: get only required fields
let fields = '*';
// const { cn } = this.hasManyRelations.find(({ tn }) => tn === child) || {};
const relColumn = (await this.model.getColumns()).find(
c => c.id === colId
);
@ -491,14 +474,6 @@ class BaseModelSqlv2 {
dbDriver: this.dbDriver
});
await parentTable.getColumns();
// if (fields !== '*' && fields.split(',').indexOf(cn) === -1) {
// fields += ',' + cn;
// }
fields = fields
.split(',')
.map(c => `${chilCol.column_name}.${c}`)
.join(',');
const qb = this.dbDriver(childTable.table_name);
@ -515,7 +490,7 @@ class BaseModelSqlv2 {
await childModel.selectObject({ qb });
const children = await qb;
const children = await this.extractRawQueryAndExec(qb);
const proto = await (
await Model.getBaseModelSQL({
@ -610,7 +585,7 @@ class BaseModelSqlv2 {
!this.isSqlite
);
const children = await finalQb;
const children = await this.extractRawQueryAndExec(finalQb);
const proto = await (
await Model.getBaseModelSQL({
id: rtnId,
@ -663,7 +638,7 @@ class BaseModelSqlv2 {
qb.limit(args?.limit || 20);
qb.offset(args?.offset || 0);
const children = await qb;
const children = await this.extractRawQueryAndExec(qb);
const proto = await (
await Model.getBaseModelSQL({ id: rtnId, dbDriver: this.dbDriver })
).getProto();
@ -1266,7 +1241,7 @@ class BaseModelSqlv2 {
await populatePk(this.model, data);
// todo: filter based on view
const insertObj = await this.model.mapAliasToColumn(data);
const insertObj = await this.model.mapAliasToColumn(data, sanitize);
await this.validate(insertObj);
@ -1394,6 +1369,9 @@ class BaseModelSqlv2 {
get isPg() {
return this.clientType === 'pg';
}
get isMySQL() {
return this.clientType === 'mysql2' || this.clientType === 'mysql';
}
get clientType() {
return this.dbDriver.clientType();
@ -2023,6 +2001,14 @@ class BaseModelSqlv2 {
break;
}
}
private async extractRawQueryAndExec(qb: QueryBuilder) {
return this.isPg
? qb
: await this.dbDriver.from(
this.dbDriver.raw(qb.toString()).wrap('(', ') __nc_alias')
);
}
}
function extractSortsObject(
@ -2156,8 +2142,8 @@ function getCompositePk(primaryKeys: Column[], row) {
return primaryKeys.map(c => row[c.title]).join('___');
}
function sanitize(v) {
return v?.replace(/\?/g, '\\?');
export function sanitize(v) {
return v?.replace(/([^\\]|^)([?])/g, '$1\\$2');
}
export { BaseModelSqlv2 };

12
packages/nocodb/src/lib/noco-jobs/JobsMgr.ts

@ -19,8 +19,8 @@ export default abstract class JobsMgr {
) => void,
options?: {
onSuccess?: (payload: any) => void;
onFailure?: (payload: any, msg: string) => void;
onProgress?: (payload: any, msgOrData: any) => void;
onFailure?: (payload: any, errorData: any) => void;
onProgress?: (payload: any, progressData: any) => void;
}
);
@ -29,13 +29,13 @@ export default abstract class JobsMgr {
this.successCbks[jobName].push(cbk);
}
addFailureCbk(jobName: string, cbk: (payload: any, msg: string) => void) {
addFailureCbk(jobName: string, cbk: (payload: any, errorData: any) => void) {
this.failureCbks[jobName] = this.failureCbks[jobName] || [];
this.failureCbks[jobName].push(cbk);
}
addProgressCbk(
jobName: string,
cbk: (payload: any, progress: string) => void
cbk: (payload: any, progressData: any) => void
) {
this.progressCbks[jobName] = this.progressCbks[jobName] || [];
this.progressCbks[jobName].push(cbk);
@ -56,10 +56,10 @@ export default abstract class JobsMgr {
protected async invokeProgressCbks(
jobName: string,
payload: any,
msg?: string
data?: any
) {
await Promise.all(
this.progressCbks?.[jobName]?.map(cb => cb(payload, msg))
this.progressCbks?.[jobName]?.map(cb => cb(payload, data))
);
}
}

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

@ -399,12 +399,13 @@ export default class Model implements TableType {
return true;
}
async mapAliasToColumn(data) {
async mapAliasToColumn(data, sanitize = v => v) {
const insertObj = {};
for (const col of await this.getColumns()) {
if (isVirtualCol(col)) continue;
const val = data?.[col.column_name] ?? data?.[col.title];
if (val !== undefined) insertObj[col.column_name] = val;
const val =
data?.[sanitize(col.column_name)] ?? data?.[sanitize(col.title)];
if (val !== undefined) insertObj[sanitize(col.column_name)] = val;
}
return insertObj;
}

5
packages/nocodb/src/lib/noco-models/View.ts

@ -108,7 +108,7 @@ export default class View implements ViewType {
));
if (!view) {
view = await ncMeta.metaGet2(null, null, MetaTable.VIEWS, viewId);
await NocoCache.set(`${CacheScope.VIEW}:${viewId}`, view);
await NocoCache.set(`${CacheScope.VIEW}:${view.id}`, view);
}
return view && new View(view);
@ -146,6 +146,7 @@ export default class View implements ViewType {
]
}
);
// todo: cache - titleOrId can be viewId so we need a different scope here
await NocoCache.set(
`${CacheScope.VIEW}:${fk_model_id}:${titleOrId}`,
view.id
@ -153,7 +154,7 @@ export default class View implements ViewType {
await NocoCache.set(`${CacheScope.VIEW}:${fk_model_id}:${view.id}`, view);
return view && new View(view);
}
return viewId && this.get(viewId);
return viewId && this.get(viewId?.id || viewId);
}
public static async list(modelId: string, ncMeta = Noco.ncMeta) {

73
packages/nocodb/src/lib/noco/Noco.ts

@ -40,6 +40,7 @@ import NcPluginMgrv2 from './meta/helpers/NcPluginMgrv2';
import User from '../noco-models/User';
import { Tele } from 'nc-help';
import * as http from 'http';
import weAreHiring from '../utils/weAreHiring';
const log = debug('nc:app');
require('dotenv').config();
@ -95,10 +96,6 @@ export default class Noco {
private config: NcConfig;
private requestContext: any;
private io: any;
// @ts-ignore
private socketClient: any;
constructor() {
process.env.PORT = process.env.PORT || '8080';
// todo: move
@ -178,8 +175,6 @@ export default class Noco {
this.initSentry();
NocoCache.init();
this.initWebSocket();
// this.apiInfInfoList = [];
//
// this.startTime = Date.now();
@ -269,6 +264,7 @@ export default class Noco {
next();
});
Tele.emit('evt_app_started', await User.count());
weAreHiring();
return this.router;
}
@ -488,71 +484,6 @@ export default class Noco {
}
}
private initWebSocket(): void {
// todo: Auth
this.router.get(`${this.config.dashboardPath}/demo`, (_req, res) => {
(Noco._ncMeta as any).updateKnex({
client: 'sqlite3',
connection: {
filename: 'xcDemo.db'
}
});
res.json({ msg: 'done' });
});
this.io = require('socket.io')();
this.io.listen(8083);
this.io.on('connection', client => {
this.socketClient = client;
client.on('disconnect', () => {
this.socketClient = null;
});
});
const statusMonitor = require('express-status-monitor')({
websocket: this.io,
port: 8083
});
this.router.use(statusMonitor);
this.router.get(
`${this.config.dashboardPath}/status`,
statusMonitor.pageRoute
);
/*
title: 'Express Status', // Default title
theme: 'default.css', // Default styles
path: '/status',
socketPath: '/socket.io', // In case you use a custom path
websocket: existingSocketIoInstance,
spans: [{
interval: 1, // Every second
retention: 60 // Keep 60 datapoints in memory
}, {
interval: 5, // Every 5 seconds
retention: 60
}, {
interval: 15, // Every 15 seconds
retention: 60
}],
chartVisibility: {
cpu: true,
mem: true,
load: true,
eventLoop: true,
heap: true,
responseTime: true,
rps: true,
statusCodes: true
},
healthChecks: [],
ignoreStartsWith: '/admin'*/
}
private async readOrGenJwtSecret(): Promise<any> {
if (this.config?.auth?.jwt && !this.config.auth.jwt.secret) {
let secret = (

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

@ -508,6 +508,11 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
await Column.insert({
...colBody,
...insertedColumnMeta,
dtxp: [UITypes.MultiSelect, UITypes.SingleSelect].includes(
colBody.uidt as any
)
? colBody.dtxp
: insertedColumnMeta.dtxp,
fk_model_id: table.id
});
}

36
packages/nocodb/src/lib/noco/meta/api/swagger/redocHtml.ts

@ -6,7 +6,6 @@ export default `<!DOCTYPE html>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
Redoc doesn't change outer page styles
-->
@ -20,5 +19,40 @@ export default `<!DOCTYPE html>
<body>
<redoc spec-url='./swagger.json'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"> </script>
<script>
console.log('%c🚀 We are Hiring!!! 🚀%c\\n%cJoin the forces http://careers.nocodb.com', 'color:#1348ba;font-size:3rem;padding:20px;', 'display:none', 'font-size:1.5rem;padding:20px')
const linkEl = document.createElement('a')
linkEl.setAttribute('href', "http://careers.nocodb.com")
linkEl.setAttribute('target', '_blank')
linkEl.setAttribute('class', 'we-are-hiring')
linkEl.innerHTML = '🚀 We are Hiring!!! 🚀'
const styleEl = document.createElement('style');
styleEl.innerHTML = \`
.we-are-hiring {
position: fixed;
bottom: 50px;
right: -250px;
opacity: 0;
background: orange;
border-radius: 4px;
padding: 19px;
z-index: 200;
text-decoration: none;
text-transform: uppercase;
color: black;
transition: 1s opacity, 1s right;
display: block;
font-weight: bold;
}
.we-are-hiring.active {
opacity: 1;
right:25px;
}
\`
document.body.appendChild(linkEl, document.body.firstChild)
document.body.appendChild(styleEl, document.body.firstChild)
setTimeout(() => linkEl.classList.add('active'), 2000)
</script>
</body>
</html>`;

33
packages/nocodb/src/lib/noco/meta/api/swagger/swaggerHtml.ts

@ -20,6 +20,39 @@ export default `<!DOCTYPE html>
SwaggerUIBundle.SwaggerUIStandalonePreset
],
})
console.log('%c🚀 We are Hiring!!! 🚀%c\\n%cJoin the forces http://careers.nocodb.com', 'color:#1348ba;font-size:3rem;padding:20px;', 'display:none', 'font-size:1.5rem;padding:20px');
const linkEl = document.createElement('a')
linkEl.setAttribute('href', "http://careers.nocodb.com")
linkEl.setAttribute('target', '_blank')
linkEl.setAttribute('class', 'we-are-hiring')
linkEl.innerHTML = '🚀 We are Hiring!!! 🚀'
const styleEl = document.createElement('style');
styleEl.innerHTML = \`
.we-are-hiring {
position: fixed;
bottom: 50px;
right: -250px;
opacity: 0;
background: orange;
border-radius: 4px;
padding: 19px;
z-index: 200;
text-decoration: none;
text-transform: uppercase;
color: black;
transition: 1s opacity, 1s right;
display: block;
font-weight: bold;
}
.we-are-hiring.active {
opacity: 1;
right:25px;
}
\`
document.body.appendChild(linkEl, document.body.firstChild)
document.body.appendChild(styleEl, document.body.firstChild)
setTimeout(() => linkEl.classList.add('active'), 2000)
</script>
</body>
</html>

473
packages/nocodb/src/lib/noco/meta/api/sync/helpers/job.ts

File diff suppressed because it is too large Load Diff

5
packages/nocodb/src/lib/noco/meta/api/sync/importApis.ts

@ -20,13 +20,14 @@ export default (router: Router, clients: { [id: string]: Socket }) => {
NocoJobs.jobsMgr.addJobWorker(AIRTABLE_IMPORT_JOB, job);
NocoJobs.jobsMgr.addProgressCbk(AIRTABLE_IMPORT_JOB, (payload, progress) => {
clients?.[payload?.id]?.emit('progress', {
msg: progress,
msg: progress?.msg,
level: progress?.level,
status: SyncStatus.PROGRESS
});
});
NocoJobs.jobsMgr.addSuccessCbk(AIRTABLE_IMPORT_JOB, payload => {
clients?.[payload?.id]?.emit('progress', {
msg: 'completed',
msg: 'Complete!',
status: SyncStatus.COMPLETED
});
});

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

@ -27,6 +27,7 @@ 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
@ -102,6 +103,13 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
}
}
// validate table name
if (/^\s+|\s+$/.test(req.body.table_name)) {
NcError.badRequest(
'Leading or trailing whitespace not allowed in table names'
);
}
if (
!(await Model.checkTitleAvailable({
table_name: req.body.table_name,
@ -176,6 +184,11 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
...colMetaFromReq,
uidt: colMetaFromReq?.uidt || c.uidt || getColumnUiType(base, c),
...c,
dtxp: [UITypes.MultiSelect, UITypes.SingleSelect].includes(
colMetaFromReq.uidt as any
)
? colMetaFromReq.dtxp
: c.dtxp,
title: colMetaFromReq?.title || getColumnNameAlias(c.cn, base),
column_name: c.cn,
order: i + 1

0
packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts

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

@ -132,7 +132,6 @@ export function axiosRequestMake(_apiMeta, user, data) {
});
} catch (e) {
apiMeta.body = parseBody(apiMeta.body, user, data, apiMeta);
console.log(e);
}
}
if (apiMeta.auth) {
@ -144,7 +143,6 @@ export function axiosRequestMake(_apiMeta, user, data) {
});
} catch (e) {
apiMeta.auth = parseBody(apiMeta.auth, user, data, apiMeta);
console.log(e);
}
}
apiMeta.response = {};

5
packages/nocodb/src/lib/utils/projectAcl.ts

@ -54,6 +54,7 @@ export default {
formViewGet: true,
projectInfoGet: true,
gridColumnUpdate: true,
galleryViewGet: true,
// old
xcTableAndViewList: true,
@ -161,6 +162,8 @@ export default {
dataGroupBy: true,
commentsCount: true,
galleryViewGet: true,
xcTableAndViewList: true,
xcVirtualTableList: true,
projectList: true,
@ -204,6 +207,8 @@ export default {
sortList: true,
projectInfoGet: true,
galleryViewGet: true,
mmList: true,
hmList: true,
commentList: true,

13
packages/nocodb/src/lib/utils/weAreHiring.ts

@ -0,0 +1,13 @@
import boxen from 'boxen';
export default function() {
console.log(`
${boxen(`Join the forces http://careers.nocodb.com`, {
title: '🚀 We are Hiring!!! 🚀',
padding: 1,
margin: 1,
titleAlignment: 'center',
borderColor: 'green'
})}
`);
}

2
scripts/cypress/integration/common/5a_user_role.js

@ -189,7 +189,7 @@ export const genTest = (apiType, dbType) => {
// right navigation menu bar
// Editor/Viewer/Commenter : can only view 'existing' views
// Rest: can create/edit
_viewMenu(roleType, false);
_viewMenu(roleType, false, 2);
});
it(`[${roles[roleType].name}] Top Right Menu bar`, () => {

2
scripts/cypress/integration/common/5b_preview_role.js

@ -115,7 +115,7 @@ export const genTest = (apiType, dbType, roleType) => {
// right navigation menu bar
// Editor/Viewer/Commenter : can only view 'existing' views
// Rest: can create/edit
_viewMenu(roleType, true);
_viewMenu(roleType, true, 2);
});
it(`Role preview: ${roleType}: Top Right Menu bar`, () => {

2
scripts/cypress/integration/common/6g_base_share.js

@ -48,7 +48,7 @@ export const genTest = (apiType, dbType) => {
});
it(`${roleType}: Validate access permissions: view's menu`, () => {
_viewMenu(roleType, false);
_viewMenu(roleType, false, 1);
});
};

5
scripts/cypress/integration/spec/roleValidation.spec.js

@ -212,9 +212,10 @@ export function _editComment(roleType, previewMode) {
// right navigation menu bar
// Editor/Viewer/Commenter : can only view 'existing' views
// Rest: can create/edit
export function _viewMenu(roleType, previewMode) {
export function _viewMenu(roleType, previewMode, navDrawListCnt) {
let columnName = "City";
let navDrawListCnt = 2;
// let navDrawListCnt = 2;
// Download CSV
let actionsMenuItemsCnt = 1;

Loading…
Cancel
Save