Browse Source

feat: Select column/model changes

Signed-off-by: mertmit <mertmit99@gmail.com>
feat/select-column
mertmit 3 years ago
parent
commit
232686a11d
  1. 10
      packages/nc-gui/components/project/spreadsheet/components/Cell.vue
  2. 35
      packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue
  3. 24
      packages/nc-gui/components/project/spreadsheet/components/EditableCell.vue
  4. 74
      packages/nc-gui/components/project/spreadsheet/components/cell/MultiSelectCell.vue
  5. 59
      packages/nc-gui/components/project/spreadsheet/components/cell/SingleSelectCell.vue
  6. 159
      packages/nc-gui/components/project/spreadsheet/components/editColumn/CustomSelectOptions.vue
  7. 96
      packages/nc-gui/components/project/spreadsheet/components/editableCell/MultiSelectEditableCell.vue
  8. 53
      packages/nc-gui/components/project/spreadsheet/components/editableCell/SingleSelectEditableCell.vue
  9. 6
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  10. 9
      packages/nocodb/src/lib/meta/api/columnApis.ts
  11. 7
      packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts
  12. 84
      packages/nocodb/src/lib/models/Column.ts
  13. 17
      packages/nocodb/src/lib/models/Model.ts
  14. 48
      packages/nocodb/src/lib/models/SelectOption.ts
  15. 87
      packages/nocodb/src/lib/models/SingleSelectColumn.ts

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

