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. 94
      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" :column="column"
@click.stop="$emit('enableedit')" @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-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" /> <url-cell v-else-if="isURL" :value="value" />
<email-cell v-else-if="isEmail" :value="value" /> <email-cell v-else-if="isEmail" :value="value" />
<json-cell v-else-if="isJSON" :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 JsonCell from '~/components/project/spreadsheet/components/cell/JsonCell'
import UrlCell from '~/components/project/spreadsheet/components/cell/UrlCell' import UrlCell from '~/components/project/spreadsheet/components/cell/UrlCell'
import cell from '@/components/project/spreadsheet/mixins/cell' import cell from '@/components/project/spreadsheet/mixins/cell'
import SetListCell from '~/components/project/spreadsheet/components/cell/SetListCell' import MultiSelectCell from '~/components/project/spreadsheet/components/cell/MultiSelectCell'
import EnumCell from '~/components/project/spreadsheet/components/cell/EnumCell' import SingleSelectCell from '~/components/project/spreadsheet/components/cell/SingleSelectCell'
import EditableAttachmentCell from '~/components/project/spreadsheet/components/editableCell/EditableAttachmentCell' import EditableAttachmentCell from '~/components/project/spreadsheet/components/editableCell/EditableAttachmentCell'
import BooleanCell from '~/components/project/spreadsheet/components/cell/BooleanCell' import BooleanCell from '~/components/project/spreadsheet/components/cell/BooleanCell'
import EmailCell from '~/components/project/spreadsheet/components/cell/EmailCell' import EmailCell from '~/components/project/spreadsheet/components/cell/EmailCell'
@ -40,7 +40,7 @@ import CurrencyCell from '@/components/project/spreadsheet/components/cell/Curre
export default { export default {
name: 'TableCell', 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], mixins: [cell],
props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'], props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'],
computed: { computed: {

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

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

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

@ -74,13 +74,13 @@
v-on="parentListeners" v-on="parentListeners"
/> />
<enum-cell <single-select-cell
v-else-if="isEnum && (( !isForm && !active) || isLocked || (isPublic && !isForm))" v-else-if="isEnum && (( !isForm && !active) || isLocked || (isPublic && !isForm))"
v-model="localState" v-model="localState"
:column="column" :column="column"
v-on="parentListeners" v-on="parentListeners"
/> />
<enum-list-cell <single-select-editable-cell
v-else-if="isEnum" v-else-if="isEnum"
v-model="localState" v-model="localState"
:is-form="isForm" :is-form="isForm"
@ -97,14 +97,14 @@
@input="$emit('save')" @input="$emit('save')"
/> />
<set-list-editable-cell <multi-select-editable-cell
v-else-if="isSet && (active || isForm) && !isLocked && !(isPublic && !isForm)" v-else-if="isSet && (active || isForm) && !isLocked && !(isPublic && !isForm)"
v-model="localState" v-model="localState"
:column="column" :column="column"
v-on="parentListeners" v-on="parentListeners"
@input="$emit('save')" @input="$emit('save')"
/> />
<set-list-cell <multi-select-cell
v-else-if="isSet" v-else-if="isSet"
v-model="localState" v-model="localState"
:column="column" :column="column"
@ -137,16 +137,16 @@ import JsonEditableCell from '~/components/project/spreadsheet/components/editab
import TextCell from '~/components/project/spreadsheet/components/editableCell/TextCell' import TextCell from '~/components/project/spreadsheet/components/editableCell/TextCell'
import DateTimePickerCell from '~/components/project/spreadsheet/components/editableCell/DateTimePickerCell' import DateTimePickerCell from '~/components/project/spreadsheet/components/editableCell/DateTimePickerCell'
import TextAreaCell from '~/components/project/spreadsheet/components/editableCell/TextAreaCell' 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 IntegerCell from '~/components/project/spreadsheet/components/editableCell/IntegerCell'
import FloatCell from '~/components/project/spreadsheet/components/editableCell/FloatCell' import FloatCell from '~/components/project/spreadsheet/components/editableCell/FloatCell'
import TimePickerCell from '~/components/project/spreadsheet/components/editableCell/TimePickerCell' import TimePickerCell from '~/components/project/spreadsheet/components/editableCell/TimePickerCell'
import BooleanCell from '~/components/project/spreadsheet/components/editableCell/BooleanCell' import BooleanCell from '~/components/project/spreadsheet/components/editableCell/BooleanCell'
import cell from '@/components/project/spreadsheet/mixins/cell' import cell from '@/components/project/spreadsheet/mixins/cell'
import EditableAttachmentCell from '~/components/project/spreadsheet/components/editableCell/EditableAttachmentCell' import EditableAttachmentCell from '~/components/project/spreadsheet/components/editableCell/EditableAttachmentCell'
import EnumCell from '~/components/project/spreadsheet/components/cell/EnumCell' import SingleSelectCell from '~/components/project/spreadsheet/components/cell/SingleSelectCell'
import SetListEditableCell from '~/components/project/spreadsheet/components/editableCell/SetListEditableCell' import MultiSelectEditableCell from '~/components/project/spreadsheet/components/editableCell/MultiSelectEditableCell'
import SetListCell from '~/components/project/spreadsheet/components/cell/SetListCell' import MultiSelectCell from '~/components/project/spreadsheet/components/cell/MultiSelectCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell' import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
export default { export default {
@ -155,15 +155,15 @@ export default {
RatingCell, RatingCell,
JsonEditableCell, JsonEditableCell,
EditableUrlCell, EditableUrlCell,
SetListCell, MultiSelectCell,
SetListEditableCell, MultiSelectEditableCell,
EnumCell, SingleSelectCell,
EditableAttachmentCell, EditableAttachmentCell,
BooleanCell, BooleanCell,
TimePickerCell, TimePickerCell,
FloatCell, FloatCell,
IntegerCell, IntegerCell,
EnumListCell, SingleSelectEditableCell,
TextAreaCell, TextAreaCell,
DateTimePickerCell, DateTimePickerCell,
TextCell, 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> <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">
<div v-for="(op,i) in options" :key="`${op.color}-${i}`" class="d-flex py-1">
<v-icon <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" class="mr-2"
@click="localState.splice(i,1)" v-on="on"
> >
mdi-arrow-down-drop-circle mdi-arrow-down-drop-circle
</v-icon> </v-icon>
</template>
<color-picker v-model="op.color" @input="colorMenus[i] = false;" />
</v-menu>
<v-text-field <v-text-field
v-model="localState[i]" v-model="op.title"
:autofocus="true" :autofocus="true"
class="caption" class="caption"
:rules="[enumNotNull, enumNoDuplicate]"
dense dense
outlined 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 mdi-close
</v-icon> </v-icon>
</div> </div>
<v-btn <v-btn
slot="footer"
x-small x-small
color="primary" color="primary"
outlined outlined
class="d-100 caption mt-2" class="d-100 caption mt-2"
@click="localState.push('')" @click="addNewOption()"
> >
<v-icon x-small outlined color="primary" class="mr-2"> <v-icon x-small outlined color="primary" class="mr-2">
mdi-plus mdi-plus
</v-icon> </v-icon>
Add option Add option
</v-btn> </v-btn>
</draggable>
</v-container> </v-container>
</template> </template>
<script> <script>
import draggable from 'vuedraggable'
import colors from '@/components/project/spreadsheet/helpers/colors' 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
}), }),
computed: { computed: {
colors() { alias() {
return this.$store.state.settings.darkTheme ? colors.dark : colors.light return this.column?.column_name
},
isMigrated() {
return ['enum', 'set'].includes(this.column.dt)
} }
}, },
watch: { created() {
localState: { this.options = this.copyOptions(this.column.colOptions?.options) || []
handler(v) { // migrate
this.$emit('input', v.map(v => `'${v.replace(/'/g, '\\\'')}'`).join(',')) if (this.isMigrated && this.options.length) {
this.options.map((el) => {
el.title = el.title.replace(/'/g, '')
return el
})
}
}, },
deep: true methods: {
addNewOption() {
const tempOption = {
title: '',
color: this.getNextColor()
}
this.options.push(tempOption)
},
async removeOption(option, index) {
this.options.splice(index, 1)
}, },
value() { getNextColor() {
this.syncState() 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
}, },
mounted() { async save() {
this.syncState() 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)
}
}, },
methods: { async update() {
syncState() { try {
this.localState = (this.value || '').split(',').map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, '')) 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)
}
}, },
listenForComma(index, value) { copyOptions(array) {
const normalisedValue = value.trim() const temp = []
if (normalisedValue.endsWith(',')) { if (array && array.length) {
this.localState.push('') for (const el of array) {
return temp.push({ ...el })
} }
this.localState[index] = normalisedValue }
return temp
},
enumNotNull(v) {
if (this.isMigrated) {
return !!v || 'Migrated options can\'t be null'
}
return true
},
enumNoDuplicate(v) {
if (this.isMigrated) {
return this.options.filter(el => el.title === v).length === 1 || 'Migrated options can\'t have duplicates'
}
return true
} }
} }
} }
@ -90,4 +178,13 @@ export default {
height: 33px; height: 33px;
} }
.handle {
cursor: pointer;
}
/deep/ .v-text-field__details {
position: absolute;
margin-top: 10px;
margin-left: 25px;
}
</style> </style>

