Browse Source

feat: editable select options

Signed-off-by: mertmit <mertmit99@gmail.com>
pull/2751/head
mertmit 2 years ago
parent
commit
cfbd84bac1
  1. 42
      packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue
  2. 199
      packages/nc-gui/components/project/spreadsheet/components/editColumn/CustomSelectOptions.vue
  3. 182
      packages/nocodb/src/lib/meta/api/columnApis.ts
  4. 7
      packages/nocodb/src/lib/models/Column.ts

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

@ -132,12 +132,7 @@
<date-options v-model="newColumn.meta" :column="newColumn" :meta="meta" />
</v-col>
<v-col v-if="isSelect" cols="12">
<custom-select-options
ref="customselect"
:column="newColumn"
:meta="meta"
v-on="$listeners"
/>
<custom-select-options ref="customselect" :column="newColumn" :meta="meta" v-on="$listeners" />
</v-col>
<v-col v-else-if="isRating" cols="12">
<rating-options v-model="newColumn.meta" :column="newColumn" :meta="meta" />
@ -649,17 +644,20 @@ 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
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()
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');
}
await this.$emit('saved')
return this.$emit('close')
return;
}
if (this.editColumn) {
@ -692,17 +690,13 @@ export default {
if (this.newColumn.uidt !== UITypes.ID) {
this.newColumn.primaryKey = false;
}
this.newColumn.ai = false
this.newColumn.cdf = null
this.newColumn.un = false
this.newColumn.dtxp = this.sqlUi.getDefaultLengthForDatatype(
this.newColumn.dt
)
this.newColumn.dtxs = this.sqlUi.getDefaultScaleForDatatype(
this.newColumn.dt
)
this.newColumn.dtx = 'specificType'
this.newColumn.ai = false;
this.newColumn.cdf = null;
this.newColumn.un = false;
this.newColumn.dtxp = this.sqlUi.getDefaultLengthForDatatype(this.newColumn.dt);
this.newColumn.dtxs = this.sqlUi.getDefaultScaleForDatatype(this.newColumn.dt);
this.newColumn.dtx = 'specificType';
if (this.isCurrency) {
if (this.column?.uidt === UITypes.Currency) {

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

@ -1,173 +1,156 @@
<template>
<v-container fluid class="wrapper">
<draggable v-model="options" handle=".nc-child-draggable-icon">
<div v-for="(op,i) in options" :key="`${op.color}-${i}`" class="d-flex py-1">
<v-icon
small
class="nc-child-draggable-icon handle"
>
mdi-drag-vertical
</v-icon>
<v-menu
v-model="colorMenus[i]"
rounded="lg"
:close-on-content-click="false"
offset-y
>
<template
#activator="{ on }"
>
<v-icon
:color="op.color"
class="mr-2"
v-on="on"
>
mdi-arrow-down-drop-circle
</v-icon>
<div v-for="(op, i) in options" :key="`${op.color}-${i}`" class="d-flex py-1">
<v-icon small class="nc-child-draggable-icon handle"> mdi-drag-vertical </v-icon>
<v-menu v-model="colorMenus[i]" rounded="lg" :close-on-content-click="false" offset-y>
<template #activator="{ on }">
<v-icon :color="op.color" class="mr-2" v-on="on"> mdi-arrow-down-drop-circle </v-icon>
</template>
<color-picker v-model="op.color" @input="colorMenus[i] = false;" />
<color-picker v-model="op.color" @input="colorMenus[i] = false" />
</v-menu>
<v-text-field
v-model="op.title"
:autofocus="true"
class="caption"
:rules="[enumNotNull, enumNoDuplicate]"
dense
outlined
:disabled="op.id && isMigrated"
/>
<v-icon class="ml-2" color="error lighten-2" size="13" @click="removeOption(op, i)">
mdi-close
</v-icon>
<v-text-field v-model="op.title" :autofocus="true" class="caption" dense outlined />
<v-icon class="ml-2" color="error lighten-2" size="13" @click="removeOption(op, i)"> mdi-close </v-icon>
</div>
<v-btn
slot="footer"
x-small
color="primary"
outlined
class="d-100 caption mt-2"
@click="addNewOption()"
>
<v-icon x-small outlined color="primary" class="mr-2">
mdi-plus
</v-icon>
<v-btn slot="footer" x-small color="primary" outlined class="d-100 caption mt-2" @click="addNewOption()">
<v-icon x-small outlined color="primary" class="mr-2"> mdi-plus </v-icon>
Add option
</v-btn>
</draggable>
<div v-show="error" class="px-2 py-1 text-left caption error--text">
{{ error }}
</div>
</v-container>
</template>
<script>
import draggable from 'vuedraggable'
import ColorPicker from '../ColorPicker.vue'
import { enumColor } from '@/components/project/spreadsheet/helpers/colors'
import draggable from 'vuedraggable';
import { UITypes } from 'nocodb-sdk';
import ColorPicker from '../ColorPicker.vue';
import { enumColor } from '@/components/project/spreadsheet/helpers/colors';
export default {
name: 'CustomSelectOptions',
components: {
draggable,
ColorPicker
ColorPicker,
},
props: ['column', 'meta'],
data: () => ({
options: [],
colorMenus: {},
colors: enumColor.light
colors: enumColor.light,
error: undefined,
}),
computed: {
alias() {
return this.column?.column_name
return this.column?.column_name;
},
isEnumOrSet() {
return ['enum', 'set'].includes(this.column.dt);
},
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
})
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: {
addNewOption() {
const tempOption = {
title: '',
color: this.getNextColor()
}
this.options.push(tempOption)
color: this.getNextColor(),
};
this.options.push(tempOption);
},
async removeOption(option, index) {
this.options.splice(index, 1)
this.options.splice(index, 1);
},
getNextColor() {
let tempColor = this.colors[0]
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]
const lastColor = this.colors.indexOf(this.options[this.options.length - 1].color);
tempColor = this.colors[(lastColor + 1) % this.colors.length];
}
return tempColor
return tempColor;
},
async save() {
try {
const selectCol = {
...this.column,
title: this.alias,
options: this.options
if (this.checkOptions()) {
const selectCol = {
...this.column,
title: this.alias,
colOptions: {
options: this.options,
},
};
await this.$api.dbTableColumn.create(this.meta.id, selectCol);
this.$toast.success('Select column saved successfully').goAway(3000);
return true;
}
await this.$api.dbTableColumn.create(this.meta.id, selectCol)
this.$toast.success('Select column saved successfully').goAway(3000)
return this.$emit('saved', this.alias)
return false;
} catch (e) {
this.$toast.error(e.message).goAway(3000)
this.$toast.error(e.message).goAway(3000);
return false;
}
},
async update() {
try {
const selectCol = {
...this.column,
title: this.alias,
options: this.options
if (this.checkOptions()) {
const selectCol = {
...this.column,
title: this.alias,
colOptions: {
options: this.options,
},
};
await this.$api.dbTableColumn.update(this.column.id, selectCol);
return true;
}
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)
return false;
} catch (e) {
this.$toast.error(e.message).goAway(3000)
this.$toast.error(e.message).goAway(3000);
return false;
}
},
copyOptions(array) {
const temp = []
const temp = [];
if (array && array.length) {
for (const el of array) {
temp.push({ ...el })
temp.push({ ...el });
}
}
return temp
return temp;
},
enumNotNull(v) {
if (this.isMigrated) {
return !!v || 'Migrated options can\'t be null'
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;
}
}
return true
},
enumNoDuplicate(v) {
if (this.isMigrated) {
return this.options.filter(el => el.title === v).length === 1 || 'Migrated options can\'t have duplicates'
if (!failed) {
this.error = null;
return true;
}
return true
}
}
}
return false;
},
},
};
</script>
<style scoped>

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