@ -7,9 +7,9 @@
:column="column"
@click.stop="$emit('enableedit')"
/>
<set-list-cell v-else-if="isSet" :value="value" :column="column" @click.stop="$emit('enableedit')" />
<multi-select-cell v-else-if="isSet" :value="value" :column="column" @click.stop="$emit('enableedit')" />
<!-- <enum-list-editable-cell @click.stop="$emit('enableedit')" v-else-if="isEnum && selected" :value="value" :column="column"></enum-list-editable-cell>-->
<enum-cell v-else-if="isEnum" :value="value" :column="column" @click.stop="$emit('enableedit')" />
<single-select-cell v-else-if="isEnum" :value="value" :column="column" @click.stop="$emit('enableedit')" />
<url-cell v-else-if="isURL" :value="value" />
<email-cell v-else-if="isEmail" :value="value" />
<json-cell v-else-if="isJSON" :value="value" />
@ -30,8 +30,8 @@ import TimeCell from './cell/TimeCell'
import JsonCell from '~/components/project/spreadsheet/components/cell/JsonCell'
import UrlCell from '~/components/project/spreadsheet/components/cell/UrlCell'
import cell from '@/components/project/spreadsheet/mixins/cell'
import SetListCell from '~/components/project/spreadsheet/components/cell/SetListCell'
import EnumCell from '~/components/project/spreadsheet/components/cell/EnumCell'
import MultiSelectCell from '~/components/project/spreadsheet/components/cell/MultiSelectCell'
import SingleSelectCell from '~/components/project/spreadsheet/components/cell/SingleSelectCell'
import EditableAttachmentCell from '~/components/project/spreadsheet/components/editableCell/EditableAttachmentCell'
import BooleanCell from '~/components/project/spreadsheet/components/cell/BooleanCell'
import EmailCell from '~/components/project/spreadsheet/components/cell/EmailCell'
@ -40,7 +40,7 @@ import CurrencyCell from '@/components/project/spreadsheet/components/cell/Curre
export default {
name: 'TableCell',
components: { RatingCell, EmailCell, TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell, BooleanCell, CurrencyCell },
components: { RatingCell, EmailCell, TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, SingleSelectCell, MultiSelectCell, BooleanCell, CurrencyCell },
mixins: [cell],
props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'],
computed: {

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

@ -148,8 +148,10 @@
<v-col v-if="isSelect" cols="12">
<custom-select-options
v-model="newColumn.dtxp"
@input="newColumn.altered = newColumn.altered || 2"
ref="customselect"
:column="newColumn"
:meta="meta"
v-on="$listeners"
/>
</v-col>
<v-col v-else-if="isRating" cols="12">
@ -746,10 +748,19 @@ 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) {
await this.$refs.customselect.update()
} else {
await this.$refs.customselect.save()
}
await this.$emit('saved')
return this.$emit('close')
}
if (this.editColumn) {
await this.$api.dbTableColumn.update(this.column.id, this.newColumn)
} else {
@ -798,15 +809,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
@ -842,15 +844,6 @@ export default {
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

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

@ -74,13 +74,13 @@
v-on="parentListeners"
/>
<enum-cell
<single-select-cell
v-else-if="isEnum && (( !isForm && !active) || isLocked || (isPublic && !isForm))"
v-model="localState"
:column="column"
v-on="parentListeners"
/>
<enum-list-cell
<single-select-editable-cell
v-else-if="isEnum"
v-model="localState"
:is-form="isForm"
@ -97,14 +97,14 @@
@input="$emit('save')"
/>
<set-list-editable-cell
<multi-select-editable-cell
v-else-if="isSet && (active || isForm) && !isLocked && !(isPublic && !isForm)"
v-model="localState"
:column="column"
v-on="parentListeners"
@input="$emit('save')"
/>
<set-list-cell
<multi-select-cell
v-else-if="isSet"
v-model="localState"
:column="column"
@ -137,16 +137,16 @@ import JsonEditableCell from '~/components/project/spreadsheet/components/editab
import TextCell from '~/components/project/spreadsheet/components/editableCell/TextCell'
import DateTimePickerCell from '~/components/project/spreadsheet/components/editableCell/DateTimePickerCell'
import TextAreaCell from '~/components/project/spreadsheet/components/editableCell/TextAreaCell'
import EnumListCell from '~/components/project/spreadsheet/components/editableCell/EnumListEditableCell'
import SingleSelectEditableCell from '~/components/project/spreadsheet/components/editableCell/SingleSelectEditableCell'
import IntegerCell from '~/components/project/spreadsheet/components/editableCell/IntegerCell'
import FloatCell from '~/components/project/spreadsheet/components/editableCell/FloatCell'
import TimePickerCell from '~/components/project/spreadsheet/components/editableCell/TimePickerCell'
import BooleanCell from '~/components/project/spreadsheet/components/editableCell/BooleanCell'
import cell from '@/components/project/spreadsheet/mixins/cell'
import EditableAttachmentCell from '~/components/project/spreadsheet/components/editableCell/EditableAttachmentCell'
import EnumCell from '~/components/project/spreadsheet/components/cell/EnumCell'
import SetListEditableCell from '~/components/project/spreadsheet/components/editableCell/SetListEditableCell'
import SetListCell from '~/components/project/spreadsheet/components/cell/SetListCell'
import SingleSelectCell from '~/components/project/spreadsheet/components/cell/SingleSelectCell'
import MultiSelectEditableCell from '~/components/project/spreadsheet/components/editableCell/MultiSelectEditableCell'
import MultiSelectCell from '~/components/project/spreadsheet/components/cell/MultiSelectCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
export default {
@ -155,15 +155,15 @@ export default {
RatingCell,
JsonEditableCell,
EditableUrlCell,
SetListCell,
SetListEditableCell,
EnumCell,
MultiSelectCell,
MultiSelectEditableCell,
SingleSelectCell,
EditableAttachmentCell,
BooleanCell,
TimePickerCell,
FloatCell,
IntegerCell,
EnumListCell,
SingleSelectEditableCell,
TextAreaCell,
DateTimePickerCell,
TextCell,

74
packages/nc-gui/components/project/spreadsheet/components/cell/MultiSelectCell.vue

@ -0,0 +1,74 @@
<template>
<div>
<span
v-for="op in selected"
:key="op.id"
:style="{
background: op ? op.color || '#ccc' : '#ccc'
}"
class="set-item ma-1 py-1 px-3"
>{{ op ? migrate(op.title) || op : '' }}</span>
</div>
</template>
<script>
export default {
name: 'MultiSelectCell',
props: ['value', 'column'],
computed: {
selected() {
if (this.column && this.value) {
return this.values.map(el => this.column.colOptions?.options.find(opt => el === opt.id ||
el === this.migrate(opt.title)) || el)
}
return []
},
values() {
return this.value ? this.value.split(',') : []
}
},
methods: {
migrate(val) {
if (this.column.dt === 'set') {
if ((typeof val === 'string' || val instanceof String)) {
return val.replace(/'/g, '')
}
}
return val
}
}
}
</script>
<style scoped>
.set-item {
display: inline-block;
border-radius: 25px;
white-space: nowrap;
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

59
packages/nc-gui/components/project/spreadsheet/components/cell/SingleSelectCell.vue

@ -0,0 +1,59 @@
<template>
<div>
<span
v-if="value"
:style="{
background: selected ? selected.color || '#ccc' : '#ccc'
}"
class="set-item ma-1 py-1 px-3"
>{{ selected ? selected.title : value }}</span>
</div>
</template>
<script>
export default {
name: 'SingleSelectCell',
props: ['value', 'column'],
computed: {
selected() {
if (this.column && this.column.colOptions) {
return this.column.colOptions?.options.find(el => el.id === this.value || el.title === this.value)
}
return null
}
}
}
</script>
<style scoped>
.set-item {
display: inline-block;
border-radius: 25px;
white-space: nowrap;
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

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

@ -1,80 +1,168 @@
<template>
<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">
<div v-for="(op,i) in options" :key="`${op.color}-${i}`" class="d-flex py-1">
<v-icon
:color="colors[i % colors.length]"
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"
@click="localState.splice(i,1)"
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="localState[i]"
v-model="op.title"
:autofocus="true"
class="caption"
:rules="[enumNotNull, enumNoDuplicate]"
dense
outlined
@input="listenForComma(i, $event)"
:disabled="op.id && isMigrated"
/>
<v-icon class="ml-2" color="error lighten-2" size="13" @click="localState.splice(i,1)">
<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="localState.push('')"
@click="addNewOption()"
>
<v-icon x-small outlined color="primary" class="mr-2">
mdi-plus
</v-icon>
Add option
</v-btn>
</draggable>
</v-container>
</template>
<script>
import colors from '@/components/project/spreadsheet/helpers/colors'
import draggable from 'vuedraggable'
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
}),
computed: {
colors() {
return this.$store.state.settings.darkTheme ? colors.dark : colors.light
alias() {
return this.column?.column_name
},
isMigrated() {
return ['enum', 'set'].includes(this.column.dt)
}
},
created() {
this.options = this.copyOptions(this.column.colOptions?.options) || []
// migrate
if (this.isMigrated && this.options.length) {
this.options.map((el) => {
el.title = el.title.replace(/'/g, '')
return el
})
}
},
methods: {
addNewOption() {
const tempOption = {
title: '',
color: this.getNextColor()
}
this.options.push(tempOption)
},
watch: {
localState: {
handler(v) {
this.$emit('input', v.map(v => `'${v.replace(/'/g, '\\\'')}'`).join(','))
async removeOption(option, index) {
this.options.splice(index, 1)
},
deep: true
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]
}
return tempColor
},
value() {
this.syncState()
async save() {
try {
const selectCol = {
...this.column,
title: this.alias,
options: this.options
}
await this.$api.dbTableColumn.create(this.meta.id, selectCol)
this.$toast.success('Select column saved successfully').goAway(3000)
return this.$emit('saved', this.alias)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
},
mounted() {
this.syncState()
async update() {
try {
const selectCol = {
...this.column,
title: this.alias,
options: this.options
}
if (this.isMigrated) {
const enums = this.options.map(el => el.title)
selectCol.dtxp = `'${enums.join("','")}'`
selectCol.ct = `${this.column.dt}(${selectCol.dtxp})`
}
await this.$api.dbTableColumn.update(this.column.id, selectCol)
return this.$emit('saved', this.alias)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
},
methods: {
syncState() {
this.localState = (this.value || '').split(',').map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, ''))
copyOptions(array) {
const temp = []
if (array && array.length) {
for (const el of array) {
temp.push({ ...el })
}
}
return temp
},
enumNotNull(v) {
if (this.isMigrated) {
return !!v || 'Migrated options can\'t be null'
}
return true
},
listenForComma(index, value) {
const normalisedValue = value.trim()
if (normalisedValue.endsWith(',')) {
this.localState.push('')
return
enumNoDuplicate(v) {
if (this.isMigrated) {
return this.options.filter(el => el.title === v).length === 1 || 'Migrated options can\'t have duplicates'
}
this.localState[index] = normalisedValue
return true
}
}
}
@ -90,4 +178,13 @@ export default {
height: 33px;
}
.handle {
cursor: pointer;
}
/deep/ .v-text-field__details {
position: absolute;
margin-top: 10px;
margin-left: 25px;
}
</style>

96
packages/nc-gui/components/project/spreadsheet/components/editableCell/SetListEditableCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/MultiSelectEditableCell.vue

@ -1,8 +1,10 @@
<template>
<div>
<v-combobox
<v-select
v-model="localState"
:items="setValues"
:menu-props="{ bottom: true, offsetY: true }"
:items="setValues.concat(unsetValues)"
item-value="id"
multiple
chips
flat
@ -14,36 +16,33 @@
>
<template #selection="data">
<v-chip
:key="data.item"
:key="data.item.id"
small
class="ma-1 "
:color="colors[setValues.indexOf(data.item) % colors.length]"
class="ma-1"
:color="data.item.color"
@click:close="data.parent.selectItem(data.item)"
>
{{ data.item }}
{{ migrate(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">
{{ migrate(item.title) }}
</v-chip>
</template>
<template #append>
<v-icon small class="mt-2">
<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],
name: 'MultiSelectEditableCell',
props: {
value: String,
column: Object
@ -51,22 +50,37 @@ export default {
computed: {
localState: {
get() {
return this.value && this.value
.match(/(?:[^',]|\\')+(?='?(?:,|$))/g)
.map(v => v.replace(/\\'/g, '\''))
return (this.column.dt === 'set')
? this.values.map(el => this.setValues.find(opt => el === this.migrate(opt.title)).id)
: this.values
},
set(val) {
this.$emit('input', val.filter(v => this.setValues.includes(v)).join(','))
if (this.column.dt === 'set' && val) {
this.$emit('input',
this.setValues.filter(el => val.includes(el.id)).map(el => this.migrate(el.title)).join(','))
} else {
this.$emit('input', val.join(',') || null)
}
}
},
unsetValues() {
if (this.value) {
const unsetVals = this.value.split(',').filter(el => !this.setValues.find(sv => sv.id === el))
return unsetVals.map((el) => {
return { id: el, title: el }
})
}
return []
},
setValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp
.match(/(?:[^']|\\')+(?='?(?:,|$))/g)
.map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, ''))
if (this.column && this.column.colOptions?.options) {
return this.column.colOptions.options
}
return []
},
values() {
return this.value ? this.value.split(',') : []
},
parentListeners() {
const $listeners = {}
@ -86,20 +100,38 @@ export default {
// event = document.createEvent('MouseEvents');
// event.initMouseEvent('mousedown', true, true, window);
// this.$el.dispatchEvent(event);
},
methods: {
migrate(val) {
if (this.column.dt === 'set') {
if ((typeof val === 'string' || val instanceof String)) {
return val.replace(/'/g, '')
}
}
return val
}
}
}
</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;
}
}
}
</style>

53
packages/nc-gui/components/project/spreadsheet/components/editableCell/EnumListEditableCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/SingleSelectEditableCell.vue

@ -1,10 +1,12 @@
<template>
<v-select
v-model="localState"
:menu-props="{ bottom: true, offsetY: true }"
solo
dense
flat
:items="enumValues"
:items="enumValues.concat(unsetValue)"
item-value="id"
hide-details
class="mt-0"
:clearable="!column.rqd"
@ -17,14 +19,14 @@
'text-center' : !isForm
}"
>
<v-chip small :color="colors[enumValues.indexOf(item) % colors.length]" class="ma-1">
{{ item }}
<v-chip small :color="item.color" class="ma-1">
{{ migrate(item.title) }}
</v-chip>
</div>
</template>
<template #item="{item}">
<v-chip small :color="colors[enumValues.indexOf(item) % colors.length]">
{{ item }}
<v-chip small :color="item.color">
{{ migrate(item.title) }}
</v-chip>
</template>
<template #append>
@ -36,12 +38,8 @@
</template>
<script>
import colors from '@/mixins/colors'
export default {
name: 'EnumListEditableCell',
mixins: [colors],
name: 'SingleSelectEditableCell',
props: {
value: String,
column: Object,
@ -50,17 +48,32 @@ export default {
computed: {
localState: {
get() {
return this.value && this.value.replace(/\\'/g, '\'').replace(/^'|'$/g, '')
return (this.column.dt === 'enum')
? this.enumValues.find((el) => {
if (this.migrate(el.title) === this.value) {
return this.value
}
return undefined
})
: this.enumValues.find(el => el.id === this.value) || this.value
},
set(val) {
if (this.column.dt === 'enum' && val) {
this.$emit('input', this.migrate(this.enumValues.find(el => el.id === val).title))
} else {
this.$emit('input', val)
}
}
},
unsetValue() {
if (this.value && !this.enumValues.find(el => el.id === this.value)) {
return { id: this.value, title: this.value }
}
return []
},
enumValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp
.split(',')
.map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, ''))
if (this.column && this.column.colOptions?.options) {
return this.column.colOptions.options
}
return []
},
@ -83,6 +96,16 @@ export default {
// event = document.createEvent('MouseEvents');
// event.initMouseEvent('mousedown', true, true, window);
// this.$el.dispatchEvent(event);
},
methods: {
migrate(val) {
if (this.column.dt === 'enum') {
if ((typeof val === 'string' || val instanceof String)) {
return val.replace(/'/g, '')
}
}
return val
}
}
}
</script>

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

@ -254,7 +254,7 @@ export class MysqlUi {
return '';
case 'enum':
return "'a','b'";
return '';
case 'set':
return "'a','b'";
@ -1135,10 +1135,10 @@ export class MysqlUi {
];
case 'MultiSelect':
return ['set', 'text', 'tinytext', 'mediumtext', 'longtext'];
return ['varchar', 'text', 'tinytext', 'mediumtext', 'longtext'];
case 'SingleSelect':
return ['enum', 'text', 'tinytext', 'mediumtext', 'longtext'];
return ['varchar', 'text', 'tinytext', 'mediumtext', 'longtext'];
case 'Year':
return ['year'];

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

@ -698,6 +698,14 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
)
};
if ([UITypes.SingleSelect, UITypes.MultiSelect].includes(colBody.uidt)) {
await Column.update(req.params.columnId, {
...colBody
});
const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id });
await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody);
} else {
const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id });
await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody);
@ -705,6 +713,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
...colBody
});
}
}
Audit.insert({
project_id: base.project_id,
op_type: AuditOperationTypes.TABLE_COLUMN,

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, '')

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

@ -2,10 +2,11 @@ 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 Base from './Base';
import Model from './Model';
import NocoCache from '../cache/NocoCache';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { ColumnType, UITypes } from 'nocodb-sdk';
import {
CacheDelDirection,
@ -232,27 +233,55 @@ export default class Column<T = any> implements ColumnType {
break;
}
case UITypes.MultiSelect: {
for (const option of column.dtxp?.split(',') || []) {
await MultiSelectColumn.insert(
if (column.dt === 'set' && !column.altered) {
for (const [i, option] of column.dtxp?.split(',').entries() || [].entries()) {
await SelectOption.insert(
{
fk_column_id: colId,
title: option
title: option,
order: i + 1
},
ncMeta
);
}
} else {
for (const [i, option] of column.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(
if (column.dt === 'enum' && !column.altered) {
for (const [i, option] of column.dtxp?.split(',').entries() || [].entries()) {
await SelectOption.insert(
{
fk_column_id: colId,
title: option
title: option,
order: i + 1
},
ncMeta
);
}
} else {
for (const [i, option] of column.options.entries() || [].entries()) {
await SelectOption.insert(
{
...option,
fk_column_id: colId,
order: i + 1
},
ncMeta
);
}
}
break;
}
@ -322,10 +351,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);
@ -769,13 +798,43 @@ export default class Column<T = any> implements ColumnType {
case UITypes.MultiSelect:
case UITypes.SingleSelect: {
const model = await oldCol.getModel();
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
dbDriver: NcConnectionMgrv2.get(base)
});
// Handle option delete
if (oldCol.colOptions?.options) {
for (const option of oldCol.colOptions.options.filter(oldOp => column.options.find(newOp => newOp.id === oldOp.id) ? false : true)) {
if (column.uidt === UITypes.SingleSelect) {
if (column.dt === 'enum') {
await baseModel.bulkUpdateAll({ where: `(${oldCol.title},eq,${option.title.replace(/'/g, '')})` }, { [oldCol.title]: null });
} else {
await baseModel.bulkUpdateAll({ where: `(${oldCol.title},eq,${option.id})` }, { [oldCol.title]: null });
}
} else if (column.uidt === UITypes.MultiSelect) {
const dbDriver = NcConnectionMgrv2.get(base);
if (column.dt === 'set') {
await dbDriver.raw(`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ','))`, [model.table_name, oldCol.title, oldCol.title, option.title.replace(/'/g, '')]);
} else {
await dbDriver.raw(`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ','))`, [model.table_name, oldCol.title, oldCol.title, option.id]);
}
}
}
}
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;
}
@ -822,6 +881,7 @@ export default class Column<T = any> implements ColumnType {
// set cache
await NocoCache.set(key, o);
}
// set meta
await ncMeta.metaUpdate(
null,

17
packages/nocodb/src/lib/models/Model.ts

@ -1,5 +1,6 @@
import Noco from '../Noco';
import Column from './Column';
import SelectOption from './SelectOption'
import NocoCache from '../cache/NocoCache';
import { XKnex } from '../db/sql-data-mapper';
import { BaseModelSqlv2 } from '../db/sql-data-mapper/lib/sql/BaseModelSqlv2';
@ -403,10 +404,26 @@ export default class Model implements TableType {
const insertObj = {};
for (const col of await this.getColumns()) {
if (isVirtualCol(col)) continue;
if ([UITypes.SingleSelect, UITypes.MultiSelect].includes(col.uidt) && !['enum', 'set'].includes(col.dt)) {
let val =
data?.[sanitize(col.column_name)] ?? data?.[sanitize(col.title)];
if (val !== undefined) {
let selection = [];
if (val !== null) {
for (const opt of val.split(',')) {
let tmp = await SelectOption.get(opt) ?? await SelectOption.find(col.id, opt)
if (tmp) selection.push(tmp.id);
}
val = selection.join(',');
}
insertObj[sanitize(col.column_name)] = val;
}
} else {
const val =
data?.[sanitize(col.column_name)] ?? data?.[sanitize(col.title)];
if (val !== undefined) insertObj[sanitize(col.column_name)] = val;
}
}
return insertObj;
}

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