94
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> <template>
<div> <div>
<v-combobox <v-select
v-model="localState" v-model="localState"
:items="setValues" :menu-props="{ bottom: true, offsetY: true }"
:items="setValues.concat(unsetValues)"
item-value="id"
multiple multiple
chips chips
flat flat
@ -14,36 +16,33 @@
> >
<template #selection="data"> <template #selection="data">
<v-chip <v-chip
:key="data.item" :key="data.item.id"
small small
class="ma-1" class="ma-1"
:color="colors[setValues.indexOf(data.item) % colors.length]" :color="data.item.color"
@click:close="data.parent.selectItem(data.item)" @click:close="data.parent.selectItem(data.item)"
> >
{{ data.item }} {{ migrate(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 }} {{ migrate(item.title) }}
</v-chip> </v-chip>
</template> </template>
<template #append> <template #append>
<v-icon small class="mt-2"> <v-icon small class="mt-1">
mdi-menu-down mdi-menu-down
</v-icon> </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: 'MultiSelectEditableCell',
mixins: [colors],
props: { props: {
value: String, value: String,
column: Object column: Object
@ -51,22 +50,37 @@ export default {
computed: { computed: {
localState: { localState: {
get() { get() {
return this.value && this.value return (this.column.dt === 'set')
.match(/(?:[^',]|\\')+(?='?(?:,|$))/g) ? this.values.map(el => this.setValues.find(opt => el === this.migrate(opt.title)).id)
.map(v => v.replace(/\\'/g, '\'')) : this.values
}, },
set(val) { 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() { setValues() {
if (this.column && this.column.dtxp) { if (this.column && this.column.colOptions?.options) {
return this.column.dtxp return this.column.colOptions.options
.match(/(?:[^']|\\')+(?='?(?:,|$))/g)
.map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, ''))
} }
return [] return []
}, },
values() {
return this.value ? this.value.split(',') : []
},
parentListeners() { parentListeners() {
const $listeners = {} const $listeners = {}
@ -86,20 +100,38 @@ export default {
// event = document.createEvent('MouseEvents'); // event = document.createEvent('MouseEvents');
// event.initMouseEvent('mousedown', true, true, window); // event.initMouseEvent('mousedown', true, true, window);
// this.$el.dispatchEvent(event); // 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> </script>
<style scoped> <style scoped lang="scss">
select {
width: 100%; ::v-deep {
height: 100%; .v-select {
color: var(--v-textColor-base); min-width: 150px;
-webkit-appearance: menulist; .v-select__selections {
/*webkit browsers */ min-height: 38px !important;
-moz-appearance: menulist; }
/*Firefox */ }
appearance: menulist; .v-input__slot{
padding-right: 0 !important;
}
.v-input__icon.v-input__icon--clear {
width: 15px !important;
.v-icon {
font-size: 13px !important;
}
}
} }
</style> </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> <template>
<v-select <v-select
v-model="localState" v-model="localState"
:menu-props="{ bottom: true, offsetY: true }"
solo solo
dense dense
flat flat
:items="enumValues" :items="enumValues.concat(unsetValue)"
item-value="id"
hide-details hide-details
class="mt-0" class="mt-0"
:clearable="!column.rqd" :clearable="!column.rqd"
@ -17,14 +19,14 @@
'text-center' : !isForm 'text-center' : !isForm
}" }"
> >
<v-chip small :color="colors[enumValues.indexOf(item) % colors.length]" class="ma-1"> <v-chip small :color="item.color" class="ma-1">
{{ item }} {{ migrate(item.title) }}
</v-chip> </v-chip>
</div> </div>
</template> </template>
<template #item="{item}"> <template #item="{item}">
<v-chip small :color="colors[enumValues.indexOf(item) % colors.length]"> <v-chip small :color="item.color">
{{ item }} {{ migrate(item.title) }}
</v-chip> </v-chip>
</template> </template>
<template #append> <template #append>
@ -36,12 +38,8 @@
</template> </template>
<script> <script>
import colors from '@/mixins/colors'
export default { export default {
name: 'EnumListEditableCell', name: 'SingleSelectEditableCell',
mixins: [colors],
props: { props: {
value: String, value: String,
column: Object, column: Object,
@ -50,17 +48,32 @@ export default {
computed: { computed: {
localState: { localState: {
get() { 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) { 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) 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() { enumValues() {
if (this.column && this.column.dtxp) { if (this.column && this.column.colOptions?.options) {
return this.column.dtxp return this.column.colOptions.options
.split(',')
.map(v => v.replace(/\\'/g, '\'').replace(/^'|'$/g, ''))
} }
return [] return []
}, },
@ -83,6 +96,16 @@ export default {
// event = document.createEvent('MouseEvents'); // event = document.createEvent('MouseEvents');
// event.initMouseEvent('mousedown', true, true, window); // event.initMouseEvent('mousedown', true, true, window);
// this.$el.dispatchEvent(event); // 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> </script>

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

@ -254,7 +254,7 @@ export class MysqlUi {
return ''; return '';
case 'enum': case 'enum':
return "'a','b'"; return '';
case 'set': case 'set':
return "'a','b'"; return "'a','b'";
@ -1135,10 +1135,10 @@ export class MysqlUi {
]; ];
case 'MultiSelect': case 'MultiSelect':
return ['set', 'text', 'tinytext', 'mediumtext', 'longtext']; return ['varchar', 'text', 'tinytext', 'mediumtext', 'longtext'];
case 'SingleSelect': case 'SingleSelect':
return ['enum', 'text', 'tinytext', 'mediumtext', 'longtext']; return ['varchar', 'text', 'tinytext', 'mediumtext', 'longtext'];
case 'Year': case 'Year':
return ['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 }); const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id });
await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody);
@ -705,6 +713,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
...colBody ...colBody
}); });
} }
}
Audit.insert({ Audit.insert({
project_id: base.project_id, project_id: base.project_id,
op_type: AuditOperationTypes.TABLE_COLUMN, 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 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, '')

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

@ -2,10 +2,11 @@ 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 Base from './Base';
import Model from './Model'; import Model from './Model';
import NocoCache from '../cache/NocoCache'; import NocoCache from '../cache/NocoCache';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { ColumnType, UITypes } from 'nocodb-sdk'; import { ColumnType, UITypes } from 'nocodb-sdk';
import { import {
CacheDelDirection, CacheDelDirection,
@ -232,27 +233,55 @@ 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.dt === 'set' && !column.altered) {
await MultiSelectColumn.insert( for (const [i, option] of column.dtxp?.split(',').entries() || [].entries()) {
await SelectOption.insert(
{ {
fk_column_id: colId, fk_column_id: colId,
title: option title: option,
order: i + 1
}, },
ncMeta ncMeta
); );
} }
} else {
for (const [i, option] of column.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.dt === 'enum' && !column.altered) {
await SingleSelectColumn.insert( for (const [i, option] of column.dtxp?.split(',').entries() || [].entries()) {
await SelectOption.insert(
{ {
fk_column_id: colId, fk_column_id: colId,
title: option title: option,
order: i + 1
}, },
ncMeta ncMeta
); );
} }
} else {
for (const [i, option] of column.options.entries() || [].entries()) {
await SelectOption.insert(
{
...option,
fk_column_id: colId,
order: i + 1
},
ncMeta
);
}
}
break; break;
} }
@ -322,10 +351,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);
@ -769,13 +798,43 @@ export default class Column<T = any> implements ColumnType {
case UITypes.MultiSelect: case UITypes.MultiSelect:
case UITypes.SingleSelect: { 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, { 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;
} }
@ -822,6 +881,7 @@ export default class Column<T = any> implements ColumnType {
// 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,

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

@ -1,5 +1,6 @@
import Noco from '../Noco'; import Noco from '../Noco';
import Column from './Column'; import Column from './Column';
import SelectOption from './SelectOption'
import NocoCache from '../cache/NocoCache'; import NocoCache from '../cache/NocoCache';
import { XKnex } from '../db/sql-data-mapper'; import { XKnex } from '../db/sql-data-mapper';
import { BaseModelSqlv2 } from '../db/sql-data-mapper/lib/sql/BaseModelSqlv2'; import { BaseModelSqlv2 } from '../db/sql-data-mapper/lib/sql/BaseModelSqlv2';
@ -403,10 +404,26 @@ export default class Model implements TableType {
const insertObj = {}; const insertObj = {};
for (const col of await this.getColumns()) { for (const col of await this.getColumns()) {
if (isVirtualCol(col)) continue; 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 = const val =
data?.[sanitize(col.column_name)] ?? data?.[sanitize(col.title)]; data?.[sanitize(col.column_name)] ?? data?.[sanitize(col.title)];
if (val !== undefined) insertObj[sanitize(col.column_name)] = val; if (val !== undefined) insertObj[sanitize(col.column_name)] = val;
} }
}
return insertObj; 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 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