@ -511,12 +511,12 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
}
if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody?.options.length)
? `${colBody.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
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?.options.length)
? `${colBody.options.map((o) => {
colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.colOptions.options.map((o) => {
if(o.title.includes(',')) {
NcError.badRequest('Illegal char(\',\') for MultiSelect');
throw new Error('');
@ -663,16 +663,21 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
NcError.notImplemented(
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`
);
} else {
} if(
[
UITypes.SingleSelect,
UITypes.MultiSelect
].includes(colBody.uidt)
) {
colBody = getColumnPropsFromUIDT(colBody, base);
if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody?.options.length)
? `${colBody.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
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?.options.length)
? `${colBody.options.map((o) => {
colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.colOptions.options.map((o) => {
if(o.title.includes(',')) {
NcError.badRequest('Illegal char(\',\') for MultiSelect');
throw new Error('');
@ -682,6 +687,95 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
: '';
}
const baseModel = await Model.getBaseModelSQL({
id: table.id,
dbDriver: NcConnectionMgrv2.get(base)
});
if (column.colOptions?.options) {
// Handle migrations
for (const op of column.colOptions.options.filter(el => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')
}
// Handle option delete
for (const option of column.colOptions.options.filter(oldOp => colBody.colOptions.options.find(newOp => newOp.id === oldOp.id) ? false : true)) {
if (column.uidt === UITypes.SingleSelect) {
await baseModel.bulkUpdateAll({ where: `(${column.title},eq,${option.title})` }, { [column.title]: null });
} else if (column.uidt === UITypes.MultiSelect) {
const dbDriver = NcConnectionMgrv2.get(base);
// TODO find_in_set for MySQL optimization
await dbDriver.raw(`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ','))`, [table.table_name, column.title, column.title, option.title]);
}
}
// Handle option update
for (const option of column.colOptions.options.filter(oldOp => colBody.colOptions.options.find(newOp => newOp.id === oldOp.id && newOp.title !== oldOp.title))) {
let newOp = colBody.colOptions.options.find(el => option.id === el.id);
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) {
await baseModel.bulkUpdateAll({ where: `(${column.title},eq,${option.title})` }, { [column.title]: newOp.title });
} else if (column.uidt === UITypes.MultiSelect) {
const dbDriver = NcConnectionMgrv2.get(base);
// TODO find_in_set for MySQL optimization
await dbDriver.raw(`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ',')))`, [table.table_name, column.title, column.title, option.title, newOp.title]);
}
}
}
const tableUpdateBody = {
...table,
tn: table.table_name,
@ -733,12 +827,72 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
),
};
const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id });
await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody);
const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id });
await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody);
await Column.update(req.params.columnId, {
...colBody,
});
} else {
colBody = getColumnPropsFromUIDT(colBody, base);
await Column.update(req.params.columnId, {
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,
});
});
}
Audit.insert({
project_id: base.project_id,

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

@ -3,7 +3,6 @@ import LinkToAnotherRecordColumn from './LinkToAnotherRecordColumn';
import LookupColumn from './LookupColumn';
import RollupColumn from './RollupColumn';
import SelectOption from './SelectOption';
import MultiSelectColumn from './MultiSelectColumn';
import Model from './Model';
import NocoCache from '../cache/NocoCache';
import { ColumnType, UITypes } from 'nocodb-sdk';
@ -244,7 +243,7 @@ export default class Column<T = any> implements ColumnType {
);
}
} else {
for (const [i, option] of column.options.entries() || [].entries()) {
for (const [i, option] of column.colOptions.options.entries() || [].entries()) {
await SelectOption.insert(
{
...option,
@ -270,7 +269,7 @@ export default class Column<T = any> implements ColumnType {
);
}
} else {
for (const [i, option] of column.options.entries() || [].entries()) {
for (const [i, option] of column.colOptions.options.entries() || [].entries()) {
await SelectOption.insert(
{
...option,
@ -850,7 +849,7 @@ export default class Column<T = any> implements ColumnType {
o = { ...o, ...updateObj };
// set cache
await NocoCache.set(key, o);
}
}
// set meta
await ncMeta.metaUpdate(
null,

Loading…
Cancel
Save