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

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

@ -1,173 +1,156 @@
<template> <template>
<v-container fluid class="wrapper"> <v-container fluid class="wrapper">
<draggable v-model="options" handle=".nc-child-draggable-icon"> <draggable v-model="options" handle=".nc-child-draggable-icon">
<div v-for="(op,i) in options" :key="`${op.color}-${i}`" class="d-flex py-1"> <div v-for="(op, i) in options" :key="`${op.color}-${i}`" class="d-flex py-1">
<v-icon <v-icon small class="nc-child-draggable-icon handle"> mdi-drag-vertical </v-icon>
small <v-menu v-model="colorMenus[i]" rounded="lg" :close-on-content-click="false" offset-y>
class="nc-child-draggable-icon handle" <template #activator="{ on }">
> <v-icon :color="op.color" class="mr-2" v-on="on"> mdi-arrow-down-drop-circle </v-icon>
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> </template>
<color-picker v-model="op.color" @input="colorMenus[i] = false;" /> <color-picker v-model="op.color" @input="colorMenus[i] = false" />
</v-menu> </v-menu>
<v-text-field <v-text-field v-model="op.title" :autofocus="true" class="caption" dense outlined />
v-model="op.title" <v-icon class="ml-2" color="error lighten-2" size="13" @click="removeOption(op, i)"> mdi-close </v-icon>
: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>
</div> </div>
<v-btn <v-btn slot="footer" x-small color="primary" outlined class="d-100 caption mt-2" @click="addNewOption()">
slot="footer" <v-icon x-small outlined color="primary" class="mr-2"> mdi-plus </v-icon>
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 Add option
</v-btn> </v-btn>
</draggable> </draggable>
<div v-show="error" class="px-2 py-1 text-left caption error--text">
{{ error }}
</div>
</v-container> </v-container>
</template> </template>
<script> <script>
import draggable from 'vuedraggable' import draggable from 'vuedraggable';
import ColorPicker from '../ColorPicker.vue' import { UITypes } from 'nocodb-sdk';
import { enumColor } 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',
components: { components: {
draggable, draggable,
ColorPicker ColorPicker,
}, },
props: ['column', 'meta'], props: ['column', 'meta'],
data: () => ({ data: () => ({
options: [], options: [],
colorMenus: {}, colorMenus: {},
colors: enumColor.light colors: enumColor.light,
error: undefined,
}), }),
computed: { computed: {
alias() { 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() { created() {
this.options = this.copyOptions(this.column.colOptions?.options) || [] this.options = this.copyOptions(this.column.colOptions?.options) || [];
// migrate // Support for older options
/* for (const op of this.options.filter(el => el.order === null)) {
if (this.isMigrated && this.options.length) { op.title = op.title.replace(/^'/, '').replace(/'$/, '')
this.options.map((el) => {
el.title = el.title.replace(/'/g, '')
return el
})
} }
*/
}, },
methods: { methods: {
addNewOption() { addNewOption() {
const tempOption = { const tempOption = {
title: '', title: '',
color: this.getNextColor() color: this.getNextColor(),
} };
this.options.push(tempOption) this.options.push(tempOption);
}, },
async removeOption(option, index) { async removeOption(option, index) {
this.options.splice(index, 1) this.options.splice(index, 1);
}, },
getNextColor() { getNextColor() {
let tempColor = this.colors[0] let tempColor = this.colors[0];
if (this.options.length && this.options[this.options.length - 1].color) { if (this.options.length && this.options[this.options.length - 1].color) {
const lastColor = this.colors.indexOf(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] tempColor = this.colors[(lastColor + 1) % this.colors.length];
} }
return tempColor return tempColor;
}, },
async save() { async save() {
try { try {
const selectCol = { if (this.checkOptions()) {
...this.column, const selectCol = {
title: this.alias, ...this.column,
options: this.options 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) return false;
this.$toast.success('Select column saved successfully').goAway(3000)
return this.$emit('saved', this.alias)
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
return false;
} }
}, },
async update() { async update() {
try { try {
const selectCol = { if (this.checkOptions()) {
...this.column, const selectCol = {
title: this.alias, ...this.column,
options: this.options title: this.alias,
colOptions: {
options: this.options,
},
};
await this.$api.dbTableColumn.update(this.column.id, selectCol);
return true;
} }
if (this.isMigrated) { return false;
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) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
return false;
} }
}, },
copyOptions(array) { copyOptions(array) {
const temp = [] const temp = [];
if (array && array.length) { if (array && array.length) {
for (const el of array) { for (const el of array) {
temp.push({ ...el }) temp.push({ ...el });
} }
} }
return temp return temp;
}, },
enumNotNull(v) { checkOptions() {
if (this.isMigrated) { let failed = false;
return !!v || 'Migrated options can\'t be null' 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 if (!failed) {
}, this.error = null;
enumNoDuplicate(v) { return true;
if (this.isMigrated) {
return this.options.filter(el => el.title === v).length === 1 || 'Migrated options can\'t have duplicates'
} }
return true return false;
} },
} },
} };
</script> </script>
<style scoped> <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) { if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody?.options.length) colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}` ? `${colBody.colOptions.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
: ''; : '';
} else if (colBody.uidt === UITypes.MultiSelect){ } else if (colBody.uidt === UITypes.MultiSelect){
colBody.dtxp = (colBody?.options.length) colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.options.map((o) => { ? `${colBody.colOptions.options.map((o) => {
if(o.title.includes(',')) { if(o.title.includes(',')) {
NcError.badRequest('Illegal char(\',\') for MultiSelect'); NcError.badRequest('Illegal char(\',\') for MultiSelect');
throw new Error(''); throw new Error('');
@ -663,16 +663,21 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
NcError.notImplemented( NcError.notImplemented(
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented` `Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`
); );
} else { } if(
[
UITypes.SingleSelect,
UITypes.MultiSelect
].includes(colBody.uidt)
) {
colBody = getColumnPropsFromUIDT(colBody, base); colBody = getColumnPropsFromUIDT(colBody, base);
if (colBody.uidt === UITypes.SingleSelect) { if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody?.options.length) colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}` ? `${colBody.colOptions.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
: ''; : '';
} else if (colBody.uidt === UITypes.MultiSelect){ } else if (colBody.uidt === UITypes.MultiSelect){
colBody.dtxp = (colBody?.options.length) colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.options.map((o) => { ? `${colBody.colOptions.options.map((o) => {
if(o.title.includes(',')) { if(o.title.includes(',')) {
NcError.badRequest('Illegal char(\',\') for MultiSelect'); NcError.badRequest('Illegal char(\',\') for MultiSelect');
throw new Error(''); 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 = { const tableUpdateBody = {
...table, ...table,
tn: table.table_name, 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 }); const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id });
await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); 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, ...colBody,
}); });
} }
Audit.insert({ Audit.insert({
project_id: base.project_id, 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 LookupColumn from './LookupColumn';
import RollupColumn from './RollupColumn'; import RollupColumn from './RollupColumn';
import SelectOption from './SelectOption'; import SelectOption from './SelectOption';
import MultiSelectColumn from './MultiSelectColumn';
import Model from './Model'; import Model from './Model';
import NocoCache from '../cache/NocoCache'; import NocoCache from '../cache/NocoCache';
import { ColumnType, UITypes } from 'nocodb-sdk'; import { ColumnType, UITypes } from 'nocodb-sdk';
@ -244,7 +243,7 @@ export default class Column<T = any> implements ColumnType {
); );
} }
} else { } else {
for (const [i, option] of column.options.entries() || [].entries()) { for (const [i, option] of column.colOptions.options.entries() || [].entries()) {
await SelectOption.insert( await SelectOption.insert(
{ {
...option, ...option,
@ -270,7 +269,7 @@ export default class Column<T = any> implements ColumnType {
); );
} }
} else { } else {
for (const [i, option] of column.options.entries() || [].entries()) { for (const [i, option] of column.colOptions.options.entries() || [].entries()) {
await SelectOption.insert( await SelectOption.insert(
{ {
...option, ...option,
@ -850,7 +849,7 @@ export default class Column<T = any> implements ColumnType {
o = { ...o, ...updateObj }; o = { ...o, ...updateObj };
// set cache // set cache
await NocoCache.set(key, o); await NocoCache.set(key, o);
} }
// set meta // set meta
await ncMeta.metaUpdate( await ncMeta.metaUpdate(
null, null,

Loading…
Cancel
Save