Browse Source

Merge pull request #2751 from nocodb/feat/select-column-v2

enhancement: select columns
pull/3037/head
navi 2 years ago committed by GitHub
parent
commit
5302bef7a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue
  2. 36
      packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue
  3. 7
      packages/nc-gui/components/project/spreadsheet/components/EditableCell.vue
  4. 36
      packages/nc-gui/components/project/spreadsheet/components/cell/EnumCell.vue
  5. 16
      packages/nc-gui/components/project/spreadsheet/components/cell/SetListCell.vue
  6. 188
      packages/nc-gui/components/project/spreadsheet/components/editColumn/CustomSelectOptions.vue
  7. 87
      packages/nc-gui/components/project/spreadsheet/components/editableCell/EnumListEditableCell.vue
  8. 77
      packages/nc-gui/components/project/spreadsheet/components/editableCell/SetListEditableCell.vue
  9. 8
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  10. 8
      packages/nocodb/src/lib/db/sql-client/lib/mssql/MssqlClient.ts
  11. 4
      packages/nocodb/src/lib/db/sql-client/lib/sqlite/SqliteClient.ts
  12. 24
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/sortV2.ts
  13. 366
      packages/nocodb/src/lib/meta/api/columnApis.ts
  14. 64
      packages/nocodb/src/lib/meta/api/sync/helpers/job.ts
  15. 7
      packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts
  16. 78
      packages/nocodb/src/lib/models/Column.ts
  17. 48
      packages/nocodb/src/lib/models/SelectOption.ts
  18. 87
      packages/nocodb/src/lib/models/SingleSelectColumn.ts

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

