Browse Source

Merge branch 'develop' into fix/formula

pull/1979/head
Wing-Kam Wong 3 years ago
parent
commit
c62acc06bc
  1. 11
      packages/nc-gui/components/project/spreadsheet/components/Cell.vue
  2. 38
      packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue
  3. 19
      packages/nc-gui/components/project/spreadsheet/components/EditableCell.vue
  4. 22
      packages/nc-gui/components/project/spreadsheet/components/cell/EmailCell.vue
  5. 14
      packages/nc-gui/components/project/spreadsheet/components/cell/UrlCell.vue
  6. 98
      packages/nc-gui/components/project/spreadsheet/components/editColumn/RatingOptions.vue
  7. 95
      packages/nc-gui/components/project/spreadsheet/components/editColumn/checkboxOptions.vue
  8. 36
      packages/nc-gui/components/project/spreadsheet/components/editableCell/BooleanCell.vue
  9. 9
      packages/nc-gui/components/project/spreadsheet/components/editableCell/EditableUrlCell.vue
  10. 71
      packages/nc-gui/components/project/spreadsheet/components/editableCell/RatingCell.vue
  11. 6
      packages/nc-gui/components/project/spreadsheet/mixins/cell.js
  12. 70
      packages/nc-gui/components/project/spreadsheet/views/GridView.vue
  13. 64
      packages/nc-gui/lang/zh_CN.json
  14. 24
      packages/noco-docs/content/en/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free.md
  15. 84
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  16. 74
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  17. 36
      packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts
  18. 97
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  19. 94
      packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts
  20. 3
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSqlv2.ts
  21. 40
      packages/nocodb/src/lib/noco-models/Column.ts
  22. 6
      packages/nocodb/src/lib/noco/common/XcMigrationSourcev2.ts
  23. 13
      packages/nocodb/src/lib/noco/meta/api/sync/helpers/job.ts
  24. 38
      packages/nocodb/src/lib/noco/migrationsv2/nc_015_add_meta_col_in_column_table.ts
  25. 4
      packages/nocodb/src/lib/noco/upgrader/jobs/ncProjectUpgraderV2_0090000.ts
  26. 10
      scripts/markdown/readme/languages/chinese.md

11
packages/nc-gui/components/project/spreadsheet/components/Cell.vue

