Browse Source

Merge branch 'develop' into feat/gui-v2-form-view

pull/3030/head
Wing-Kam Wong 2 years ago
parent
commit
b622879fad
  1. 20
      packages/nc-gui-v2/app.vue
  2. 5
      packages/nc-gui-v2/components.d.ts
  3. 1
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  4. 4
      packages/nc-gui-v2/nuxt.config.ts
  5. 2
      packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue
  6. 36
      packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue
  7. 7
      packages/nc-gui/components/project/spreadsheet/components/EditableCell.vue
  8. 36
      packages/nc-gui/components/project/spreadsheet/components/cell/EnumCell.vue
  9. 16
      packages/nc-gui/components/project/spreadsheet/components/cell/SetListCell.vue
  10. 188
      packages/nc-gui/components/project/spreadsheet/components/editColumn/CustomSelectOptions.vue
  11. 87
      packages/nc-gui/components/project/spreadsheet/components/editableCell/EnumListEditableCell.vue
  12. 77
      packages/nc-gui/components/project/spreadsheet/components/editableCell/SetListEditableCell.vue
  13. 8
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  14. 8
      packages/nocodb/src/lib/db/sql-client/lib/mssql/MssqlClient.ts
  15. 4
      packages/nocodb/src/lib/db/sql-client/lib/sqlite/SqliteClient.ts
  16. 24
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/sortV2.ts
  17. 366
      packages/nocodb/src/lib/meta/api/columnApis.ts
  18. 64
      packages/nocodb/src/lib/meta/api/sync/helpers/job.ts
  19. 7
      packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts
  20. 78
      packages/nocodb/src/lib/models/Column.ts
  21. 48
      packages/nocodb/src/lib/models/SelectOption.ts
  22. 87
      packages/nocodb/src/lib/models/SingleSelectColumn.ts

20
packages/nc-gui-v2/app.vue