@ -889,7 +889,7 @@ export default {
this.$store.dispatch('meta/ActLoadMeta', { this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env, env: this.nodes.env,
dbAlias: this.nodes.dbAlias, dbAlias: this.nodes.dbAlias,
tn: this.table, table_name: this.table,
force: true, force: true,
}); });
if (this.selectedView && this.selectedView.show_as === 'kanban') { 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" /> <date-options v-model="newColumn.meta" :column="newColumn" :meta="meta" />
</v-col> </v-col>
<v-col v-if="isSelect" cols="12"> <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-col v-else-if="isRating" cols="12"> <v-col v-else-if="isRating" cols="12">
<rating-options v-model="newColumn.meta" :column="newColumn" :meta="meta" /> <rating-options v-model="newColumn.meta" :column="newColumn" :meta="meta" />
@ -399,7 +399,7 @@
<v-textarea <v-textarea
v-model="newColumn.cdf" v-model="newColumn.cdf"
:label="$t('placeholder.defaultValue')" :label="$t('placeholder.defaultValue')"
:hint="sqlUi.getDefaultValueForDatatype(newColumn.dt)" :hint="defaultValueHint"
persistent-hint persistent-hint
rows="3" rows="3"
outlined outlined
@ -586,6 +586,14 @@ export default {
isDate() { isDate() {
return this.newColumn && this.newColumn.uidt === UITypes.Date; 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: { watch: {
column() { column() {
@ -644,10 +652,22 @@ export default {
if (this.newColumn.uidt === 'Formula' && this.$refs.formula) { if (this.newColumn.uidt === 'Formula' && this.$refs.formula) {
return await this.$refs.formula.save(); return await this.$refs.formula.save();
} }
this.newColumn.table_name = this.nodes.table_name; this.newColumn.table_name = this.nodes.table_name;
this.newColumn.title = this.newColumn.column_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) { if (this.editColumn) {
await this.$api.dbTableColumn.update(this.column.id, this.newColumn); await this.$api.dbTableColumn.update(this.column.id, this.newColumn);
} else { } else {
@ -686,11 +706,6 @@ export default {
this.newColumn.dtx = 'specificType'; 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.isCurrency) {
if (this.column?.uidt === UITypes.Currency) { if (this.column?.uidt === UITypes.Currency) {
this.newColumn.dtxp = this.column.dtxp; this.newColumn.dtxp = this.column.dtxp;
@ -722,11 +737,6 @@ export default {
this.newColumn.dtxp = this.sqlUi.getDefaultLengthForDatatype(this.newColumn.dt); this.newColumn.dtxp = this.sqlUi.getDefaultLengthForDatatype(this.newColumn.dt);
this.newColumn.dtxs = this.sqlUi.getDefaultScaleForDatatype(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)) { if (columnToValidate.includes(this.newColumn.uidt)) {
this.newColumn.meta = { this.newColumn.meta = {
validate: this.newColumn.meta && this.newColumn.meta.validate, 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" /> <date-time-picker-cell v-else-if="isDateTime" v-model="localState" ignore-focus v-on="parentListeners" />
<enum-cell <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" v-model="localState"
:column="column" :column="column"
v-on="parentListeners" v-on="parentListeners"
@ -80,9 +82,10 @@
/> />
<set-list-editable-cell <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" v-model="localState"
:column="column" :column="column"
:is-form="isForm"
v-on="parentListeners" v-on="parentListeners"
/> />
<set-list-cell v-else-if="isSet" v-model="localState" :column="column" 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> <template>
<div> <div>
<span <v-chip
v-for="v in [(value || '').replace(/\\'/g, '\'').replace(/^'|'$/g, '')]" v-if="enumValues.find(el => el.title === value)"
:key="v" :color="enumValues.find(el => el.title === value) ? enumValues.find(el => el.title === value).color : ''"
:style="{ small
background: colors[v],
}"
class="set-item ma-1 py-1 px-3" class="set-item ma-1 py-1 px-3"
>{{ v }}</span
> >
{{ enumValues.find(el => el.title === value).title }}
</v-chip>
</div> </div>
</template> </template>
<script> <script>
import { enumColor as colors } from '@/components/project/spreadsheet/helpers/colors';
export default { export default {
name: 'EnumCell', name: 'EnumCell',
props: ['value', 'column'], props: ['value', 'column'],
computed: { computed: {
colors() { enumValues() {
const col = this.$store.state.settings.darkTheme ? colors.dark : colors.light; const opts = this.column.colOptions ? this.column.colOptions.options.filter(el => el.title !== '') || [] : [];
if (this.column && this.column.dtxp) { for (const op of opts.filter(el => el.order === null)) {
return this.column.dtxp op.title = op.title.replace(/^'/, '').replace(/'$/, '');
.split(',')
.map(v => v.replace(/\\'/g, "'").replace(/^'|'$/g, ''))
.reduce(
(obj, v, i) => ({
...obj,
[v]: col[i],
}),
{}
);
} }
return {}; return opts;
}, },
}, },
}; };
</script> </script>
<style scoped> <style scoped>
/*
.set-item { .set-item {
display: inline-block; display: inline-block;
border-radius: 25px; border-radius: 25px;
white-space: nowrap; white-space: nowrap;
} }
*/
</style> </style>
<!-- <!--
/** /**

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

@ -2,10 +2,10 @@
<div> <div>
<v-chip <v-chip
v-for="v in selectedValues" v-for="v in selectedValues"
v-show="v || setValues.includes(v)" v-show="v && setValues.find(el => el.title === v)"
:key="v" :key="v"
small 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" class="set-item ma-1 py-1 px-3"
> >
{{ v }} {{ v }}
@ -14,21 +14,19 @@
</template> </template>
<script> <script>
import colors from '@/mixins/colors';
export default { export default {
name: 'SetListCell', name: 'SetListCell',
mixins: [colors],
props: ['value', 'column'], props: ['value', 'column'],
computed: { computed: {
setValues() { setValues() {
if (this.column && this.column.dtxp) { const opts = this.column.colOptions ? this.column.colOptions.options.filter(el => el.title !== '') || [] : [];
return this.column.dtxp.split(',').map(v => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')); for (const op of opts.filter(el => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '');
} }
return []; return opts;
}, },
selectedValues() { 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> <template>
<v-container fluid class="wrapper"> <v-container fluid class="wrapper">
<div v-for="(op, i) in localState" :key="i" class="d-flex py-1"> <draggable v-model="options" handle=".nc-child-draggable-icon">
<v-icon :color="colors[i % colors.length]" class="mr-2" @click="localState.splice(i, 1)"> <div v-for="(op, i) in options" :key="`${op.color}-${i}`" class="d-flex py-1">
mdi-arrow-down-drop-circle <v-icon small class="nc-child-draggable-icon handle"> mdi-drag-vertical </v-icon>
</v-icon> <v-menu v-model="colorMenus[i]" rounded="lg" :close-on-content-click="false" offset-y>
<v-text-field <template #activator="{ on }">
v-model="localState[i]" <v-icon :color="op.color" class="mr-2" v-on="on"> mdi-arrow-down-drop-circle </v-icon>
:autofocus="true" </template>
class="caption" <color-picker v-model="op.color" @input="colorMenus[i] = false" />
dense </v-menu>
outlined <v-text-field v-model="op.title" :autofocus="true" class="caption" dense outlined />
@input="listenForComma(i, $event)" <v-icon class="ml-2" color="error lighten-2" size="13" @click="removeOption(op, i)"> mdi-close </v-icon>
/> </div>
<v-icon class="ml-2" color="error lighten-2" size="13" @click="localState.splice(i, 1)"> mdi-close </v-icon> <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> </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> </v-container>
</template> </template>
<script> <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 { export default {
name: 'CustomSelectOptions', name: 'CustomSelectOptions',
props: ['value'], components: {
draggable,
ColorPicker,
},
props: ['column', 'meta'],
data: () => ({ data: () => ({
localState: [], options: [],
colorMenus: {},
colors: enumColor.light,
error: undefined,
}), }),
computed: { computed: {
colors() { alias() {
return this.$store.state.settings.darkTheme ? colors.dark : colors.light; return this.column?.column_name;
},
},
watch: {
localState: {
handler(v) {
this.$emit('input', v.map(v => `'${v.replace(/'/g, "\\'")}'`).join(','));
},
deep: true,
}, },
value() { isEnumOrSet() {
this.syncState(); return ['enum', 'set'].includes(this.column.dt);
}, },
}, },
mounted() { created() {
this.syncState(); 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: { methods: {
syncState() { addNewOption() {
this.localState = (this.value || '').split(',').map(v => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')); const tempOption = {
title: '',
color: this.getNextColor(),
};
this.options.push(tempOption);
}, },
listenForComma(index, value) { async removeOption(option, index) {
const normalisedValue = value.trim(); this.options.splice(index, 1);
if (normalisedValue.endsWith(',')) { },
this.localState.push(''); getNextColor() {
return; 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 { /deep/ .v-input__control {
height: 33px; height: 33px;
} }
.handle {
cursor: pointer;
}
/deep/ .v-text-field__details {
position: absolute;
margin-top: 10px;
margin-left: 25px;
}
</style> </style>

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

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

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

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

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

@ -254,10 +254,10 @@ export class MysqlUi {
return ''; return '';
case 'enum': case 'enum':
return "'a','b'"; return '';
case 'set': case 'set':
return "'a','b'"; return '';
case 'geometry': case 'geometry':
return ''; return '';
@ -976,10 +976,10 @@ export class MysqlUi {
colProp.dtxp = 1; colProp.dtxp = 1;
break; break;
case 'MultiSelect': case 'MultiSelect':
colProp.dt = 'text'; colProp.dt = 'set';
break; break;
case 'SingleSelect': case 'SingleSelect':
colProp.dt = 'text'; colProp.dt = 'enum';
break; break;
case 'Collaborator': case 'Collaborator':
colProp.dt = 'varchar'; 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; return this.schema ? `${this.schema}.${t}` : t;
} }
alterTableRemoveColumn(t, n, o, existingQuery) { alterTableRemoveColumn(t, n, _o, existingQuery) {
const shouldSanitize = true; const shouldSanitize = true;
let query = existingQuery ? ';' : ''; let query = existingQuery ? ';' : '';
if (n.cdf) { if (n.cdf) {
query += this.genQuery( query += this.genQuery(
`\nALTER TABLE ?? DROP CONSTRAINT ??;`, `\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 shouldSanitize
); );
} }
@ -2625,6 +2625,10 @@ function getDefaultValue(n) {
} }
return JSON.stringify(n.cdf); return JSON.stringify(n.cdf);
break; break;
case 'text':
case 'ntext':
return `'${n.cdf}'`;
break;
default: default:
return JSON.stringify(n.cdf); return JSON.stringify(n.cdf);
break; break;

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

@ -1965,13 +1965,13 @@ class SqliteClient extends KnexClient {
query = existingQuery ? ',' : ''; query = existingQuery ? ',' : '';
query += this.genQuery(`?? ${n.dt}`, [n.cn], shouldSanitize); query += this.genQuery(`?? ${n.dt}`, [n.cn], shouldSanitize);
query += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : ''; 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 += n.rqd ? ` NOT NULL` : ' ';
} else if (change === 1) { } else if (change === 1) {
shouldSanitize = true; shouldSanitize = true;
query += this.genQuery(` ADD ?? ${n.dt}`, [n.cn], shouldSanitize); query += this.genQuery(` ADD ?? ${n.dt}`, [n.cn], shouldSanitize);
query += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : ''; 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 += n.rqd ? ` NOT NULL` : ' ';
query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize); query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize);
} else { } 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'); qb.orderBy(selectQb, sort.direction || 'asc');
} }
break; 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: default:
qb.orderBy(sanitize(column.column_name), sort.direction || 'asc'); qb.orderBy(sanitize(column.column_name), sort.direction || 'asc');
break; 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 // Duration column needs more that that
colBody.dtxs = '4'; 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 = { const tableUpdateBody = {
...table, ...table,
tn: table.table_name, tn: table.table_name,
@ -540,13 +606,6 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
const insertedColumnMeta = const insertedColumnMeta =
columns.find((c) => c.cn === colBody.column_name) || ({} as any); 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({ await Column.insert({
...colBody, ...colBody,
...insertedColumnMeta, ...insertedColumnMeta,
@ -653,6 +712,299 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
NcError.notImplemented( NcError.notImplemented(
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented` `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 { } else {
colBody = getColumnPropsFromUIDT(colBody, base); colBody = getColumnPropsFromUIDT(colBody, base);
const tableUpdateBody = { 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); 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 ( export default async (
syncDB: AirtableSyncConfig, syncDB: AirtableSyncConfig,
progress: (data: { msg?: string; level?: any }) => void progress: (data: { msg?: string; level?: any }) => void
@ -383,21 +396,37 @@ export default async (
// prepare options list in CSV format // prepare options list in CSV format
// note: NC doesn't allow comma's in options // 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)) { 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( sMap.addToMappingTbl(
(value as any).id, (value as any).id,
undefined, undefined,
(value as any).name (value as any).name
); );
} }
// const csvOpt = "'" + opt.join("','") + "'"; return { type: col.type, data: options };
const optSansDuplicate = [...new Set(opt)];
const csvOpt = optSansDuplicate
.map((v) => `'${v.replace(/'/g, "\\'").replace(/,/g, '.')}'`)
.join(',');
return { type: 'select', data: csvOpt };
} }
default: default:
return { type: undefined }; return { type: undefined };
@ -507,9 +536,12 @@ export default async (
switch (colOptions.type) { switch (colOptions.type) {
case 'select': case 'select':
ncCol.dtxp = colOptions.data; case 'multiSelect':
ncCol.colOptions = {
options: [...colOptions.data]
}
ncCol.dtxp = colOptions.data.map(el => `'${el.title}'`).join(',');
break; break;
case undefined: case undefined:
break; break;
} }
@ -1323,11 +1355,19 @@ export default async (
break; break;
case UITypes.SingleSelect: case UITypes.SingleSelect:
rec[key] = value.replace(/,/g, '.'); if (value === '') {
rec[key] = 'nc_empty';
}
rec[key] = value;
break; break;
case UITypes.MultiSelect: 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; break;
case UITypes.Attachment: 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 Model from '../../models/Model';
import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import LookupColumn from '../../models/LookupColumn'; import LookupColumn from '../../models/LookupColumn';
import MultiSelectColumn from '../../models/MultiSelectColumn'; import SelectOption from '../../models/SelectOption';
import SingleSelectColumn from '../../models/SingleSelectColumn';
export default async function populateSamplePayload( export default async function populateSamplePayload(
viewOrModel: View | Model, viewOrModel: View | Model,
@ -105,7 +104,7 @@ async function getSampleColumnValue(column: Column): Promise<any> {
break; break;
case UITypes.MultiSelect: case UITypes.MultiSelect:
{ {
const colOpt = await column.getColOptions<MultiSelectColumn[]>(); const colOpt = await column.getColOptions<SelectOption[]>();
return ( return (
colOpt?.[0]?.title || colOpt?.[0]?.title ||
column?.dtxp?.split(',')?.[0]?.replace(/^['"]|['"]$/g, '') column?.dtxp?.split(',')?.[0]?.replace(/^['"]|['"]$/g, '')
@ -114,7 +113,7 @@ async function getSampleColumnValue(column: Column): Promise<any> {
break; break;
case UITypes.SingleSelect: case UITypes.SingleSelect:
{ {
const colOpt = await column.getColOptions<SingleSelectColumn[]>(); const colOpt = await column.getColOptions<SelectOption[]>();
return ( return (
colOpt?.[0]?.title || colOpt?.[0]?.title ||
column?.dtxp?.split(',')?.[0]?.replace(/^['"]|['"]$/g, '') 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 LinkToAnotherRecordColumn from './LinkToAnotherRecordColumn';
import LookupColumn from './LookupColumn'; import LookupColumn from './LookupColumn';
import RollupColumn from './RollupColumn'; import RollupColumn from './RollupColumn';
import SingleSelectColumn from './SingleSelectColumn'; import SelectOption from './SelectOption';
import MultiSelectColumn from './MultiSelectColumn';
import Model from './Model'; import Model from './Model';
import NocoCache from '../cache/NocoCache'; import NocoCache from '../cache/NocoCache';
import { ColumnType, UITypes } from 'nocodb-sdk'; import { ColumnType, UITypes } from 'nocodb-sdk';
@ -232,26 +231,58 @@ export default class Column<T = any> implements ColumnType {
break; break;
} }
case UITypes.MultiSelect: { case UITypes.MultiSelect: {
for (const option of column.dtxp?.split(',') || []) { if (!column.colOptions?.options) {
await MultiSelectColumn.insert( const selectColors = [ '#cfdffe', '#d0f1fd', '#c2f5e8', '#ffdaf6', '#ffdce5', '#fee2d5', '#ffeab6', '#d1f7c4', '#ede2fe', '#eeeeee', ];
{ for (const [i, option] of column.dtxp?.split(',').entries() || [].entries()) {
fk_column_id: colId, await SelectOption.insert(
title: option, {
}, fk_column_id: colId,
ncMeta 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; break;
} }
case UITypes.SingleSelect: { case UITypes.SingleSelect: {
for (const option of column.dtxp?.split(',') || []) { if (!column.colOptions?.options) {
await SingleSelectColumn.insert( const selectColors = [ '#cfdffe', '#d0f1fd', '#c2f5e8', '#ffdaf6', '#ffdce5', '#fee2d5', '#ffeab6', '#d1f7c4', '#ede2fe', '#eeeeee', ];
{ for (const [i, option] of column.dtxp?.split(',').entries() || [].entries()) {
fk_column_id: colId, await SelectOption.insert(
title: option, {
}, fk_column_id: colId,
ncMeta 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; break;
} }
@ -322,10 +353,10 @@ export default class Column<T = any> implements ColumnType {
res = await LinkToAnotherRecordColumn.read(this.id, ncMeta); res = await LinkToAnotherRecordColumn.read(this.id, ncMeta);
break; break;
case UITypes.MultiSelect: case UITypes.MultiSelect:
res = await MultiSelectColumn.get(this.id, ncMeta); res = await SelectOption.read(this.id, ncMeta);
break; break;
case UITypes.SingleSelect: case UITypes.SingleSelect:
res = await SingleSelectColumn.get(this.id, ncMeta); res = await SelectOption.read(this.id, ncMeta);
break; break;
case UITypes.Formula: case UITypes.Formula:
res = await FormulaColumn.read(this.id, ncMeta); 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, { await ncMeta.metaDelete(null, null, MetaTable.COL_SELECT_OPTIONS, {
fk_column_id: colId, fk_column_id: colId,
}); });
await NocoCache.deepDel( await NocoCache.deepDel(
CacheScope.COL_SELECT_OPTION, CacheScope.COL_SELECT_OPTION,
`${CacheScope.COL_SELECT_OPTION}:${colId}`, `${CacheScope.COL_SELECT_OPTION}:${colId}:list`,
CacheDelDirection.CHILD_TO_PARENT CacheDelDirection.PARENT_TO_CHILD
); );
break; break;
} }
@ -821,7 +853,7 @@ export default class Column<T = any> implements ColumnType {
o = { ...o, ...updateObj }; o = { ...o, ...updateObj };
// set cache // set cache
await NocoCache.set(key, o); await NocoCache.set(key, o);
} }
// set meta // set meta
await ncMeta.metaUpdate( await ncMeta.metaUpdate(
null, 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 NocoCache from '../cache/NocoCache';
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals'; import { CacheGetType, CacheScope, MetaTable } from '../utils/globals';
export default class MultiSelectColumn { export default class SelectOption {
title: string; title: string;
fk_column_id: string; fk_column_id: string;
color: string;
order: number;
constructor(data: Partial<MultiSelectColumn>) { constructor(data: Partial<SelectOption>) {
Object.assign(this, data); Object.assign(this, data);
} }
public static async insert( public static async insert(
data: Partial<MultiSelectColumn>, data: Partial<SelectOption>,
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
) { ) {
const { id } = await ncMeta.metaInsert2( const { id } = await ncMeta.metaInsert2(
null, null,
null, null,
MetaTable.COL_SELECT_OPTIONS, MetaTable.COL_SELECT_OPTIONS,
{ data
fk_column_id: data.fk_column_id,
title: data.title,
}
); );
await NocoCache.appendToList( await NocoCache.appendToList(
@ -36,7 +35,7 @@ export default class MultiSelectColumn {
public static async get( public static async get(
selectOptionId: string, selectOptionId: string,
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<MultiSelectColumn> { ): Promise<SelectOption> {
let data = let data =
selectOptionId && selectOptionId &&
(await NocoCache.get( (await NocoCache.get(
@ -55,33 +54,52 @@ export default class MultiSelectColumn {
data 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, [ let options = await NocoCache.getList(CacheScope.COL_SELECT_OPTION, [
columnId, fk_column_id
]); ]);
if (!options.length) { if (!options.length) {
options = await ncMeta.metaList2( options = await ncMeta.metaList2(
null, //, null, //,
null, //model.db_alias, null, //model.db_alias,
MetaTable.COL_SELECT_OPTIONS, MetaTable.COL_SELECT_OPTIONS,
{ condition: { fk_column_id: columnId } } { condition: { fk_column_id } }
); );
await NocoCache.setList( await NocoCache.setList(
CacheScope.COL_SELECT_OPTION, CacheScope.COL_SELECT_OPTION,
[columnId], [fk_column_id],
options options.map(({created_at, updated_at, ...others}) => others)
); );
} }
return options?.length return options?.length
? { ? {
options: options.map((c) => new MultiSelectColumn(c)), options: options.map(({created_at, updated_at, ...c}) => new SelectOption(c))
} }
: null; : 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; 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