@ -11,11 +11,14 @@
<!-- <enum-list-editable-cell @click.stop="$emit('enableedit')" v-else-if="isEnum && selected" :value="value" :column="column"></enum-list-editable-cell>-->
<enum-cell v-else-if="isEnum" :value="value" :column="column" @click.stop="$emit('enableedit')" />
<url-cell v-else-if="isURL" :value="value" />
<email-cell v-else-if="isEmail" :value="value" />
<json-cell v-else-if="isJSON" :value="value" />
<date-cell v-else-if="isDate" :value="value" />
<date-time-cell v-else-if="isDateTime" :value="value" />
<time-cell v-else-if="isTime" :value="value" />
<boolean-cell v-else-if="isBoolean" :value="value" />
<boolean-cell v-else-if="isBoolean" :value="value" read-only />
<rating-cell v-else-if="isRating" :value="value" read-only />
<span v-else :class="{'long-text-cell' : isTextArea}" :title="title">{{ value }}</span>
</template>
@ -30,12 +33,14 @@ import SetListCell from '~/components/project/spreadsheet/components/cell/SetLis
import EnumCell from '~/components/project/spreadsheet/components/cell/EnumCell'
import EditableAttachmentCell from '~/components/project/spreadsheet/components/editableCell/EditableAttachmentCell'
import BooleanCell from '~/components/project/spreadsheet/components/cell/BooleanCell'
import EmailCell from '~/components/project/spreadsheet/components/cell/EmailCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
export default {
name: 'TableCell',
components: { TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell, BooleanCell },
components: { RatingCell, EmailCell, TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell, BooleanCell },
mixins: [cell],
props: ['value', 'dbAlias', 'isLocked', 'selected'],
props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'],
computed: {
title() {
if (typeof this.value === 'string') { return this.value }

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

@ -137,6 +137,21 @@
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<v-col v-else-if="isRating" cols="12">
<rating-options
v-model="newColumn.meta"
:column="newColumn"
:meta="meta"
/>
</v-col>
<v-col v-else-if="isCheckbox" cols="12">
<checkbox-options
v-model="newColumn.meta"
:column="newColumn"
:meta="meta"
/>
</v-col>
<v-col
v-if="accordion"
cols="12"
@ -160,6 +175,10 @@
<v-col v-show="advanceOptions || !accordion" cols="12">
<v-row>
<v-col v-if="newColumn.meta && columnToValidate.includes(newColumn.uidt)" cols="12" class="pt-0 pb-0">
<v-checkbox v-model="newColumn.meta.validate" dense hide-details :label="`Accept only valid ${newColumn.uidt}`" class="mt-0" />
</v-col>
<template v-if="newColumn.uidt !== 'Formula'">
<v-col v-if="isLookup" cols="12">
<lookup-options
@ -526,10 +545,15 @@ import RelationOptions from '~/components/project/spreadsheet/components/editCol
import DlgLabelSubmitCancel from '~/components/utils/DlgLabelSubmitCancel'
import LinkedToAnotherOptions from '~/components/project/spreadsheet/components/editColumn/LinkedToAnotherOptions'
import { validateColumnName } from '~/helpers'
import RatingOptions from '~/components/project/spreadsheet/components/editColumn/RatingOptions'
import CheckboxOptions from '~/components/project/spreadsheet/components/editColumn/checkboxOptions'
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
export default {
name: 'EditColumn',
components: {
CheckboxOptions,
RatingOptions,
RollupOptions,
FormulaOptions,
LookupOptions,
@ -548,6 +572,7 @@ export default {
value: Boolean
},
data: () => ({
columnToValidate,
valid: false,
relationDeleteDlg: false,
newColumn: {},
@ -594,6 +619,12 @@ export default {
isLookup() {
return this.newColumn && this.newColumn.uidt === 'Lookup'
},
isRating() {
return this.newColumn && this.newColumn.uidt === UITypes.Rating
},
isCheckbox() {
return this.newColumn && this.newColumn.uidt === UITypes.Checkbox
},
isRollup() {
return this.newColumn && this.newColumn.uidt === 'Rollup'
},
@ -734,6 +765,7 @@ export default {
const colProp = this.sqlUi.getDataTypeForUiType(this.newColumn)
this.newColumn = {
...this.newColumn,
meta: null,
rqd: false,
pk: false,
ai: false,
@ -759,6 +791,12 @@ export default {
this.newColumn.dtxp = this.column.dtxp
}
if (columnToValidate.includes(this.newColumn.uidt)) {
this.newColumn.meta = {
validate: this.newColumn.meta && this.newColumn.meta.validate
}
}
this.newColumn.altered = this.newColumn.altered || 2
},
focusInput() {

19
packages/nc-gui/components/project/spreadsheet/components/EditableCell.vue

@ -23,9 +23,22 @@
v-on="$listeners"
/>
<rating-cell
v-if="isRating"
v-model="localState"
:active="active"
:is-form="isForm"
:column="column"
:is-public-grid="isPublic && !isForm"
:is-public-form="isPublic && isForm"
:is-locked="isLocked"
v-on="$listeners"
/>
<boolean-cell
v-else-if="isBoolean"
v-model="localState"
:column="column"
:is-form="isForm"
v-on="parentListeners"
/>
@ -132,10 +145,12 @@ import EditableAttachmentCell from '~/components/project/spreadsheet/components/
import EnumCell from '~/components/project/spreadsheet/components/cell/EnumCell'
import SetListEditableCell from '~/components/project/spreadsheet/components/editableCell/SetListEditableCell'
import SetListCell from '~/components/project/spreadsheet/components/cell/SetListCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
export default {
name: 'EditableCell',
components: {
RatingCell,
JsonEditableCell,
EditableUrlCell,
SetListCell,
@ -182,7 +197,7 @@ export default {
if (val !== this.value) {
this.changed = true
this.$emit('input', val)
if (this.isAttachment || this.isEnum || this.isBoolean || this.isSet || this.isTime || this.isDateTime || this.isDate) {
if (this.isAttachment || this.isEnum || this.isBoolean || this.isRating || this.isSet || this.isTime || this.isDateTime || this.isDate) {
this.syncData()
} else if (!this.isCurrency) {
this.syncDataDebounce(this)
@ -213,7 +228,7 @@ export default {
// this.$refs.input.focus();
},
beforeDestroy() {
if (this.changed && !(this.isAttachment || this.isEnum || this.isBoolean || this.isSet || this.isTime || this.isDateTime)) {
if (this.changed && !(this.isAttachment || this.isEnum || this.isBoolean || this.isRating || this.isSet || this.isTime || this.isDateTime)) {
this.changed = false
this.$emit('change')
}

22
packages/nc-gui/components/project/spreadsheet/components/cell/EmailCell.vue

@ -0,0 +1,22 @@
<template>
<a v-if="isEmail" :href="`mailto:${value}`" target="_blank">{{ value }}</a>
<span v-else>{{ value }}</span>
</template>
<script>
import { isEmail } from '~/helpers'
export default {
name: 'EmailCell',
props: ['value'],
computed: {
isEmail() {
return isEmail(this.value || '')
}
}
}
</script>
<style scoped>
</style>

14
packages/nc-gui/components/project/spreadsheet/components/cell/UrlCell.vue

@ -1,12 +1,20 @@
<template>
<a v-if="value" :href="value" target="_blank">{{ value }}</a>
<span v-else />
<a v-if="isValid" :href="value" target="_blank">{{ value }}</a>
<span v-else>{{ value }}</span>
</template>
<script>
import { isValidURL } from '~/helpers'
export default {
name: 'UrlCell',
props: ['value']
props: ['value'],
computed: {
isValid() {
return this.value && isValidURL(this.value)
}
}
}
</script>

98
packages/nc-gui/components/project/spreadsheet/components/editColumn/RatingOptions.vue

@ -0,0 +1,98 @@
<template>
<div>
<div class="nc-rating-wrapper ui-type">
<v-select
v-model="colMeta.icon"
label="Icon"
:items="icons"
dense
outlined
class="caption"
:item-value="v=>v"
:value-comparator="(a,b) => a && b && a.full === b.full && a.empty === b.empty"
>
<template #item="{ item }">
<v-icon small :color="colMeta.color">
{{ item.full }}
</v-icon>
<v-icon class="ml-2" small :color="colMeta.color">
{{ item.empty }}
</v-icon>
</template>
<template #selection="{ item }">
<v-icon small :color="colMeta.color">
{{ item.full }}
</v-icon>
<v-icon class="ml-2" small :color="colMeta.color">
{{ item.empty }}
</v-icon>
</template>
</v-select>
<v-select
v-model="colMeta.max"
label="Max"
:items="[1,2,3,4,5,6,7,8,9,10]"
dense
outlined
class="caption"
/>
</div>
<v-color-picker
v-model="colMeta.color"
class="mx-auto"
hide-inputs
/>
</div>
</template>
<script>
export default {
name: 'RatingOptions',
props: ['column', 'meta', 'value'],
data: () => ({
colMeta: {
icon: {
full: 'mdi-star',
empty: 'mdi-star-outline'
},
color: '#fcb401',
max: 5
},
icons: [{
full: 'mdi-star',
empty: 'mdi-star-outline'
}, {
full: 'mdi-heart',
empty: 'mdi-heart-outline'
}, {
full: 'mdi-moon-full',
empty: 'mdi-moon-new'
}, {
full: 'mdi-thumb-up',
empty: 'mdi-thumb-up-outline'
}, {
full: 'mdi-flag',
empty: 'mdi-flag-outline'
}]
}),
watch: {
value() {
this.colMeta = this.value || {}
},
colMeta(v) {
this.$emit('input', v)
}
},
created() {
this.colMeta = this.value || { ...this.colMeta }
}
}
</script>
<style scoped lang="scss">
.nc-rating-wrapper {
display: flex;
gap: 16px
}
</style>

95
packages/nc-gui/components/project/spreadsheet/components/editColumn/checkboxOptions.vue

@ -0,0 +1,95 @@
<template>
<div>
<div class="nc-rating-wrapper ui-type">
<v-select
v-model="colMeta.icon"
label="Icon"
:items="icons"
dense
outlined
class="caption"
:item-value="v=>v"
:value-comparator="(a,b) => a && b && a.checked === b.checked && a.unchecked === b.unchecked"
>
<template #item="{ item }">
<v-icon small :color="colMeta.color">
{{ item.checked }}
</v-icon>
<v-icon class="ml-2" small :color="colMeta.color">
{{ item.unchecked }}
</v-icon>
</template>
<template #selection="{ item }">
<v-icon small :color="colMeta.color">
{{ item.checked }}
</v-icon>
<v-icon class="ml-2" small :color="colMeta.color">
{{ item.unchecked }}
</v-icon>
</template>
</v-select>
</div>
<v-color-picker
v-model="colMeta.color"
class="mx-auto"
hide-inputs
/>
</div>
</template>
<script>
export default {
name: 'CheckboxOptions',
props: ['column', 'meta', 'value'],
data: () => ({
colMeta: {
icon: {
checked: 'mdi-check-bold',
unchecked: 'mdi-crop-square'
},
color: '#777'
},
icons: [{
checked: 'mdi-check-bold',
unchecked: 'mdi-crop-square'
}, {
checked: 'mdi-check-circle-outline',
unchecked: 'mdi-checkbox-blank-circle-outline'
}, {
checked: 'mdi-star',
unchecked: 'mdi-star-outline'
}, {
checked: 'mdi-heart',
unchecked: 'mdi-heart-outline'
}, {
checked: 'mdi-moon-full',
unchecked: 'mdi-moon-new'
}, {
checked: 'mdi-thumb-up',
unchecked: 'mdi-thumb-up-outline'
}, {
checked: 'mdi-flag',
unchecked: 'mdi-flag-outline'
}]
}),
watch: {
value() {
this.colMeta = this.value || {}
},
colMeta(v) {
this.$emit('input', v)
}
},
created() {
this.colMeta = this.value || { ...this.colMeta }
}
}
</script>
<style scoped lang="scss">
.nc-rating-wrapper {
display: flex;
gap: 16px
}
</style>

36
packages/nc-gui/components/project/spreadsheet/components/editableCell/BooleanCell.vue

@ -1,6 +1,8 @@
<template>
<div class="d-flex align-center " :class="{'justify-center':!isForm}">
<input v-model="localState" type="checkbox" v-on="parentListeners">
<div class="d-flex align-center " :class="{'justify-center':!isForm,'nc-cell-hover-show': !localState}">
<v-icon small :color="checkboxMeta.color" @click="toggle">
{{ localState ? checkedIcon :uncheckedIcon }}
</v-icon>
</div>
</template>
@ -8,10 +10,19 @@
export default {
name: 'BooleanCell',
props: {
column: Object,
value: [String, Number, Boolean],
isForm: Boolean
isForm: Boolean,
readOnly: Boolean
},
computed: {
checkedIcon() {
return (this.checkboxMeta && this.checkboxMeta.icon && this.checkboxMeta.icon.checked) || 'mdi-check-bold'
},
uncheckedIcon() {
return (this.checkboxMeta && this.checkboxMeta.icon && this.checkboxMeta.icon.unchecked) || 'mdi-crop-square'
},
localState: {
get() {
return this.value
@ -20,14 +31,27 @@ export default {
this.$emit('input', val)
}
},
parentListeners() {
const $listeners = {}
return $listeners
},
checkboxMeta() {
return {
icon: {
checked: 'mdi-check-circle-outline',
unchecked: 'mdi-checkbox-blank-circle-outline'
},
color: 'primary',
...(this.column && this.column.meta
? this.column.meta
: {})
}
}
},
mounted() {
this.$el.focus()
methods: {
toggle() {
this.localState = !this.localState
}
}
}
</script>

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

@ -8,7 +8,8 @@ import { isValidURL } from '@/helpers'
export default {
name: 'EditableUrlCell',
props: {
value: String
value: String,
column: Object
},
computed: {
localState: {
@ -16,7 +17,11 @@ export default {
return this.value
},
set(val) {
if (isValidURL(val)) { this.$emit('input', val) }
if (!(
this.column &&
this.column.meta &&
this.column.meta.validate
) || isValidURL(val)) { this.$emit('input', val) }
}
},
parentListeners() {

71
packages/nc-gui/components/project/spreadsheet/components/editableCell/RatingCell.vue

@ -0,0 +1,71 @@
<template>
<div class="d-100 h-100" :class="{'nc-cell-hover-show': localState == 0 || !localState}">
<v-rating
v-model="localState"
:length="ratingMeta.max"
dense
x-small
:disabled="readOnly"
clearable
>
<template #item="{isFilled, click}">
<v-icon v-if="isFilled" :size="15" :color="ratingMeta.color" @click="click">
{{ fullIcon }}
</v-icon>
<v-icon
v-else
:color="ratingMeta.color"
:size="15"
class="nc-cell-hover-show"
@click="click"
>
{{ emptyIcon }}
</v-icon>
</template>
</v-rating>
</div>
</template>
<script>
export default {
name: 'RatingCell',
props: {
column: Object,
value: [String, Number],
readOnly: Boolean
},
computed: {
fullIcon() {
return (this.ratingMeta && this.ratingMeta.icon && this.ratingMeta.icon.full) || 'mdi-star'
},
emptyIcon() {
return (this.ratingMeta && this.ratingMeta.icon && this.ratingMeta.icon.empty) || 'mdi-star-outline'
},
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
ratingMeta() {
return {
icon: {
full: 'mdi-star',
empty: 'mdi-star-outline'
},
color: '#fcb401',
max: 5,
...(this.column && this.column.meta
? this.column.meta
: {})
}
}
}
}
</script>
<style scoped>
</style>

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

@ -56,9 +56,15 @@ export default {
isURL() {
return this.uiDatatype === 'URL'
},
isEmail() {
return this.uiDatatype === UITypes.Email
},
isAttachment() {
return this.uiDatatype === 'Attachment'
},
isRating() {
return this.uiDatatype === UITypes.Rating
},
isCurrency() {
return this.column.uidt == 'Currency'
}

70
packages/nc-gui/components/project/spreadsheet/views/GridView.vue

@ -139,7 +139,7 @@
v-show="!rowMeta || !rowMeta.selected"
class="ml-2 grey--text"
:class="{ 'row-no': !isPublicView }"
>{{ row + 1 }}</span>
>{{ row + 1 }}</span>
<template v-if="!isPublicView">
<v-checkbox
@ -632,31 +632,30 @@ export default {
break
// delete
case 46:
{
if (this.editEnabled.col != null && this.editEnabled.row != null) {
return
}
const rowObj = this.data[this.selected.row].row
const columnObj = this.availableColumns[this.selected.col]
case 46: {
if (this.editEnabled.col != null && this.editEnabled.row != null) {
return
}
if (
// this.isRequired(columnObj, rowObj, true) ||
columnObj.virtual
) {
return
}
const rowObj = this.data[this.selected.row].row
const columnObj = this.availableColumns[this.selected.col]
this.$set(rowObj, columnObj.title, null)
// update/save cell value
this.onCellValueChange(
this.selected.col,
this.selected.row,
columnObj,
true
)
if (
// this.isRequired(columnObj, rowObj, true) ||
columnObj.virtual
) {
return
}
this.$set(rowObj, columnObj.title, null)
// update/save cell value
this.onCellValueChange(
this.selected.col,
this.selected.row,
columnObj,
true
)
}
break
// left
case 37:
@ -735,11 +734,11 @@ export default {
onClickOutside() {
if (
(this.meta.columns &&
this.meta.columns[this.selected.col] &&
this.meta.columns[this.selected.col].virtual) ||
this.meta.columns[this.selected.col] &&
this.meta.columns[this.selected.col].virtual) ||
(this.availableColumns &&
this.availableColumns[this.editEnabled.col] &&
this.availableColumns[this.editEnabled.col].uidt === 'JSON')
this.availableColumns[this.editEnabled.col] &&
this.availableColumns[this.editEnabled.col].uidt === 'JSON')
) {
return
}
@ -820,6 +819,7 @@ export default {
(column && column.uidt === UITypes.DateTime) ||
(column && column.uidt === UITypes.Date) ||
(column && column.uidt === UITypes.Time) ||
(column && column.uidt === UITypes.Rating) ||
(this.sqlUi &&
column.dt &&
this.sqlUi.getAbstractType(column) === 'boolean')
@ -930,7 +930,7 @@ export default {
}
.search-field.v-text-field.v-text-field--solo.v-input--dense
> .v-input__control {
> .v-input__control {
min-height: auto;
}
@ -1163,4 +1163,18 @@ td:first-child {
.has-many-icon {
transform: rotate(90deg);
}
::v-deep {
tr {
.nc-cell-hover-show {
opacity: 0;
transition: .3s opacity;
}
&:hover .nc-cell-hover-show {
opacity: .7;
}
}
}
</style>

64
packages/nc-gui/lang/zh_CN.json

@ -38,7 +38,7 @@
"signIn": "登录",
"signOut": "登出",
"required": "必填项",
"preferred": "Preferred",
"preferred": "首选",
"mandatory": "强制的",
"loading": "加载中...",
"title": "标题",
@ -59,7 +59,7 @@
"objects": {
"project": "项目",
"projects": "项目",
"table": "格",
"table": "格",
"tables": "表格",
"field": "字段",
"fields": "字段",
@ -74,7 +74,7 @@
"view": "视图",
"views": "视图",
"viewType": {
"grid": "格",
"grid": "格",
"gallery": "画廊",
"form": "表单",
"kanban": "看板",
@ -154,7 +154,7 @@
"teamAndAuth": "团队和认证",
"rolesUserMgmt": "角色和用户管理",
"userMgmt": "用户账号管理",
"apiTokenMgmt": "API 令牌管理",
"apiTokenMgmt": "API Tokens 管理",
"rolesMgmt": "角色管理",
"projMeta": "项目基础信息",
"metaMgmt": "项目基础信息管理",
@ -170,12 +170,12 @@
"headCreateProject": "新建项目 | NocoDB",
"headLogin": "登录 | NocoDB",
"resetPassword": "重置密码",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs",
"teamAndSettings": "团队和设置",
"apiDocs": "API 文档",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "通知Via",
"notifyVia": "通知通过",
"projName": "项目名",
"tableName": "表名称",
"viewName": "查看名称",
@ -192,7 +192,7 @@
"port": "端口号",
"username": "用户名",
"password": "密码",
"schemaName": "Schema name",
"schemaName": "架构名称",
"action": "行动",
"actions": "行动",
"operation": "操作",
@ -201,7 +201,7 @@
"description": "描述",
"authentication": "验证",
"token": "令牌",
"where": "where",
"where": "在哪里",
"cache": "缓存",
"chat": "聊天",
"email": "电子邮件",
@ -225,15 +225,15 @@
},
"community": {
"starUs1": "点赞",
"starUs2": "我们 Github",
"starUs2": "我们 Github",
"bookDemo": "查看免费演示",
"getAnswered": "通过这里让你的问题得到解答",
"joinDiscord": "加入 Discord",
"joinCommunity": "Join NocoDB Community",
"joinReddit": "加入/r/NocoDB",
"joinCommunity": "加入 NocoDB 社区",
"joinReddit": "加入 /r/NocoDB",
"followNocodb": "关注 NocoDB"
},
"docReference": "文件参考文献",
"docReference": "参考文档",
"selectUserRole": "选择用户角色",
"childTable": "子表",
"childColumn": "子列",
@ -256,7 +256,7 @@
"excel": "从Excel创建项目",
"template": "从模板创建项目"
},
"OkSaveProject": "好的和保存项目",
"OkSaveProject": "确认并保存项目",
"upgrade": {
"available": "升级可用",
"releaseNote": "发行说明",
@ -265,7 +265,7 @@
"translate": "帮助翻译",
"account": {
"authToken": "复制auth令牌",
"swagger": "Swagger Apis Doc.",
"swagger": "Swagger Apis 文档",
"projInfo": "复制项目信息",
"themes": "主题"
},
@ -287,7 +287,7 @@
"editUser": "编辑用户",
"deleteUser": "从项目中删除用户",
"resendInvite": "重新发送邀请电子邮件",
"copyInviteURL": "复制邀请URL.",
"copyInviteURL": "复制邀请链接",
"newRole": "新角色",
"reloadRoles": "重新加载角色",
"nextPage": "下一页",
@ -306,10 +306,10 @@
"insertRow": "插入新行",
"deleteRow": "删除行",
"deleteSelectedRow": "删除所选行",
"importExcel": "导入Excel.",
"importExcel": "导入Excel",
"importCSV": "Import CSV",
"downloadCSV": "下载为CSV.",
"uploadCSV": "上传CSV.",
"downloadCSV": "下载为CSV",
"uploadCSV": "上传CSV",
"import": "导入",
"importMetadata": "导入元数据",
"exportMetadata": "导出元数据",
@ -326,16 +326,16 @@
"createGrid": "创建网格视图",
"createGallery": "创建画廊视图",
"createCalendar": "创建日历视图",
"createKanban": "创建寻呼视图",
"createKanban": "创建看板视图",
"createForm": "创建表单视图",
"showSystemFields": "显示系统字段",
"copyUrl": "复制URL.",
"copyUrl": "复制链接",
"openTab": "打开新标签",
"iFrame": "复制嵌入HTML代码",
"iFrame": "复制嵌入HTML代码",
"addWebhook": "添加新的webhook.",
"newToken": "添加新令牌",
"newToken": "添加新 Token",
"exportZip": "导出为zip格式",
"importZip": "zip格式导入",
"importZip": "导入zip格式",
"metaSync": "立即同步",
"settings": "设置",
"previewAs": "预览",
@ -348,7 +348,7 @@
},
"tooltip": {
"saveChanges": "保存更改",
"xcDB": "新建一个项目",
"xcDB": "新建项目",
"extDB": "支持 MySQL、PostgreSQL、SQL Server 和 SQLite",
"apiRest": "通过 REST APIs 访问",
"apiGQL": "通过 GraphQL APIs 访问",
@ -365,7 +365,7 @@
"reloadList": "重新加载列表",
"metaSync": "同步元数据",
"sqlMigration": "重新加载迁移",
"updateRestart": "更新 & 重启",
"updateRestart": "更新重启",
"cancelReturn": "取消并返回",
"exportMetadata": "从元数据表格导出所有元数据到元数据目录",
"importMetadata": "从元数据目录导入所有的元数据到元数据表格",
@ -401,7 +401,7 @@
"excelURL": "输入Excel文件URL",
"csvURL": "Enter CSV file URL",
"footMsg": "要解析为推断数据类型的行数",
"excelImport": "板材可用于进口",
"excelImport": "可用于导入的表格",
"exportMetadata": "您想从元表导出元数据吗?",
"importMetadata": "您想从元表导入元数据吗?",
"clearMetadata": "你想清除元表中的元数据吗?",
@ -410,7 +410,7 @@
"startProject": "你想启动这个项目吗?",
"restartProject": "你想重新启动项目吗?",
"deleteProject": "你想删除这个项目吗?",
"shareBasePrivate": "产生公开可享的Readonly Base",
"shareBasePrivate": "产生公开共享的只读基础",
"shareBasePublic": "互联网上的任何人都可以查看",
"userInviteNoSMTP": "看起来你还没有配置邮件!请复制上面的邀请链接并将其发送给",
"dragDropHide": "在此处拖放字段以隐藏",
@ -485,7 +485,7 @@
"tableNameInDb": "数据库中保存的表名"
},
"error": {
"searchProject": "你的搜索: {search} 没有发现匹配的结果",
"searchProject": "搜索: {search} 没有发现匹配的结果",
"invalidChar": "文件夹路径中的字符无效。",
"invalidDbCredentials": "无效的数据库凭据。",
"unableToConnectToDb": "无法连接到数据库,请检查您的数据库是否已启动。",
@ -503,14 +503,14 @@
"exportMetadata": "项目元数据成功导出",
"importMetadata": "项目元数据成功导入",
"clearMetadata": "项目元数据已成功清除",
"stopProject": "项目成功停止",
"stopProject": "项目成功停止",
"startProject": "项目成功开始",
"restartProject": "项目成功重新启动",
"deleteProject": "项目已成功删除",
"authToken": "验证令牌复制到剪贴板",
"projInfo": "将项目信息复制到剪贴板",
"inviteUrlCopy": "复制邀请URL到剪贴板",
"createView": "查看成功创建",
"inviteUrlCopy": "复制邀请链接到剪贴板",
"createView": "视图创建成功",
"formEmailSMTP": "请激活App Store中的SMTP插件以启用电子邮件通知",
"collabView": "成功转换为协作视图",
"lockedView": "成功转换为锁定视图",

24
packages/noco-docs/content/en/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free.md

@ -1,4 +1,3 @@
---
title: 'Import: Airtable to NocoDB'
description: 'Import: Airtable to NocoDB'
position: 1150
@ -9,7 +8,26 @@ menuTitle: 'Import: Airtable to NocoDB'
## Import Airtable to NocoDB
### Find & enter your Airtable API Key
- TODO
Copy API Key from [Airtable Accounts](https://airtable.com/account) page
![Screenshot 2022-05-16 at 1 50 07 PM](https://user-images.githubusercontent.com/86527202/168569905-48c16d6d-c44a-4337-be49-0ac3dc1f7b75.png)
### Share you Airtable base
- TODO
Detailed procedure is captured [here](https://support.airtable.com/hc/en-us/articles/205752117-Creating-a-base-share-link-or-a-view-share-link#basesharelink)
Quick steps
1. Open `Share` menu in your Project/ Base
![Screenshot 2022-05-16 at 3 47 27 PM](https://user-images.githubusercontent.com/86527202/168572054-533b8c19-d76e-4add-b876-f1e0570ac33c.png)
3. Open tab `Share Publicly`
4. Enable `Turn on full base access`
5. Copy generated shared base URL
![Screenshot 2022-05-16 at 3 41 54 PM](https://user-images.githubusercontent.com/86527202/168572062-5dee065d-2394-426d-8f43-77ecc0c9b73f.png)

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

@ -1,4 +1,4 @@
import UITypes from "../UITypes";
import UITypes from '../UITypes';
const dbTypes = [
'bigint',
@ -35,15 +35,15 @@ const dbTypes = [
'uniqueidentifier',
'varbinary',
'xml',
'varchar'
'varchar',
];
export class MssqlUi {
static getNewTableColumns() {
return [
{
column_name: 'id',
title: 'Id',
column_name: 'id',
title: 'Id',
dt: 'int',
dtx: 'integer',
ct: 'int(11)',
@ -62,11 +62,11 @@ export class MssqlUi {
altered: 1,
uidt: 'ID',
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'title',
title: 'Title',
column_name: 'title',
title: 'Title',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar(45)',
@ -85,11 +85,11 @@ export class MssqlUi {
altered: 1,
uidt: 'SingleLineText',
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'created_at',
title: 'CreatedAt',
column_name: 'created_at',
title: 'CreatedAt',
dt: 'datetime',
dtx: 'specificType',
ct: 'varchar(45)',
@ -108,11 +108,11 @@ export class MssqlUi {
altered: 1,
uidt: UITypes.DateTime,
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'updated_at',
title: 'UpdatedAt',
column_name: 'updated_at',
title: 'UpdatedAt',
dt: 'datetime',
dtx: 'specificType',
ct: 'varchar(45)',
@ -131,8 +131,8 @@ export class MssqlUi {
altered: 1,
uidt: UITypes.DateTime,
uip: '',
uicn: ''
}
uicn: '',
},
];
}
@ -157,7 +157,7 @@ export class MssqlUi {
altered: 1,
uidt: 'SingleLineText',
uip: '',
uicn: ''
uicn: '',
};
}
@ -741,13 +741,15 @@ export class MssqlUi {
}
static extractFunctionName(query) {
const reg = /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const reg =
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const match = query.match(reg);
return match && match[1];
}
static extractProcedureName(query) {
const reg = /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const reg =
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const match = query.match(reg);
return match && match[1];
}
@ -808,7 +810,7 @@ export class MssqlUi {
const column = {
dp: null,
tn,
column_name: keys[i],
column_name: keys[i],
cno: keys[i],
np: 10,
ns: 0,
@ -827,7 +829,7 @@ export class MssqlUi {
dtx: 'specificType',
dtxp: null,
dtxs: 0,
altered: 1
altered: 1,
};
switch (typeof json[keys[i]]) {
@ -835,13 +837,13 @@ export class MssqlUi {
if (Number.isInteger(json[keys[i]])) {
if (MssqlUi.isValidTimestamp(keys[i], json[keys[i]])) {
Object.assign(column, {
dt: 'timestamp'
dt: 'timestamp',
});
} else {
Object.assign(column, {
dt: 'int',
np: 10,
ns: 0
ns: 0,
});
}
} else {
@ -850,25 +852,25 @@ export class MssqlUi {
np: 10,
ns: 2,
dtxp: '11',
dtxs: 2
dtxs: 2,
});
}
break;
case 'string':
if (MssqlUi.isValidDate(json[keys[i]])) {
Object.assign(column, {
dt: 'datetime'
dt: 'datetime',
});
} else if (json[keys[i]].length <= 255) {
Object.assign(column, {
dt: 'varchar',
np: 255,
ns: 0,
dtxp: '255'
dtxp: '255',
});
} else {
Object.assign(column, {
dt: 'text'
dt: 'text',
});
}
break;
@ -876,7 +878,7 @@ export class MssqlUi {
Object.assign(column, {
dt: 'bit',
np: null,
ns: 0
ns: 0,
});
break;
case 'object':
@ -884,7 +886,7 @@ export class MssqlUi {
dt: 'varchar',
np: 255,
ns: 0,
dtxp: '255'
dtxp: '255',
});
break;
default:
@ -1043,9 +1045,7 @@ export class MssqlUi {
}
}
static getDataTypeForUiType(
col
): {
static getDataTypeForUiType(col): {
readonly dt: string;
readonly [key: string]: any;
} {
@ -1098,7 +1098,7 @@ export class MssqlUi {
colProp.validate = {
func: ['isMobilePhone'],
args: [''],
msg: ['Validation failed : isMobilePhone']
msg: ['Validation failed : isMobilePhone'],
};
break;
case 'Email':
@ -1106,7 +1106,7 @@ export class MssqlUi {
colProp.validate = {
func: ['isEmail'],
args: [''],
msg: ['Validation failed : isEmail']
msg: ['Validation failed : isEmail'],
};
break;
case 'URL':
@ -1114,7 +1114,7 @@ export class MssqlUi {
colProp.validate = {
func: ['isURL'],
args: [''],
msg: ['Validation failed : isURL']
msg: ['Validation failed : isURL'],
};
break;
case 'Number':
@ -1128,7 +1128,7 @@ export class MssqlUi {
colProp.validate = {
func: ['isCurrency'],
args: [''],
msg: ['Validation failed : isCurrency']
msg: ['Validation failed : isCurrency'],
};
break;
case 'Percent':
@ -1138,7 +1138,7 @@ export class MssqlUi {
colProp.dt = 'int';
break;
case 'Rating':
colProp.dt = 'float';
colProp.dt = 'int';
break;
case 'Formula':
colProp.dt = 'varchar';
@ -1224,7 +1224,7 @@ export class MssqlUi {
'numeric',
'real',
'smallint',
'tinyint'
'tinyint',
];
case 'Decimal':
@ -1240,7 +1240,7 @@ export class MssqlUi {
'numeric',
'real',
'smallint',
'tinyint'
'tinyint',
];
case 'Percent':
@ -1253,7 +1253,7 @@ export class MssqlUi {
'numeric',
'real',
'smallint',
'tinyint'
'tinyint',
];
case 'Duration':
@ -1266,7 +1266,7 @@ export class MssqlUi {
'numeric',
'real',
'smallint',
'tinyint'
'tinyint',
];
case 'Rating':
@ -1279,7 +1279,7 @@ export class MssqlUi {
'numeric',
'real',
'smallint',
'tinyint'
'tinyint',
];
case 'Formula':
@ -1302,7 +1302,7 @@ export class MssqlUi {
case 'LastModifiedTime':
return [
'datetimeoffset',
'datetime2'
'datetime2',
// 'datetime'
];

74
packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts

@ -1,4 +1,4 @@
import UITypes from "../UITypes";
import UITypes from '../UITypes';
const dbTypes = [
'int',
@ -39,7 +39,7 @@ const dbTypes = [
'multipoint',
'multilinestring',
'multipolygon',
'json'
'json',
];
export class MysqlUi {
@ -66,7 +66,7 @@ export class MysqlUi {
altered: 1,
uidt: 'ID',
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'title',
@ -89,7 +89,7 @@ export class MysqlUi {
altered: 1,
uidt: 'SingleLineText',
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'created_at',
@ -112,7 +112,7 @@ export class MysqlUi {
altered: 1,
uidt: UITypes.DateTime,
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'updated_at',
@ -135,8 +135,8 @@ export class MysqlUi {
altered: 1,
uidt: UITypes.DateTime,
uip: '',
uicn: ''
}
uicn: '',
},
];
}
@ -161,7 +161,7 @@ export class MysqlUi {
altered: 1,
uidt: 'SingleLineText',
uip: '',
uicn: ''
uicn: '',
};
}
@ -659,13 +659,15 @@ export class MysqlUi {
}
static extractFunctionName(query) {
const reg = /^\s*CREATE\s+.*?(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:`?[\w\d_]+`?\.)?`?([\w_\d]+)`?/i;
const reg =
/^\s*CREATE\s+.*?(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:`?[\w\d_]+`?\.)?`?([\w_\d]+)`?/i;
const match = query.match(reg);
return match && match[1];
}
static extractProcedureName(query) {
const reg = /^\s*CREATE.*?\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const reg =
/^\s*CREATE.*?\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const match = query.match(reg);
return match && match[1];
}
@ -677,14 +679,14 @@ export class MysqlUi {
// set headers before settings result
for (let i = 0; i < keys.length; i++) {
const text = keys[i];
headers.push({text, value: text, sortable: false});
headers.push({ text, value: text, sortable: false });
}
} else {
const keys = Object.keys(result);
for (let i = 0; i < keys.length; i++) {
const text = keys[i];
if (typeof text !== 'function') {
headers.push({text, value: text, sortable: false});
headers.push({ text, value: text, sortable: false });
}
}
result = [result];
@ -750,7 +752,7 @@ export class MysqlUi {
dtx: 'specificType',
dtxp: null,
dtxs: 0,
altered: 1
altered: 1,
};
switch (typeof json[keys[i]]) {
@ -758,13 +760,13 @@ export class MysqlUi {
if (Number.isInteger(json[keys[i]])) {
if (MysqlUi.isValidTimestamp(keys[i], json[keys[i]])) {
Object.assign(column, {
dt: 'timestamp'
dt: 'timestamp',
});
} else {
Object.assign(column, {
dt: 'int',
np: 10,
ns: 0
ns: 0,
});
}
} else {
@ -773,25 +775,25 @@ export class MysqlUi {
np: 10,
ns: 2,
dtxp: '11',
dtxs: 2
dtxs: 2,
});
}
break;
case 'string':
if (MysqlUi.isValidDate(json[keys[i]])) {
Object.assign(column, {
dt: 'datetime'
dt: 'datetime',
});
} else if (json[keys[i]].length <= 255) {
Object.assign(column, {
dt: 'varchar',
np: 255,
ns: 0,
dtxp: '255'
dtxp: '255',
});
} else {
Object.assign(column, {
dt: 'text'
dt: 'text',
});
}
break;
@ -799,14 +801,14 @@ export class MysqlUi {
Object.assign(column, {
dt: 'boolean',
np: 3,
ns: 0
ns: 0,
});
break;
case 'object':
Object.assign(column, {
dt: 'json',
np: 3,
ns: 0
ns: 0,
});
break;
default:
@ -991,7 +993,7 @@ export class MysqlUi {
colProp.validate = {
func: ['isMobilePhone'],
args: [''],
msg: ['Validation failed : isMobilePhone ({cn})']
msg: ['Validation failed : isMobilePhone ({cn})'],
};
break;
case 'Email':
@ -999,7 +1001,7 @@ export class MysqlUi {
colProp.validate = {
func: ['isEmail'],
args: [''],
msg: ['Validation failed : isEmail ({cn})']
msg: ['Validation failed : isEmail ({cn})'],
};
break;
case 'URL':
@ -1007,7 +1009,7 @@ export class MysqlUi {
colProp.validate = {
func: ['isURL'],
args: [''],
msg: ['Validation failed : isURL ({cn})']
msg: ['Validation failed : isURL ({cn})'],
};
break;
case 'Number':
@ -1021,7 +1023,7 @@ export class MysqlUi {
colProp.validate = {
func: ['isCurrency'],
args: [''],
msg: ['Validation failed : isCurrency']
msg: ['Validation failed : isCurrency'],
};
break;
case 'Percent':
@ -1031,7 +1033,7 @@ export class MysqlUi {
colProp.dt = 'int';
break;
case 'Rating':
colProp.dt = 'float';
colProp.dt = 'int';
break;
case 'Formula':
colProp.dt = 'varchar';
@ -1089,7 +1091,7 @@ export class MysqlUi {
'text',
'tinytext',
'mediumtext',
'longtext'
'longtext',
];
case 'Attachment':
@ -1101,7 +1103,7 @@ export class MysqlUi {
'text',
'tinytext',
'mediumtext',
'longtext'
'longtext',
];
case 'JSON':
@ -1116,7 +1118,7 @@ export class MysqlUi {
'bit',
'boolean',
'serial',
'tinyint'
'tinyint',
];
case 'MultiSelect':
@ -1148,7 +1150,7 @@ export class MysqlUi {
'float',
'decimal',
'double',
'serial'
'serial',
];
case 'Decimal':
@ -1164,7 +1166,7 @@ export class MysqlUi {
'smallint',
'mediumint',
'bigint',
'bit'
'bit',
];
case 'Percent':
@ -1177,7 +1179,7 @@ export class MysqlUi {
'smallint',
'mediumint',
'bigint',
'bit'
'bit',
];
case 'Duration':
@ -1190,7 +1192,7 @@ export class MysqlUi {
'smallint',
'mediumint',
'bigint',
'bit'
'bit',
];
case 'Rating':
@ -1203,7 +1205,7 @@ export class MysqlUi {
'smallint',
'mediumint',
'bigint',
'bit'
'bit',
];
case 'Formula':
@ -1214,7 +1216,7 @@ export class MysqlUi {
'text',
'tinytext',
'mediumtext',
'longtext'
'longtext',
];
case 'Rollup':
@ -1248,7 +1250,7 @@ export class MysqlUi {
'polygon',
'multipoint',
'multilinestring',
'multipolygon'
'multipolygon',
];
case 'Button':

36
packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts

@ -22,7 +22,7 @@ export class OracleUi {
altered: 1,
uidt: 'ID',
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'title',
@ -45,8 +45,8 @@ export class OracleUi {
altered: 1,
uidt: 'SingleLineText',
uip: '',
uicn: ''
}
uicn: '',
},
// {
// column_name: "created_at",
// dt: "timestamp",
@ -107,7 +107,7 @@ export class OracleUi {
altered: 1,
uidt: 'SingleLineText',
uip: '',
uicn: ''
uicn: '',
};
}
@ -435,13 +435,15 @@ export class OracleUi {
}
static extractFunctionName(query) {
const reg = /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const reg =
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const match = query.match(reg);
return match && match[1];
}
static extractProcedureName(query) {
const reg = /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const reg =
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const match = query.match(reg);
return match && match[1];
}
@ -522,7 +524,7 @@ export class OracleUi {
dtx: 'specificType',
dtxp: '11',
dtxs: 0,
altered: 1
altered: 1,
});
} else {
columns.push({
@ -548,7 +550,7 @@ export class OracleUi {
dtx: 'specificType',
dtxp: '11',
dtxs: 2,
altered: 1
altered: 1,
});
}
@ -579,7 +581,7 @@ export class OracleUi {
dtx: 'specificType',
dtxp: '45',
dtxs: 0,
altered: 1
altered: 1,
});
} else {
columns.push({
@ -605,7 +607,7 @@ export class OracleUi {
dtx: 'specificType',
dtxp: null,
dtxs: 0,
altered: 1
altered: 1,
});
}
@ -635,7 +637,7 @@ export class OracleUi {
dtx: 'specificType',
dtxp: '1',
dtxs: 0,
altered: 1
altered: 1,
});
break;
@ -663,7 +665,7 @@ export class OracleUi {
dtx: 'specificType',
dtxp: null,
dtxs: 0,
altered: 1
altered: 1,
});
break;
@ -839,7 +841,7 @@ export class OracleUi {
colProp.validate = {
func: ['isMobilePhone'],
args: [''],
msg: ['Validation failed : isMobilePhone']
msg: ['Validation failed : isMobilePhone'],
};
break;
case 'Email':
@ -847,7 +849,7 @@ export class OracleUi {
colProp.validate = {
func: ['isEmail'],
args: [''],
msg: ['Validation failed : isEmail']
msg: ['Validation failed : isEmail'],
};
break;
case 'URL':
@ -855,7 +857,7 @@ export class OracleUi {
colProp.validate = {
func: ['isURL'],
args: [''],
msg: ['Validation failed : isURL']
msg: ['Validation failed : isURL'],
};
break;
case 'Number':
@ -869,7 +871,7 @@ export class OracleUi {
colProp.validate = {
func: ['isCurrency'],
args: [''],
msg: ['Validation failed : isCurrency']
msg: ['Validation failed : isCurrency'],
};
break;
case 'Percent':
@ -879,7 +881,7 @@ export class OracleUi {
colProp.dt = 'integer';
break;
case 'Rating':
colProp.dt = 'float';
colProp.dt = 'integer';
break;
case 'Formula':
colProp.dt = 'varchar';

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

@ -1,4 +1,4 @@
import UITypes from "../UITypes";
import UITypes from '../UITypes';
const dbTypes = [
'int',
@ -98,15 +98,15 @@ const dbTypes = [
'unknown',
'void',
'xid',
'xml'
'xml',
];
export class PgUi {
static getNewTableColumns() {
return [
{
column_name: 'id',
title: 'Id',
column_name: 'id',
title: 'Id',
dt: 'int4',
dtx: 'integer',
ct: 'int(11)',
@ -125,11 +125,11 @@ export class PgUi {
altered: 1,
uidt: 'ID',
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'title',
title: 'Title',
column_name: 'title',
title: 'Title',
dt: 'character varying',
dtx: 'specificType',
ct: 'varchar(45)',
@ -148,11 +148,11 @@ export class PgUi {
altered: 1,
uidt: 'SingleLineText',
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'created_at',
title: 'CreatedAt',
column_name: 'created_at',
title: 'CreatedAt',
dt: 'timestamp',
dtx: 'specificType',
ct: 'varchar(45)',
@ -171,11 +171,11 @@ export class PgUi {
altered: 1,
uidt: UITypes.DateTime,
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'updated_at',
title: 'UpdatedAt',
column_name: 'updated_at',
title: 'UpdatedAt',
dt: 'timestamp',
dtx: 'specificType',
ct: 'varchar(45)',
@ -195,8 +195,8 @@ export class PgUi {
altered: 1,
uidt: UITypes.DateTime,
uip: '',
uicn: ''
}
uicn: '',
},
];
}
@ -221,7 +221,7 @@ export class PgUi {
altered: 1,
uidt: 'SingleLineText',
uip: '',
uicn: ''
uicn: '',
};
}
@ -1160,13 +1160,15 @@ export class PgUi {
}
static extractFunctionName(query) {
const reg = /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const reg =
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const match = query.match(reg);
return match && match[1];
}
static extractProcedureName(query) {
const reg = /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const reg =
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const match = query.match(reg);
return match && match[1];
}
@ -1176,8 +1178,8 @@ export class PgUi {
headers.push({ text: 'Row count', value: 'rowCount', sortable: false });
result = [
{
rowCount: result.rowCount
}
rowCount: result.rowCount,
},
];
} else {
result = result.rows;
@ -1232,7 +1234,7 @@ export class PgUi {
const column = {
dp: null,
tn,
column_name: keys[i],
column_name: keys[i],
cno: keys[i],
np: 10,
ns: 0,
@ -1251,7 +1253,7 @@ export class PgUi {
dtx: 'specificType',
dtxp: null,
dtxs: 0,
altered: 1
altered: 1,
};
switch (typeof json[keys[i]]) {
@ -1259,13 +1261,13 @@ export class PgUi {
if (Number.isInteger(json[keys[i]])) {
if (PgUi.isValidTimestamp(keys[i], json[keys[i]])) {
Object.assign(column, {
dt: 'timestamp'
dt: 'timestamp',
});
} else {
Object.assign(column, {
dt: 'int',
np: 10,
ns: 0
ns: 0,
});
}
} else {
@ -1274,25 +1276,25 @@ export class PgUi {
np: null,
ns: null,
dtxp: null,
dtxs: null
dtxs: null,
});
}
break;
case 'string':
if (PgUi.isValidDate(json[keys[i]])) {
Object.assign(column, {
dt: 'date'
dt: 'date',
});
} else if (json[keys[i]].length <= 255) {
Object.assign(column, {
dt: 'character varying',
np: null,
ns: 0,
dtxp: null
dtxp: null,
});
} else {
Object.assign(column, {
dt: 'text'
dt: 'text',
});
}
break;
@ -1300,14 +1302,14 @@ export class PgUi {
Object.assign(column, {
dt: 'boolean',
np: 3,
ns: 0
ns: 0,
});
break;
case 'object':
Object.assign(column, {
dt: 'json',
np: 3,
ns: 0
ns: 0,
});
break;
default:
@ -1521,7 +1523,6 @@ export class PgUi {
case 'longblob':
return 'blob';
case 'enum':
return 'enum';
case 'set':
return 'set';
@ -1616,7 +1617,7 @@ export class PgUi {
colProp.validate = {
func: ['isMobilePhone'],
args: [''],
msg: ['Validation failed : isMobilePhone']
msg: ['Validation failed : isMobilePhone'],
};
break;
case 'Email':
@ -1624,7 +1625,7 @@ export class PgUi {
colProp.validate = {
func: ['isEmail'],
args: [''],
msg: ['Validation failed : isEmail']
msg: ['Validation failed : isEmail'],
};
break;
case 'URL':
@ -1632,7 +1633,7 @@ export class PgUi {
colProp.validate = {
func: ['isURL'],
args: [''],
msg: ['Validation failed : isURL']
msg: ['Validation failed : isURL'],
};
break;
case 'Number':
@ -1646,7 +1647,7 @@ export class PgUi {
colProp.validate = {
func: ['isCurrency'],
args: [''],
msg: ['Validation failed : isCurrency']
msg: ['Validation failed : isCurrency'],
};
break;
case 'Percent':
@ -1656,7 +1657,7 @@ export class PgUi {
colProp.dt = 'int8';
break;
case 'Rating':
colProp.dt = 'float8';
colProp.dt = 'smallint';
break;
case 'Formula':
colProp.dt = 'character varying';
@ -1732,7 +1733,7 @@ export class PgUi {
'int8range',
'serial',
'serial2',
'serial8'
'serial8',
];
case 'MultiSelect':
@ -1753,7 +1754,7 @@ export class PgUi {
'timestamptz',
'timestamp with time zone',
'timetz',
'time with time zone'
'time with time zone',
];
case 'PhoneNumber':
@ -1780,7 +1781,7 @@ export class PgUi {
'float8',
'smallint',
'smallserial',
'numeric'
'numeric',
];
case 'Decimal':
@ -1802,7 +1803,7 @@ export class PgUi {
'money',
'float4',
'float8',
'numeric'
'numeric',
];
case 'Percent':
@ -1822,7 +1823,7 @@ export class PgUi {
'float8',
'smallint',
'smallserial',
'numeric'
'numeric',
];
case 'Duration':
@ -1842,7 +1843,7 @@ export class PgUi {
'float8',
'smallint',
'smallserial',
'numeric'
'numeric',
];
case 'Rating':
@ -1862,7 +1863,7 @@ export class PgUi {
'float8',
'smallint',
'smallserial',
'numeric'
'numeric',
];
case 'Formula':
@ -1884,7 +1885,7 @@ export class PgUi {
'serial2',
'serial8',
'smallint',
'smallserial'
'smallserial',
];
case 'Lookup':
@ -1896,7 +1897,7 @@ export class PgUi {
'timestamp',
'timestamp without time zone',
'timestamptz',
'timestamp with time zone'
'timestamp with time zone',
];
case 'DateTime':
@ -1906,7 +1907,7 @@ export class PgUi {
'timestamp',
'timestamp without time zone',
'timestamptz',
'timestamp with time zone'
'timestamp with time zone',
];
case 'AutoNumber':
@ -1922,7 +1923,7 @@ export class PgUi {
'serial2',
'serial8',
'smallint',
'smallserial'
'smallserial',
];
case 'Barcode':
@ -1937,7 +1938,7 @@ export class PgUi {
'line',
'lseg',
'path',
'circle'
'circle',
];
case 'Button':

94
packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts

@ -1,4 +1,4 @@
import UITypes from "../UITypes";
import UITypes from '../UITypes';
const dbTypes = [
'int',
@ -22,15 +22,15 @@ const dbTypes = [
'datetime',
'text',
'varchar',
'timestamp'
'timestamp',
];
export class SqliteUi {
static getNewTableColumns() {
return [
{
column_name: 'id',
title: 'Id',
column_name: 'id',
title: 'Id',
dt: 'integer',
dtx: 'integer',
ct: 'int(11)',
@ -49,11 +49,11 @@ export class SqliteUi {
altered: 1,
uidt: 'ID',
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'title',
title: 'Title',
column_name: 'title',
title: 'Title',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar',
@ -72,11 +72,11 @@ export class SqliteUi {
altered: 1,
uidt: 'SingleLineText',
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'created_at',
title: 'CreatedAt',
column_name: 'created_at',
title: 'CreatedAt',
dt: 'datetime',
dtx: 'specificType',
ct: 'varchar',
@ -95,11 +95,11 @@ export class SqliteUi {
altered: 1,
uidt: UITypes.DateTime,
uip: '',
uicn: ''
uicn: '',
},
{
column_name: 'updated_at',
title: 'UpdatedAt',
column_name: 'updated_at',
title: 'UpdatedAt',
dt: 'datetime',
dtx: 'specificType',
ct: 'varchar',
@ -118,8 +118,8 @@ export class SqliteUi {
altered: 1,
uidt: UITypes.DateTime,
uip: '',
uicn: ''
}
uicn: '',
},
];
}
@ -144,7 +144,7 @@ export class SqliteUi {
altered: 1,
uidt: 'SingleLineText',
uip: '',
uicn: ''
uicn: '',
};
}
@ -498,13 +498,15 @@ export class SqliteUi {
}
static extractFunctionName(query) {
const reg = /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const reg =
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const match = query.match(reg);
return match && match[1];
}
static extractProcedureName(query) {
const reg = /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const reg =
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const match = query.match(reg);
return match && match[1];
}
@ -565,7 +567,7 @@ export class SqliteUi {
const column = {
dp: null,
tn,
column_name: keys[i],
column_name: keys[i],
cno: keys[i],
np: null,
ns: null,
@ -584,7 +586,7 @@ export class SqliteUi {
dtx: 'specificType',
dtxp: null,
dtxs: 0,
altered: 1
altered: 1,
};
switch (typeof json[keys[i]]) {
@ -592,16 +594,16 @@ export class SqliteUi {
if (Number.isInteger(json[keys[i]])) {
if (SqliteUi.isValidTimestamp(keys[i], json[keys[i]])) {
Object.assign(column, {
dt: 'timestamp'
dt: 'timestamp',
});
} else {
Object.assign(column, {
dt: 'integer'
dt: 'integer',
});
}
} else {
Object.assign(column, {
dt: 'real'
dt: 'real',
});
}
break;
@ -613,24 +615,24 @@ export class SqliteUi {
// } else
if (json[keys[i]].length <= 255) {
Object.assign(column, {
dt: 'varchar'
dt: 'varchar',
});
} else {
Object.assign(column, {
dt: 'text'
dt: 'text',
});
}
break;
case 'boolean':
Object.assign(column, {
dt: 'integer'
dt: 'integer',
});
break;
case 'object':
Object.assign(column, {
dt: 'text',
np: null,
dtxp: null
dtxp: null,
});
break;
default:
@ -807,7 +809,7 @@ export class SqliteUi {
colProp.validate = {
func: ['isMobilePhone'],
args: [''],
msg: ['Validation failed : isMobilePhone']
msg: ['Validation failed : isMobilePhone'],
};
break;
case 'Email':
@ -815,7 +817,7 @@ export class SqliteUi {
colProp.validate = {
func: ['isEmail'],
args: [''],
msg: ['Validation failed : isEmail']
msg: ['Validation failed : isEmail'],
};
break;
case 'URL':
@ -823,7 +825,7 @@ export class SqliteUi {
colProp.validate = {
func: ['isURL'],
args: [''],
msg: ['Validation failed : isURL']
msg: ['Validation failed : isURL'],
};
break;
case 'Number':
@ -837,7 +839,7 @@ export class SqliteUi {
colProp.validate = {
func: ['isCurrency'],
args: [''],
msg: ['Validation failed : isCurrency']
msg: ['Validation failed : isCurrency'],
};
break;
case 'Percent':
@ -847,7 +849,7 @@ export class SqliteUi {
colProp.dt = 'integer';
break;
case 'Rating':
colProp.dt = 'float';
colProp.dt = 'integer';
break;
case 'Formula':
colProp.dt = 'varchar';
@ -911,7 +913,7 @@ export class SqliteUi {
'bigint',
'int2',
'int8',
'boolean'
'boolean',
];
case 'MultiSelect':
@ -929,7 +931,7 @@ export class SqliteUi {
'mediumint',
'bigint',
'int2',
'int8'
'int8',
];
case 'Time':
@ -941,7 +943,7 @@ export class SqliteUi {
'mediumint',
'bigint',
'int2',
'int8'
'int8',
];
case 'PhoneNumber':
@ -965,7 +967,7 @@ export class SqliteUi {
'real',
'double',
'double precision',
'float'
'float',
];
case 'Decimal':
@ -985,7 +987,7 @@ export class SqliteUi {
'bigint',
'int2',
'int8',
'numeric'
'numeric',
];
case 'Percent':
@ -1002,7 +1004,7 @@ export class SqliteUi {
'bigint',
'int2',
'int8',
'numeric'
'numeric',
];
case 'Duration':
@ -1014,15 +1016,11 @@ export class SqliteUi {
'mediumint',
'bigint',
'int2',
'int8'
'int8',
];
case 'Rating':
return [
'real',
'double',
'double precision',
'float',
'int',
'integer',
'tinyint',
@ -1031,7 +1029,11 @@ export class SqliteUi {
'bigint',
'int2',
'int8',
'numeric'
'numeric',
'real',
'double',
'double precision',
'float',
];
case 'Formula':
@ -1049,7 +1051,7 @@ export class SqliteUi {
'mediumint',
'bigint',
'int2',
'int8'
'int8',
];
case 'Lookup':
@ -1072,7 +1074,7 @@ export class SqliteUi {
'mediumint',
'bigint',
'int2',
'int8'
'int8',
];
case 'Barcode':

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

@ -1776,6 +1776,9 @@ class BaseModelSqlv2 {
// let cols = Object.keys(this.columns);
for (let i = 0; i < this.model.columns.length; ++i) {
const column = this.model.columns[i];
// skip validation if `validate` is undefined or false
if (!column?.meta?.validate) continue;
const validate = column.getValidators();
const cn = column.column_name;
if (!validate) continue;

40
packages/nocodb/src/lib/noco-models/Column.ts

@ -56,6 +56,7 @@ export default class Column<T = any> implements ColumnType {
public order: number;
public validate: any;
public meta: any;
constructor(data: Partial<ColumnType | Column>) {
Object.assign(this, data);
@ -106,7 +107,11 @@ export default class Column<T = any> implements ColumnType {
order: column.order,
project_id: column.project_id,
base_id: column.base_id,
system: column.system
system: column.system,
meta:
column.meta && typeof column.meta === 'object'
? JSON.stringify(column.meta)
: column.meta
};
if (column.validate) {
@ -375,6 +380,17 @@ export default class Column<T = any> implements ColumnType {
order: 'asc'
}
});
columnsList.forEach(column => {
if (column.meta && typeof column.meta === 'string') {
try {
column.meta = JSON.parse(column.meta);
} catch {
column.meta = {};
}
}
});
await NocoCache.setList(CacheScope.COLUMN, [fk_model_id], columnsList);
}
columnsList.sort(
@ -452,6 +468,11 @@ export default class Column<T = any> implements ColumnType {
MetaTable.COLUMNS,
colId
);
try {
colData.meta = JSON.parse(colData.meta);
} catch {
colData.meta = {};
}
await NocoCache.set(`${CacheScope.COLUMN}:${colId}`, colData);
}
if (colData) {
@ -783,7 +804,8 @@ export default class Column<T = any> implements ColumnType {
au: column.au,
pv: column.pv,
system: column.system,
validate: null
validate: null,
meta: column.meta
};
if (column.validate) {
@ -801,7 +823,19 @@ export default class Column<T = any> implements ColumnType {
await NocoCache.set(key, o);
}
// set meta
await ncMeta.metaUpdate(null, null, MetaTable.COLUMNS, updateObj, colId);
await ncMeta.metaUpdate(
null,
null,
MetaTable.COLUMNS,
{
...updateObj,
meta:
updateObj.meta && typeof updateObj.meta === 'object'
? JSON.stringify(updateObj.meta)
: updateObj.meta
},
colId
);
await this.insertColOption(column, colId, ncMeta);
}

6
packages/nocodb/src/lib/noco/common/XcMigrationSourcev2.ts

@ -2,6 +2,7 @@ import * as nc_011 from '../migrationsv2/nc_011';
import * as nc_012_alter_column_data_types from '../migrationsv2/nc_012_alter_column_data_types';
import * as nc_013_sync_source from '../migrationsv2/nc_013_sync_source';
import * as nc_014_alter_column_data_types from '../migrationsv2/nc_014_alter_column_data_types';
import * as nc_015_add_meta_col_in_column_table from '../migrationsv2/nc_015_add_meta_col_in_column_table';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@ -14,7 +15,8 @@ export default class XcMigrationSourcev2 {
'nc_011',
'nc_012_alter_column_data_types',
'nc_013_sync_source',
'nc_014_alter_column_data_types'
'nc_014_alter_column_data_types',
'nc_015_add_meta_col_in_column_table'
]);
}
@ -32,6 +34,8 @@ export default class XcMigrationSourcev2 {
return nc_013_sync_source;
case 'nc_014_alter_column_data_types':
return nc_014_alter_column_data_types;
case 'nc_015_add_meta_col_in_column_table':
return nc_015_add_meta_col_in_column_table;
}
}
}

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

@ -95,7 +95,7 @@ export default async (
async function getAtableSchema(sDB) {
const start = Date.now();
if(sDB.shareId.startsWith('exp')) {
if (sDB.shareId.startsWith('exp')) {
const template = await FetchAT.readTemplate(sDB.shareId);
await FetchAT.initialize(template.template.exploreApplication.shareId);
} else {
@ -160,9 +160,14 @@ export default async (
function nc_sanitizeName(name) {
// knex complains use of '?' in field name
// good to replace all special characters by _ in one go
// https://stackoverflow.com/questions/18862256/how-to-detect-emoji-using-javascript
const regex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g;
const col_name = name
.replace(/\?/g, 'QQ')
.replace(/\?/g, '_')
.replace('.', '_')
.replace(regex, '_')
.trim();
return col_name;
@ -1980,7 +1985,9 @@ export default async (
progress(`Linked data to ${ncTbl.title}`);
}
} catch (error) {
progress(`There was an error while migrating data! Please make sure your API key (${syncDB.apiKey}) is correct.`);
progress(
`There was an error while migrating data! Please make sure your API key (${syncDB.apiKey}) is correct.`
);
progress(`Error: ${error}`);
}
}

38
packages/nocodb/src/lib/noco/migrationsv2/nc_015_add_meta_col_in_column_table.ts

@ -0,0 +1,38 @@
import Knex from 'knex';
import { MetaTable } from '../../utils/globals';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.COLUMNS, table => {
table.text('meta');
});
};
const down = async knex => {
await knex.schema.alterTable(MetaTable.COLUMNS, table => {
table.dropColumn('meta');
});
};
export { up, down };
/**
* @copyright Copyright (c) 2022, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@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/>.
*
*/

4
packages/nocodb/src/lib/noco/upgrader/jobs/ncProjectUpgraderV2_0090000.ts

@ -436,6 +436,10 @@ async function migrateProjectModels(
columnMeta.uidt = UITypes.ForeignKey;
}
if (columnMeta.uidt === UITypes.Rating) {
columnMeta.uidt = UITypes.Number;
}
const column = await Column.insert(
{
...columnMeta,

10
scripts/markdown/readme/languages/chinese.md

@ -87,22 +87,22 @@ npm start
![1](https://user-images.githubusercontent.com/86527202/136066713-5408634f-5469-40eb-94c9-7eafae5e179c.png)
<br>
![2](https://user-images.githubusercontent.com/86527202/136066729-9b6a261a-231d-4d7f-9fc2-061c301d6192.png)
![2](https://user-images.githubusercontent.com/86527202/168545293-b48a4237-8646-4f9a-a56b-56d5c55adc79.jpg)
<br>
![5](https://user-images.githubusercontent.com/86527202/136066734-3f25aecc-bb7e-4db7-81c4-00b368d799d3.png)
<br>
![6](https://user-images.githubusercontent.com/86527202/136066735-2d4cb656-02dc-4233-ac4a-1ba9bd8acdf0.png)
![6](https://user-images.githubusercontent.com/86527202/168545527-3948125d-1640-4c07-ac80-15db9a85f66f.jpg)
<br>
![7](https://user-images.githubusercontent.com/86527202/136066737-eb6a56fb-5e2e-4423-912b-ced32e8b479c.png)
![7](https://user-images.githubusercontent.com/86527202/168545772-dfbffe13-bcf6-4a49-8a10-1bc8a933d77e.jpg)
<br>
![8](https://user-images.githubusercontent.com/86527202/136066742-94c7eff7-d88e-4002-ad72-ffd23090847c.png)
![8](https://user-images.githubusercontent.com/86527202/168545839-0ba950a4-400f-45b2-b2db-b22b1853af4a.jpg)
<br>
![9](https://user-images.githubusercontent.com/86527202/136066743-1b4030c5-042f-4338-99b0-06237878ce53.png)
![9](https://user-images.githubusercontent.com/86527202/168545872-c2a4b63a-9dc9-4c12-add7-69d5f4d0a6e1.jpg)
<br>
![9a](https://user-images.githubusercontent.com/86527202/136066745-9797775d-7db0-4681-ab10-d7ecbbd972ef.png)

Loading…
Cancel
Save