@ -1,9 +1,4 @@
<script lang="ts" setup>
import MdiAt from '~icons/mdi/at'
import MdiLogout from '~icons/mdi/logout'
import MdiDotsVertical from '~icons/mdi/dots-vertical'
import MaterialSymbolsMenu from '~icons/material-symbols/menu'
import MdiReload from '~icons/mdi/reload'
import { navigateTo } from '#app'
import { useGlobal } from '#imports'
@ -31,7 +26,7 @@ const toggleSidebar = () => {
<template>
<a-layout class="min-h-[100vh]">
<a-layout-header class="flex !bg-primary items-center text-white px-4 shadow-md">
<MaterialSymbolsMenu v-if="state.signedIn.value" class="text-xl cursor-pointer" @click="toggleSidebar" />
<material-symbols-menu v-if="state.signedIn.value" class="text-xl cursor-pointer" @click="toggleSidebar" />
<div class="flex-1" />
@ -43,7 +38,7 @@ const toggleSidebar = () => {
<div v-show="state.isLoading.value" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin': state.isLoading.value }" />
<mdi-reload :class="{ 'animate-infinite animate-spin': state.isLoading.value }" />
</div>
</div>
@ -54,7 +49,7 @@ const toggleSidebar = () => {
<template v-if="state.signedIn.value">
<a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />
<mdi-dots-vertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />
<template #overlay>
<a-menu class="!py-0 nc-user-menu min-w-32 dark:(!bg-gray-800) leading-8 !rounded">
@ -69,10 +64,11 @@ const toggleSidebar = () => {
<a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="signOut">
<MdiLogout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">{{
$t('general.signOut')
}}</span>
<mdi-logout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>

5
packages/nc-gui-v2/components.d.ts vendored

@ -61,6 +61,11 @@ declare module '@vue/runtime-core' {
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
MaterialSymbolsMenu: typeof import('~icons/material-symbols/menu')['default']
MdiAt: typeof import('~icons/mdi/at')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}

1
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -270,6 +270,7 @@ const onNavigate = (dir: NavigateDir) => {
active: !isPublicView && selected.col === colIndex && selected.row === rowIndex,
}"
:data-col="columnObj.id"
:data-title="columnObj.title"
@click="selectCell(rowIndex, colIndex)"
@dblclick="editEnabled = true"
@contextmenu="contextMenuTarget = { row: rowIndex, col: colIndex }"

4
packages/nc-gui-v2/nuxt.config.ts

@ -2,6 +2,7 @@ import path from 'path'
import { defineNuxtConfig } from 'nuxt'
import vueI18n from '@intlify/vite-plugin-vue-i18n'
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
@ -68,6 +69,9 @@ export default defineNuxtConfig({
AntDesignVueResolver({
importStyle: 'less',
}),
IconsResolver({
prefix: false,
}),
],
}),
monacoEditorPlugin({

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

@ -889,7 +889,7 @@ export default {
this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.table,
table_name: this.table,
force: true,
});
if (this.selectedView && this.selectedView.show_as === 'kanban') {

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

@ -132,7 +132,7 @@
<date-options v-model="newColumn.meta" :column="newColumn" :meta="meta" />
</v-col>
<v-col v-if="isSelect" cols="12">
<custom-select-options v-model="newColumn.dtxp" @input="newColumn.altered = newColumn.altered || 2" />
<custom-select-options ref="customselect" :column="newColumn" :meta="meta" v-on="$listeners" />
</v-col>
<v-col v-else-if="isRating" cols="12">
<rating-options v-model="newColumn.meta" :column="newColumn" :meta="meta" />
@ -399,7 +399,7 @@
<v-textarea
v-model="newColumn.cdf"
:label="$t('placeholder.defaultValue')"
:hint="sqlUi.getDefaultValueForDatatype(newColumn.dt)"
:hint="defaultValueHint"
persistent-hint
rows="3"
outlined
@ -586,6 +586,14 @@ export default {
isDate() {
return this.newColumn && this.newColumn.uidt === UITypes.Date;
},
defaultValueHint() {
if (this.newColumn.uidt === UITypes.MultiSelect) {
return 'eg : a,b,c';
} else if (this.newColumn.uidt === UITypes.SingleSelect) {
return 'eg : a';
}
return this.sqlUi.getDefaultValueForDatatype(this.newColumn.dt);
},
},
watch: {
column() {
@ -644,10 +652,22 @@ export default {
if (this.newColumn.uidt === 'Formula' && this.$refs.formula) {
return await this.$refs.formula.save();
}
this.newColumn.table_name = this.nodes.table_name;
this.newColumn.title = this.newColumn.column_name;
if (this.isSelect && this.$refs.customselect) {
if (this.column) {
if (await this.$refs.customselect.update()) {
await this.$emit('saved');
return this.$emit('close');
}
} else if (await this.$refs.customselect.save()) {
await this.$emit('saved');
return this.$emit('close');
}
return;
}
if (this.editColumn) {
await this.$api.dbTableColumn.update(this.column.id, this.newColumn);
} else {
@ -686,11 +706,6 @@ export default {
this.newColumn.dtx = 'specificType';
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect];
if (this.column && selectTypes.includes(this.newColumn.uidt) && selectTypes.includes(this.column.uidt)) {
this.newColumn.dtxp = this.column.dtxp;
}
if (this.isCurrency) {
if (this.column?.uidt === UITypes.Currency) {
this.newColumn.dtxp = this.column.dtxp;
@ -722,11 +737,6 @@ export default {
this.newColumn.dtxp = this.sqlUi.getDefaultLengthForDatatype(this.newColumn.dt);
this.newColumn.dtxs = this.sqlUi.getDefaultScaleForDatatype(this.newColumn.dt);
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect];
if (this.column && selectTypes.includes(this.newColumn.uidt) && selectTypes.includes(this.column.uidt)) {
this.newColumn.dtxp = this.column.dtxp;
}
if (columnToValidate.includes(this.newColumn.uidt)) {
this.newColumn.meta = {
validate: this.newColumn.meta && this.newColumn.meta.validate,

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

@ -64,7 +64,9 @@
<date-time-picker-cell v-else-if="isDateTime" v-model="localState" ignore-focus v-on="parentListeners" />
<enum-cell
v-else-if="isEnum && ((!isForm && !active) || isLocked || (isPublic && !isForm))"
v-else-if="
isEnum && ((!isForm && !active) || isLocked || (isPublic && !isForm) || !_isUIAllowed('tableRowUpdate'))
"
v-model="localState"
:column="column"
v-on="parentListeners"
@ -80,9 +82,10 @@
/>
<set-list-editable-cell
v-else-if="isSet && (active || isForm) && !isLocked && !(isPublic && !isForm)"
v-else-if="isSet && (active || isForm) && !isLocked && !(isPublic && !isForm) && _isUIAllowed('tableRowUpdate')"
v-model="localState"
:column="column"
:is-form="isForm"
v-on="parentListeners"
/>
<set-list-cell v-else-if="isSet" v-model="localState" :column="column" v-on="parentListeners" />

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

@ -1,50 +1,40 @@
<template>
<div>
<span
v-for="v in [(value || '').replace(/\\'/g, '\'').replace(/^'|'$/g, '')]"
:key="v"
:style="{
background: colors[v],
}"
<v-chip
v-if="enumValues.find(el => el.title === value)"
:color="enumValues.find(el => el.title === value) ? enumValues.find(el => el.title === value).color : ''"
small
class="set-item ma-1 py-1 px-3"
>{{ v }}</span
>
{{ enumValues.find(el => el.title === value).title }}
</v-chip>
</div>
</template>
<script>
import { enumColor as colors } from '@/components/project/spreadsheet/helpers/colors';
export default {
name: 'EnumCell',
props: ['value', 'column'],
computed: {
colors() {
const col = this.$store.state.settings.darkTheme ? colors.dark : colors.light;
if (this.column && this.column.dtxp) {
return this.column.dtxp
.split(',')
.map(v => v.replace(/\\'/g, "'").replace(/^'|'$/g, ''))
.reduce(
(obj, v, i) => ({
...obj,
[v]: col[i],
}),
{}
);
enumValues() {
const opts = this.column.colOptions ? this.column.colOptions.options.filter(el => el.title !== '') || [] : [];
for (const op of opts.filter(el => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '');
}
return {};
return opts;
},
},
};
</script>
<style scoped>
/*
.set-item {
display: inline-block;
border-radius: 25px;
white-space: nowrap;
}
*/
</style>
<!--
/**

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

@ -2,10 +2,10 @@
<div>
<v-chip
v-for="v in selectedValues"
v-show="v || setValues.includes(v)"
v-show="v && setValues.find(el => el.title === v)"
:key="v"
small
:color="colors[setValues.indexOf(v) % colors.length]"
:color="setValues.find(el => el.title === v) ? setValues.find(el => el.title === v).color : ''"
class="set-item ma-1 py-1 px-3"
>
{{ v }}
@ -14,21 +14,19 @@
</template>
<script>
import colors from '@/mixins/colors';
export default {
name: 'SetListCell',
mixins: [colors],
props: ['value', 'column'],
computed: {
setValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map(v => v.replace(/\\'/g, "'").replace(/^'|'$/g, ''));
const opts = this.column.colOptions ? this.column.colOptions.options.filter(el => el.title !== '') || [] : [];
for (const op of opts.filter(el => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '');
}
return [];
return opts;
},
selectedValues() {
return this.value ? this.value.split(',').map(v => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) : [];
return this.value ? this.value.split(',') : [];
},
},
};

188
packages/nc-gui/components/project/spreadsheet/components/editColumn/CustomSelectOptions.vue

@ -1,65 +1,159 @@
<template>
<v-container fluid class="wrapper">
<div v-for="(op, i) in localState" :key="i" class="d-flex py-1">
<v-icon :color="colors[i % colors.length]" class="mr-2" @click="localState.splice(i, 1)">
mdi-arrow-down-drop-circle
</v-icon>
<v-text-field
v-model="localState[i]"
:autofocus="true"
class="caption"
dense
outlined
@input="listenForComma(i, $event)"
/>
<v-icon class="ml-2" color="error lighten-2" size="13" @click="localState.splice(i, 1)"> mdi-close </v-icon>
<draggable v-model="options" handle=".nc-child-draggable-icon">
<div v-for="(op, i) in options" :key="`${op.color}-${i}`" class="d-flex py-1">
<v-icon small class="nc-child-draggable-icon handle"> mdi-drag-vertical </v-icon>
<v-menu v-model="colorMenus[i]" rounded="lg" :close-on-content-click="false" offset-y>
<template #activator="{ on }">
<v-icon :color="op.color" class="mr-2" v-on="on"> mdi-arrow-down-drop-circle </v-icon>
</template>
<color-picker v-model="op.color" @input="colorMenus[i] = false" />
</v-menu>
<v-text-field v-model="op.title" :autofocus="true" class="caption" dense outlined />
<v-icon class="ml-2" color="error lighten-2" size="13" @click="removeOption(op, i)"> mdi-close </v-icon>
</div>
<v-btn slot="footer" x-small color="primary" outlined class="d-100 caption mt-2" @click="addNewOption()">
<v-icon x-small outlined color="primary" class="mr-2"> mdi-plus </v-icon>
Add option
</v-btn>
</draggable>
<div v-show="error" class="px-2 py-1 text-left caption error--text">
{{ error }}
</div>
<v-btn x-small color="primary" outlined class="d-100 caption mt-2" @click="localState.push('')">
<v-icon x-small outlined color="primary" class="mr-2"> mdi-plus </v-icon>
Add option
</v-btn>
</v-container>
</template>
<script>
import colors from '@/components/project/spreadsheet/helpers/colors';
import draggable from 'vuedraggable';
import { UITypes } from 'nocodb-sdk';
import ColorPicker from '../ColorPicker.vue';
import { enumColor } from '@/components/project/spreadsheet/helpers/colors';
export default {
name: 'CustomSelectOptions',
props: ['value'],
components: {
draggable,
ColorPicker,
},
props: ['column', 'meta'],
data: () => ({
localState: [],
options: [],
colorMenus: {},
colors: enumColor.light,
error: undefined,
}),
computed: {
colors() {
return this.$store.state.settings.darkTheme ? colors.dark : colors.light;
},
},
watch: {
localState: {
handler(v) {
this.$emit('input', v.map(v => `'${v.replace(/'/g, "\\'")}'`).join(','));
},
deep: true,
alias() {
return this.column?.column_name;
},
value() {
this.syncState();
isEnumOrSet() {
return ['enum', 'set'].includes(this.column.dt);
},
},
mounted() {
this.syncState();
created() {
this.options = this.copyOptions(this.column.colOptions?.options) || [];
// Support for older options
for (const op of this.options.filter(el => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '');
}
},
methods: {
syncState() {
this.localState = (this.value || '').split(',').map(v => v.replace(/\\'/g, "'").replace(/^'|'$/g, ''));
addNewOption() {
const tempOption = {
title: '',
color: this.getNextColor(),
};
this.options.push(tempOption);
},
listenForComma(index, value) {
const normalisedValue = value.trim();
if (normalisedValue.endsWith(',')) {
this.localState.push('');
return;
async removeOption(option, index) {
this.options.splice(index, 1);
},
getNextColor() {
let tempColor = this.colors[0];
if (this.options.length && this.options[this.options.length - 1].color) {
const lastColor = this.colors.indexOf(this.options[this.options.length - 1].color);
tempColor = this.colors[(lastColor + 1) % this.colors.length];
}
this.localState[index] = normalisedValue;
return tempColor;
},
async save() {
try {
if (this.checkOptions()) {
const selectCol = {
...this.column,
title: this.alias,
colOptions: {
options: this.options,
},
};
await this.$api.dbTableColumn.create(this.meta.id, selectCol);
if (this.column.colOptions) {
this.$store.dispatch('meta/ActLoadMeta', { force: true, id: this.column.fk_model_id }).then(() => {});
}
this.$toast.success('Select column saved successfully').goAway(3000);
return true;
}
return false;
} catch (e) {
this.$toast.error(e.message).goAway(3000);
return false;
}
},
async update() {
try {
if (this.checkOptions()) {
const selectCol = {
...this.column,
title: this.alias,
colOptions: {
options: this.options,
},
};
await this.$api.dbTableColumn.update(this.column.id, selectCol);
if (this.column.colOptions) {
this.$store.dispatch('meta/ActLoadMeta', { force: true, id: this.column.fk_model_id }).then(() => {});
}
return true;
}
return false;
} catch (e) {
this.$toast.error(e.message).goAway(3000);
return false;
}
},
copyOptions(array) {
const temp = [];
if (array && array.length) {
for (const el of array) {
temp.push({ ...el });
}
}
return temp;
},
checkOptions() {
let failed = false;
for (const opt of this.options) {
if (!opt.title.length) {
this.error = "Select options can't be null";
failed = true;
break;
}
if (this.column.uidt === UITypes.MultiSelect && opt.title.includes(',')) {
this.error = "MultiSelect columns can't have commas(',')";
failed = true;
break;
}
if (this.options.filter(el => el.title === opt.title).length !== 1) {
this.error = "Select options can't have duplicates";
failed = true;
break;
}
}
if (!failed) {
this.error = null;
return true;
}
return false;
},
},
};
@ -74,4 +168,14 @@ export default {
/deep/ .v-input__control {
height: 33px;
}
.handle {
cursor: pointer;
}
/deep/ .v-text-field__details {
position: absolute;
margin-top: 10px;
margin-left: 25px;
}
</style>

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

@ -1,45 +1,45 @@
<template>
<v-select
v-model="localState"
solo
dense
flat
:items="enumValues"
hide-details
class="mt-0"
:clearable="!column.rqd"
v-on="parentListeners"
>
<template #selection="{ item }">
<div
class="d-100"
:class="{
'text-center': !isForm,
}"
>
<v-chip small :color="colors[enumValues.indexOf(item) % colors.length]" class="ma-1">
{{ item }}
<div>
<v-select
v-model="localState"
:items="enumValues"
:menu-props="{ bottom: true, offsetY: true }"
item-value="title"
solo
dense
flat
hide-details
:class="`mt-0 ${isForm ? 'form-select' : ''}`"
:clearable="!column.rqd"
v-on="parentListeners"
>
<template #selection="{ item }">
<div
class="d-100"
:class="{
'text-center': !isForm,
}"
>
<v-chip small :color="item.color" class="ma-1">
{{ item.title }}
</v-chip>
</div>
</template>
<template #item="{ item }">
<v-chip small :color="item.color">
{{ item.title }}
</v-chip>
</div>
</template>
<template #item="{ item }">
<v-chip small :color="colors[enumValues.indexOf(item) % colors.length]">
{{ item }}
</v-chip>
</template>
<template #append>
<v-icon small class="mt-1"> mdi-menu-down </v-icon>
</template>
</v-select>
</template>
<template #append>
<v-icon small class="mt-1"> mdi-menu-down </v-icon>
</template>
</v-select>
</div>
</template>
<script>
import colors from '@/mixins/colors';
export default {
name: 'EnumListEditableCell',
mixins: [colors],
props: {
value: String,
column: Object,
@ -48,17 +48,18 @@ export default {
computed: {
localState: {
get() {
return this.value && this.value.replace(/\\'/g, "'").replace(/^'|'$/g, '');
return this.value;
},
set(val) {
this.$emit('input', val);
},
},
enumValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map(v => v.replace(/\\'/g, "'").replace(/^'|'$/g, ''));
const opts = this.column.colOptions ? this.column.colOptions.options.filter(el => el.title !== '') || [] : [];
for (const op of opts.filter(el => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '');
}
return [];
return opts;
},
parentListeners() {
const $listeners = {};
@ -100,6 +101,14 @@ export default {
font-size: 13px !important;
}
}
.form-select {
.v-select__selections {
border: 1px solid rgba(127, 130, 139, 0.2);
}
input {
z-index: -1;
}
}
}
</style>
<!--

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

@ -1,8 +1,10 @@
<template>
<div>
<v-combobox
<v-select
v-model="localState"
:items="setValues"
:menu-props="{ bottom: true, offsetY: true }"
item-value="title"
multiple
chips
flat
@ -10,58 +12,56 @@
solo
hide-details
deletable-chips
class="text-center mt-0"
:class="`text-center mt-0 ${isForm ? 'form-select' : ''}`"
>
<template #selection="data">
<v-chip
:key="data.item"
:color="data.item.color"
small
close
close-icon="mdi-close"
class="ma-1"
:color="colors[setValues.indexOf(data.item) % colors.length]"
@click:close="data.parent.selectItem(data.item)"
>
{{ data.item }}
{{ data.item.title }}
</v-chip>
</template>
<template #item="{ item }">
<v-chip small :color="colors[setValues.indexOf(item) % colors.length]">
{{ item }}
<v-chip small :color="item.color">
{{ item.title }}
</v-chip>
</template>
<template #append>
<v-icon small class="mt-2"> mdi-menu-down </v-icon>
<v-icon small class="mt-1"> mdi-menu-down </v-icon>
</template>
</v-combobox>
</v-select>
</div>
</template>
<script>
import colors from '@/mixins/colors';
export default {
name: 'SetListEditableCell',
mixins: [colors],
props: {
value: String,
column: Object,
isForm: Boolean,
},
computed: {
localState: {
get() {
return this.value && this.value.match(/(?:[^',]|\\')+(?='?(?:,|$))/g).map(v => v.replace(/\\'/g, "'"));
return typeof this.value === 'string' ? this.value.split(',') : [];
},
set(val) {
this.$emit('input', val.filter(v => this.setValues.includes(v)).join(','));
this.$emit('input', val.filter(v => this.setValues.find(el => el.title === v)).join(','));
},
},
setValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp
.match(/(?:[^']|\\')+(?='?(?:,|$))/g)
.map(v => v.replace(/\\'/g, "'").replace(/^'|'$/g, ''));
const opts = this.column.colOptions ? this.column.colOptions.options.filter(el => el.title !== '') || [] : [];
for (const op of opts.filter(el => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '');
}
return [];
return opts;
},
parentListeners() {
const $listeners = {};
@ -86,16 +86,35 @@ export default {
};
</script>
<style scoped>
select {
width: 100%;
height: 100%;
color: var(--v-textColor-base);
-webkit-appearance: menulist;
/*webkit browsers */
-moz-appearance: menulist;
/*Firefox */
appearance: menulist;
<style scoped lang="scss">
::v-deep {
.v-select {
min-width: 150px;
.v-select__selections {
min-height: 38px !important;
}
}
.v-input__slot {
padding-right: 0 !important;
}
.v-input__icon.v-input__icon--clear {
width: 15px !important;
.v-icon {
font-size: 13px !important;
}
}
.mdi-close {
font-size: 12px !important;
color: gray !important;
}
.form-select {
.v-select__selections {
border: 1px solid rgba(127, 130, 139, 0.2);
}
input {
z-index: -1;
}
}
}
</style>
<!--

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

@ -254,10 +254,10 @@ export class MysqlUi {
return '';
case 'enum':
return "'a','b'";
return '';
case 'set':
return "'a','b'";
return '';
case 'geometry':
return '';
@ -976,10 +976,10 @@ export class MysqlUi {
colProp.dtxp = 1;
break;
case 'MultiSelect':
colProp.dt = 'text';
colProp.dt = 'set';
break;
case 'SingleSelect':
colProp.dt = 'text';
colProp.dt = 'enum';
break;
case 'Collaborator':
colProp.dt = 'varchar';

8
packages/nocodb/src/lib/db/sql-client/lib/mssql/MssqlClient.ts

@ -2379,13 +2379,13 @@ class MssqlClient extends KnexClient {
return this.schema ? `${this.schema}.${t}` : t;
}
alterTableRemoveColumn(t, n, o, existingQuery) {
alterTableRemoveColumn(t, n, _o, existingQuery) {
const shouldSanitize = true;
let query = existingQuery ? ';' : '';
if (n.cdf) {
query += this.genQuery(
`\nALTER TABLE ?? DROP CONSTRAINT ??;`,
[this.getTnPath(t), o.default_constraint_name || `DF_${t}_${n.cn}`],
[this.getTnPath(t), n.default_constraint_name || `DF_${t}_${n.cn}`],
shouldSanitize
);
}
@ -2625,6 +2625,10 @@ function getDefaultValue(n) {
}
return JSON.stringify(n.cdf);
break;
case 'text':
case 'ntext':
return `'${n.cdf}'`;
break;
default:
return JSON.stringify(n.cdf);
break;

4
packages/nocodb/src/lib/db/sql-client/lib/sqlite/SqliteClient.ts

@ -1965,13 +1965,13 @@ class SqliteClient extends KnexClient {
query = existingQuery ? ',' : '';
query += this.genQuery(`?? ${n.dt}`, [n.cn], shouldSanitize);
query += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : '';
query += n.cdf ? ` DEFAULT ${n.cdf}` : ' ';
query += n.cdf ? (n.cdf.includes(',') ? ` DEFAULT ('${n.cdf}')` : ` DEFAULT ${n.cdf}`) : ' ';
query += n.rqd ? ` NOT NULL` : ' ';
} else if (change === 1) {
shouldSanitize = true;
query += this.genQuery(` ADD ?? ${n.dt}`, [n.cn], shouldSanitize);
query += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : '';
query += n.cdf ? ` DEFAULT ${n.cdf}` : ' ';
query += n.cdf ? (n.cdf.includes(',') ? ` DEFAULT ('${n.cdf}')` : ` DEFAULT ${n.cdf}`) : ' ';
query += n.rqd ? ` NOT NULL` : ' ';
query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize);
} else {

24
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/sortV2.ts

@ -207,6 +207,30 @@ export default async function sortV2(
qb.orderBy(selectQb, sort.direction || 'asc');
}
break;
case UITypes.SingleSelect:
{
const clientType = knex.clientType();
if (clientType === 'mysql' || clientType === 'mysql2') {
qb.orderBy(sanitize(knex.raw('CONCAT(??)', [column.column_name])), sort.direction || 'asc');
} else if (clientType === 'mssql') {
qb.orderBy(sanitize(knex.raw('CAST(?? AS VARCHAR(MAX))', [column.column_name])), sort.direction || 'asc');
} else {
qb.orderBy(sanitize(column.column_name), sort.direction || 'asc');
}
break;
}
case UITypes.MultiSelect:
{
const clientType = knex.clientType();
if (clientType === 'mysql' || clientType === 'mysql2') {
qb.orderBy(sanitize(knex.raw('CONCAT(??)', [column.column_name])), sort.direction || 'asc');
} else if (clientType === 'mssql') {
qb.orderBy(sanitize(knex.raw('CAST(?? AS VARCHAR(MAX))', [column.column_name])), sort.direction || 'asc');
} else {
qb.orderBy(sanitize(column.column_name), sort.direction || 'asc');
}
break;
}
default:
qb.orderBy(sanitize(column.column_name), sort.direction || 'asc');
break;

366
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -509,6 +509,72 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
// Duration column needs more that that
colBody.dtxs = '4';
}
if ([UITypes.SingleSelect, UITypes.MultiSelect].includes(colBody.uidt)) {
const dbDriver = NcConnectionMgrv2.get(base);
const driverType = dbDriver.clientType();
const optionTitles = colBody.colOptions.options.map(el => el.title);
// Handle default values
if (colBody.cdf) {
if (colBody.uidt === UITypes.SingleSelect) {
if (!optionTitles.includes(colBody.cdf)) {
NcError.badRequest(`Default value '${colBody.cdf}' is not a select option.`);
}
} else {
for (const cdf of colBody.cdf.split(',')) {
if (!optionTitles.includes(cdf)) {
NcError.badRequest(`Default value '${cdf}' is not a select option.`);
}
}
}
if (driverType === 'pg') {
colBody.cdf = `'${colBody.cdf}'`;
}
}
// Restrict duplicates
const titles = colBody.colOptions.options.map(el => el.title)
if (titles
.some( function(item) {
return titles.indexOf(item) !== titles.lastIndexOf(item);
})
) {
NcError.badRequest('Duplicates are not allowed!');
}
// Restrict empty options
if (titles
.some( function(item) {
return item === '';
})
) {
NcError.badRequest('Empty options are not allowed!');
}
// Handle empty enum/set for mysql (we restrict empty user options beforehand)
if (driverType === 'mysql' || driverType === 'mysql2') {
if (!colBody.colOptions.options.length && (!colBody.dtxp || colBody.dtxp === '')) {
colBody.colOptions.options.push({ title: '' });
}
}
if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.colOptions.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
: '';
} else if (colBody.uidt === UITypes.MultiSelect){
colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.colOptions.options.map((o) => {
if(o.title.includes(',')) {
NcError.badRequest('Illegal char(\',\') for MultiSelect');
}
return `'${o.title.replace(/'/gi, '\'\'')}'`;
}).join(',')}`
: '';
}
}
const tableUpdateBody = {
...table,
tn: table.table_name,
@ -540,13 +606,6 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
const insertedColumnMeta =
columns.find((c) => c.cn === colBody.column_name) || ({} as any);
if (
colBody.uidt === UITypes.SingleSelect ||
colBody.uidt === UITypes.MultiSelect
) {
insertedColumnMeta.dtxp = colBody.dtxp;
}
await Column.insert({
...colBody,
...insertedColumnMeta,
@ -653,6 +712,299 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
NcError.notImplemented(
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`
);
} else if(
[
UITypes.SingleSelect,
UITypes.MultiSelect
].includes(colBody.uidt)
) {
colBody = getColumnPropsFromUIDT(colBody, base);
if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.colOptions.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
: '';
} else if (colBody.uidt === UITypes.MultiSelect){
colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.colOptions.options.map((o) => {
if(o.title.includes(',')) {
NcError.badRequest('Illegal char(\',\') for MultiSelect');
}
return `'${o.title.replace(/'/gi, '\'\'')}'`;
}).join(',')}`
: '';
}
const baseModel = await Model.getBaseModelSQL({
id: table.id,
dbDriver: NcConnectionMgrv2.get(base)
});
if (column.colOptions?.options) {
const supportedDrivers = ['mysql', 'mysql2', 'pg', 'mssql', 'sqlite3'];
const dbDriver = NcConnectionMgrv2.get(base);
const driverType = dbDriver.clientType();
// MultiSelect to SingleSelect
if (column.uidt === UITypes.MultiSelect && colBody.uidt === UITypes.SingleSelect) {
if (driverType === 'mysql' || driverType === 'mysql2') {
await dbDriver.raw(`UPDATE ?? SET ?? = SUBSTRING_INDEX(??, ',', 1) WHERE ?? LIKE '%,%';`, [table.table_name, column.title, column.title, column.title]);
} else if (driverType === 'pg') {
await dbDriver.raw(`UPDATE ?? SET ?? = split_part(??, ',', 1);`, [table.table_name, column.title, column.title]);
} else if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = LEFT(cast(?? as varchar(max)), CHARINDEX(',', ??) - 1) WHERE CHARINDEX(',', ??) > 0;`, [table.table_name, column.title, column.title, column.title, column.title]);
} else if (driverType === 'sqlite3') {
await dbDriver.raw(`UPDATE ?? SET ?? = substr(??, 1, instr(??, ',') - 1) WHERE ?? LIKE '%,%';`, [table.table_name, column.title, column.title, column.title, column.title]);
}
}
// Handle migrations
for (const op of column.colOptions.options.filter(el => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')
}
// Handle default values
if (colBody.cdf) {
if (driverType === 'mysql' || driverType === 'mysql2') {
} else if (driverType === 'pg') {
} else if (driverType === 'mssql') {
} else if (driverType === 'sqlite3') {
}
}
// Restrict duplicates
const titles = colBody.colOptions.options.map(el => el.title)
if (titles
.some( function(item) {
return titles.indexOf(item) !== titles.lastIndexOf(item);
})
) {
NcError.badRequest('Duplicates are not allowed!');
}
// Restrict empty options
if (titles
.some( function(item) {
return item === '';
})
) {
NcError.badRequest('Empty options are not allowed!');
}
// Handle empty enum/set for mysql (we restrict empty user options beforehand)
if (driverType === 'mysql' || driverType === 'mysql2') {
if (!colBody.colOptions.options.length && (!colBody.dtxp || colBody.dtxp === '')) {
colBody.dtxp = '\'\'';
}
}
// Handle option delete
for (const option of column.colOptions.options.filter(oldOp => colBody.colOptions.options.find(newOp => newOp.id === oldOp.id) ? false : true)) {
if (!supportedDrivers.includes(driverType) && column.uidt === UITypes.MultiSelect) {
NcError.badRequest('Your database not yet supported for this operation. Please remove option from records manually before dropping.');
}
if (column.uidt === UITypes.SingleSelect) {
if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = NULL WHERE ?? LIKE ?`, [table.table_name, column.title, column.title, option.title]);
} else {
await baseModel.bulkUpdateAll({ where: `(${column.title},eq,${option.title})` }, { [column.title]: null });
}
} else if (column.uidt === UITypes.MultiSelect) {
if (driverType === 'mysql' || driverType === 'mysql2') {
await dbDriver.raw(`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ',')) WHERE FIND_IN_SET(?, ??)`, [table.table_name, column.title, column.title, option.title, option.title, column.title]);
} else if (driverType === 'pg') {
await dbDriver.raw(`UPDATE ?? SET ?? = array_to_string(array_remove(string_to_array(??, ','), ?), ',')`, [table.table_name, column.title, column.title, option.title]);
} else if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), ','), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), ',')) - 2)`, [table.table_name, column.title, column.title, option.title, column.title, option.title]);
} else if (driverType === 'sqlite3') {
await dbDriver.raw(`UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ','), ',')`, [table.table_name, column.title, column.title, option.title]);
}
}
}
let interchange = [];
// Handle option update
const old_titles = column.colOptions.options.map(el => el.title);
for (const option of column.colOptions.options.filter(oldOp => colBody.colOptions.options.find(newOp => newOp.id === oldOp.id && newOp.title !== oldOp.title))) {
if (!supportedDrivers.includes(driverType) && column.uidt === UITypes.MultiSelect) {
NcError.badRequest('Your database not yet supported for this operation. Please remove option from records manually before updating.');
}
let newOp = { ...colBody.colOptions.options.find(el => option.id === el.id) };
if (old_titles.includes(newOp.title)) {
let def_option = { ...newOp };
let title_counter = 1;
while (old_titles.includes(newOp.title)) {
newOp.title = `${def_option.title}_${title_counter++}`;
}
interchange.push( {
def_option,
temp_title: newOp.title
} );
}
// Append new option before editing
if ((driverType === 'mysql' || driverType === 'mysql2') && (column.dt === 'enum' || column.dt === 'set')) {
column.colOptions.options.push({ title: newOp.title });
let temp_dtxp = '';
if (column.uidt === UITypes.SingleSelect) {
temp_dtxp = (column.colOptions?.options.length)
? `${column.colOptions.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
: '';
} else if (column.uidt === UITypes.MultiSelect){
temp_dtxp = (column.colOptions?.options.length)
? `${column.colOptions.options.map((o) => {
if(o.title.includes(',')) {
NcError.badRequest('Illegal char(\',\') for MultiSelect');
throw new Error('');
}
return `'${o.title.replace(/'/gi, '\'\'')}'`;
}).join(',')}`
: '';
}
const tableUpdateBody = {
...table,
tn: table.table_name,
originalColumns: table.columns.map((c) => ({
...c,
cn: c.column_name,
cno: c.column_name,
})),
columns: await Promise.all(
table.columns.map(async (c) => {
if (c.id === req.params.columnId) {
const res = {
...c,
...column,
cn: column.column_name,
cno: c.column_name,
dtxp: temp_dtxp,
altered: Altered.UPDATE_COLUMN,
};
return Promise.resolve(res);
} else {
(c as any).cn = c.column_name;
}
return Promise.resolve(c);
})
),
};
const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id });
await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody);
await Column.update(req.params.columnId, {
...column,
});
}
if (column.uidt === UITypes.SingleSelect) {
if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [table.table_name, column.title, newOp.title, column.title, option.title]);
} else {
await baseModel.bulkUpdateAll({ where: `(${column.title},eq,${option.title})` }, { [column.title]: newOp.title });
}
} else if (column.uidt === UITypes.MultiSelect) {
if (driverType === 'mysql' || driverType === 'mysql2') {
await dbDriver.raw(`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??)`, [table.table_name, column.title, column.title, option.title, newOp.title, option.title, column.title]);
} else if (driverType === 'pg') {
await dbDriver.raw(`UPDATE ?? SET ?? = array_to_string(array_replace(string_to_array(??, ','), ?, ?), ',')`, [table.table_name, column.title, column.title, option.title, newOp.title]);
} else if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ',')), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ','))) - 2)`, [table.table_name, column.title, column.title, option.title, newOp.title, column.title, option.title, newOp.title]);
} else if (driverType === 'sqlite3') {
await dbDriver.raw(`UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ',' || ? || ','), ',')`, [table.table_name, column.title, column.title, option.title, newOp.title]);
}
}
}
for (const ch of interchange) {
let newOp = ch.def_option;
if (column.uidt === UITypes.SingleSelect) {
if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [table.table_name, column.title, newOp.title, column.title, ch.temp_title]);
} else {
await baseModel.bulkUpdateAll({ where: `(${column.title},eq,${ch.temp_title})` }, { [column.title]: newOp.title });
}
} else if (column.uidt === UITypes.MultiSelect) {
if (driverType === 'mysql' || driverType === 'mysql2') {
await dbDriver.raw(`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??)`, [table.table_name, column.title, column.title, ch.temp_title, newOp.title, ch.temp_title, column.title]);
} else if (driverType === 'pg') {
await dbDriver.raw(`UPDATE ?? SET ?? = array_to_string(array_replace(string_to_array(??, ','), ?, ?), ',')`, [table.table_name, column.title, column.title, ch.temp_title, newOp.title]);
} else if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ',')), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ','))) - 2)`, [table.table_name, column.title, column.title, ch.temp_title, newOp.title, column.title, ch.temp_title, newOp.title]);
} else if (driverType === 'sqlite3') {
await dbDriver.raw(`UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ',' || ? || ','), ',')`, [table.table_name, column.title, column.title, ch.temp_title, newOp.title]);
}
}
}
}
const tableUpdateBody = {
...table,
tn: table.table_name,
originalColumns: table.columns.map((c) => ({
...c,
cn: c.column_name,
cno: c.column_name,
})),
columns: await Promise.all(
table.columns.map(async (c) => {
if (c.id === req.params.columnId) {
const res = {
...c,
...colBody,
cn: colBody.column_name,
cno: c.column_name,
altered: Altered.UPDATE_COLUMN,
};
// update formula with new column name
if (c.column_name != colBody.column_name) {
const formulas = await Noco.ncMeta
.knex(MetaTable.COL_FORMULA)
.where('formula', 'like', `%${c.id}%`);
if (formulas) {
const new_column = c;
new_column.column_name = colBody.column_name;
new_column.title = colBody.title;
for (const f of formulas) {
// the formula with column IDs only
const formula = f.formula;
// replace column IDs with alias to get the new formula_raw
const new_formula_raw = substituteColumnIdWithAliasInFormula(
formula,
[new_column]
);
await FormulaColumn.update(c.id, {
formula_raw: new_formula_raw,
});
}
}
}
return Promise.resolve(res);
} else {
(c as any).cn = c.column_name;
}
return Promise.resolve(c);
})
),
};
const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id });
await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody);
await Column.update(req.params.columnId, {
...colBody,
});
} else {
colBody = getColumnPropsFromUIDT(colBody, base);
const tableUpdateBody = {

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

@ -15,6 +15,19 @@ import { importData, importLTARData } from './readAndProcessData';
dayjs.extend(utc);
const selectColors = {
"blue": "#cfdfff",
"cyan": "#d0f0fd",
"teal": "#c2f5e9",
"green": "#d1f7c4",
"orange": "#fee2d5",
"yellow": "#ffeab6",
"red": "#ffdce5",
"pink": "#ffdaf6",
"purple": "#ede2fe",
"gray": "#eee"
}
export default async (
syncDB: AirtableSyncConfig,
progress: (data: { msg?: string; level?: any }) => void
@ -383,21 +396,37 @@ export default async (
// prepare options list in CSV format
// note: NC doesn't allow comma's in options
//
const opt = [];
const options = [];
let order = 1;
for (const [, value] of Object.entries(col.typeOptions.choices)) {
opt.push((value as any).name);
// replace commas with dot for multiselect
if (col.type === 'multiSelect') {
(value as any).name = (value as any).name.replace(/,/g, '.');
}
// we don't allow empty records, placeholder instead
if ((value as any).name === '') {
(value as any).name = 'nc_empty';
}
// enumerate duplicates (we don't allow them)
// TODO fix record mapping (this causes every record to map first option, we can't handle them using data api as they don't provide option id within data we might instead get the correct mapping from schema file )
let dupNo = 1;
const defaultName = (value as any).name;
while (options.find(el => el.title === (value as any).name)) {
(value as any).name = `${defaultName}_${dupNo++}`;
}
options.push({
order: order++,
title: (value as any).name,
color: selectColors[(value as any).color] ? selectColors[(value as any).color] : null
});
sMap.addToMappingTbl(
(value as any).id,
undefined,
(value as any).name
);
}
// const csvOpt = "'" + opt.join("','") + "'";
const optSansDuplicate = [...new Set(opt)];
const csvOpt = optSansDuplicate
.map((v) => `'${v.replace(/'/g, "\\'").replace(/,/g, '.')}'`)
.join(',');
return { type: 'select', data: csvOpt };
return { type: col.type, data: options };
}
default:
return { type: undefined };
@ -507,9 +536,12 @@ export default async (
switch (colOptions.type) {
case 'select':
ncCol.dtxp = colOptions.data;
case 'multiSelect':
ncCol.colOptions = {
options: [...colOptions.data]
}
ncCol.dtxp = colOptions.data.map(el => `'${el.title}'`).join(',');
break;
case undefined:
break;
}
@ -1323,11 +1355,19 @@ export default async (
break;
case UITypes.SingleSelect:
rec[key] = value.replace(/,/g, '.');
if (value === '') {
rec[key] = 'nc_empty';
}
rec[key] = value;
break;
case UITypes.MultiSelect:
rec[key] = value.map((v) => `${v.replace(/,/g, '.')}`).join(',');
rec[key] = value.map((v) => {
if (v === '') {
return 'nc_empty';
}
return `${v.replace(/,/g, '.')}`;
}).join(',');
break;
case UITypes.Attachment:

7
packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts

@ -4,8 +4,7 @@ import { RelationTypes, UITypes } from 'nocodb-sdk';
import Model from '../../models/Model';
import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import LookupColumn from '../../models/LookupColumn';
import MultiSelectColumn from '../../models/MultiSelectColumn';
import SingleSelectColumn from '../../models/SingleSelectColumn';
import SelectOption from '../../models/SelectOption';
export default async function populateSamplePayload(
viewOrModel: View | Model,
@ -105,7 +104,7 @@ async function getSampleColumnValue(column: Column): Promise<any> {
break;
case UITypes.MultiSelect:
{
const colOpt = await column.getColOptions<MultiSelectColumn[]>();
const colOpt = await column.getColOptions<SelectOption[]>();
return (
colOpt?.[0]?.title ||
column?.dtxp?.split(',')?.[0]?.replace(/^['"]|['"]$/g, '')
@ -114,7 +113,7 @@ async function getSampleColumnValue(column: Column): Promise<any> {
break;
case UITypes.SingleSelect:
{
const colOpt = await column.getColOptions<SingleSelectColumn[]>();
const colOpt = await column.getColOptions<SelectOption[]>();
return (
colOpt?.[0]?.title ||
column?.dtxp?.split(',')?.[0]?.replace(/^['"]|['"]$/g, '')

78
packages/nocodb/src/lib/models/Column.ts

@ -2,8 +2,7 @@ import FormulaColumn from './FormulaColumn';
import LinkToAnotherRecordColumn from './LinkToAnotherRecordColumn';
import LookupColumn from './LookupColumn';
import RollupColumn from './RollupColumn';
import SingleSelectColumn from './SingleSelectColumn';
import MultiSelectColumn from './MultiSelectColumn';
import SelectOption from './SelectOption';
import Model from './Model';
import NocoCache from '../cache/NocoCache';
import { ColumnType, UITypes } from 'nocodb-sdk';
@ -232,26 +231,58 @@ export default class Column<T = any> implements ColumnType {
break;
}
case UITypes.MultiSelect: {
for (const option of column.dtxp?.split(',') || []) {
await MultiSelectColumn.insert(
{
fk_column_id: colId,
title: option,
},
ncMeta
);
if (!column.colOptions?.options) {
const selectColors = [ '#cfdffe', '#d0f1fd', '#c2f5e8', '#ffdaf6', '#ffdce5', '#fee2d5', '#ffeab6', '#d1f7c4', '#ede2fe', '#eeeeee', ];
for (const [i, option] of column.dtxp?.split(',').entries() || [].entries()) {
await SelectOption.insert(
{
fk_column_id: colId,
title: option.replace(/^'/, '').replace(/'$/, ''),
order: i + 1,
color: selectColors[i % selectColors.length]
},
ncMeta
);
}
} else {
for (const [i, option] of column.colOptions.options.entries() || [].entries()) {
await SelectOption.insert(
{
...option,
fk_column_id: colId,
order: i + 1
},
ncMeta
);
}
}
break;
}
case UITypes.SingleSelect: {
for (const option of column.dtxp?.split(',') || []) {
await SingleSelectColumn.insert(
{
fk_column_id: colId,
title: option,
},
ncMeta
);
if (!column.colOptions?.options) {
const selectColors = [ '#cfdffe', '#d0f1fd', '#c2f5e8', '#ffdaf6', '#ffdce5', '#fee2d5', '#ffeab6', '#d1f7c4', '#ede2fe', '#eeeeee', ];
for (const [i, option] of column.dtxp?.split(',').entries() || [].entries()) {
await SelectOption.insert(
{
fk_column_id: colId,
title: option.replace(/^'/, '').replace(/'$/, ''),
order: i + 1,
color: selectColors[i % selectColors.length]
},
ncMeta
);
}
} else {
for (const [i, option] of column.colOptions.options.entries() || [].entries()) {
await SelectOption.insert(
{
...option,
fk_column_id: colId,
order: i + 1
},
ncMeta
);
}
}
break;
}
@ -322,10 +353,10 @@ export default class Column<T = any> implements ColumnType {
res = await LinkToAnotherRecordColumn.read(this.id, ncMeta);
break;
case UITypes.MultiSelect:
res = await MultiSelectColumn.get(this.id, ncMeta);
res = await SelectOption.read(this.id, ncMeta);
break;
case UITypes.SingleSelect:
res = await SingleSelectColumn.get(this.id, ncMeta);
res = await SelectOption.read(this.id, ncMeta);
break;
case UITypes.Formula:
res = await FormulaColumn.read(this.id, ncMeta);
@ -772,10 +803,11 @@ export default class Column<T = any> implements ColumnType {
await ncMeta.metaDelete(null, null, MetaTable.COL_SELECT_OPTIONS, {
fk_column_id: colId,
});
await NocoCache.deepDel(
CacheScope.COL_SELECT_OPTION,
`${CacheScope.COL_SELECT_OPTION}:${colId}`,
CacheDelDirection.CHILD_TO_PARENT
`${CacheScope.COL_SELECT_OPTION}:${colId}:list`,
CacheDelDirection.PARENT_TO_CHILD
);
break;
}
@ -821,7 +853,7 @@ export default class Column<T = any> implements ColumnType {
o = { ...o, ...updateObj };
// set cache
await NocoCache.set(key, o);
}
}
// set meta
await ncMeta.metaUpdate(
null,

48
packages/nocodb/src/lib/models/MultiSelectColumn.ts → packages/nocodb/src/lib/models/SelectOption.ts

@ -2,26 +2,25 @@ import Noco from '../Noco';
import NocoCache from '../cache/NocoCache';
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals';
export default class MultiSelectColumn {
export default class SelectOption {
title: string;
fk_column_id: string;
color: string;
order: number;
constructor(data: Partial<MultiSelectColumn>) {
constructor(data: Partial<SelectOption>) {
Object.assign(this, data);
}
public static async insert(
data: Partial<MultiSelectColumn>,
data: Partial<SelectOption>,
ncMeta = Noco.ncMeta
) {
const { id } = await ncMeta.metaInsert2(
null,
null,
MetaTable.COL_SELECT_OPTIONS,
{
fk_column_id: data.fk_column_id,
title: data.title,
}
data
);
await NocoCache.appendToList(
@ -36,7 +35,7 @@ export default class MultiSelectColumn {
public static async get(
selectOptionId: string,
ncMeta = Noco.ncMeta
): Promise<MultiSelectColumn> {
): Promise<SelectOption> {
let data =
selectOptionId &&
(await NocoCache.get(
@ -55,33 +54,52 @@ export default class MultiSelectColumn {
data
);
}
return data && new MultiSelectColumn(data);
return data && new SelectOption(data);
}
public static async read(columnId: string, ncMeta = Noco.ncMeta) {
public static async read(fk_column_id: string, ncMeta = Noco.ncMeta) {
let options = await NocoCache.getList(CacheScope.COL_SELECT_OPTION, [
columnId,
fk_column_id
]);
if (!options.length) {
options = await ncMeta.metaList2(
null, //,
null, //model.db_alias,
MetaTable.COL_SELECT_OPTIONS,
{ condition: { fk_column_id: columnId } }
{ condition: { fk_column_id } }
);
await NocoCache.setList(
CacheScope.COL_SELECT_OPTION,
[columnId],
options
[fk_column_id],
options.map(({created_at, updated_at, ...others}) => others)
);
}
return options?.length
? {
options: options.map((c) => new MultiSelectColumn(c)),
options: options.map(({created_at, updated_at, ...c}) => new SelectOption(c))
}
: null;
}
public static async find(
fk_column_id: string,
title: string,
ncMeta = Noco.ncMeta
): Promise<SelectOption> {
let data = await ncMeta.metaGet2(
null,
null,
MetaTable.COL_SELECT_OPTIONS,
{
fk_column_id,
title
}
);
return data && new SelectOption(data);
}
id: string;
}

87
packages/nocodb/src/lib/models/SingleSelectColumn.ts

@ -1,87 +0,0 @@
import Noco from '../Noco';
import NocoCache from '../cache/NocoCache';
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals';
export default class SingleSelectColumn {
title: string;
fk_column_id: string;
constructor(data: Partial<SingleSelectColumn>) {
Object.assign(this, data);
}
public static async insert(
data: Partial<SingleSelectColumn>,
ncMeta = Noco.ncMeta
) {
const { id } = await ncMeta.metaInsert2(
null,
null,
MetaTable.COL_SELECT_OPTIONS,
{
fk_column_id: data.fk_column_id,
title: data.title,
}
);
await NocoCache.appendToList(
CacheScope.COL_SELECT_OPTION,
[data.fk_column_id],
`${CacheScope.COL_SELECT_OPTION}:${id}`
);
return this.get(id, ncMeta);
}
public static async get(
selectOptionId: string,
ncMeta = Noco.ncMeta
): Promise<SingleSelectColumn> {
let data =
selectOptionId &&
(await NocoCache.get(
`${CacheScope.COL_SELECT_OPTION}:${selectOptionId}`,
CacheGetType.TYPE_OBJECT
));
if (!data) {
data = await ncMeta.metaGet2(
null,
null,
MetaTable.COL_SELECT_OPTIONS,
selectOptionId
);
await NocoCache.set(
`${CacheScope.COL_SELECT_OPTION}:${selectOptionId}`,
data
);
}
return data && new SingleSelectColumn(data);
}
public static async read(columnId: string, ncMeta = Noco.ncMeta) {
let options = await NocoCache.getList(CacheScope.COL_SELECT_OPTION, [
columnId,
]);
if (!options.length) {
options = await ncMeta.metaList2(
null, //,
null, //model.db_alias,
MetaTable.COL_SELECT_OPTIONS,
{ condition: { fk_column_id: columnId } }
);
await NocoCache.setList(
CacheScope.COL_SELECT_OPTION,
[columnId],
options
);
}
return options?.length
? {
options: options.map((c) => new SingleSelectColumn(c)),
}
: null;
}
id: string;
}
Loading…
Cancel
Save