Browse Source

feat: select options

Signed-off-by: mertmit <mertmit99@gmail.com>
pull/2751/head
mertmit 2 years ago
parent
commit
045f8d1039
  1. 50
      packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue
  2. 205
      packages/nc-gui/components/project/spreadsheet/components/editColumn/CustomSelectOptions.vue
  3. 8
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  4. 51
      packages/nocodb/src/lib/meta/api/columnApis.ts
  5. 7
      packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts
  6. 71
      packages/nocodb/src/lib/models/Column.ts
  7. 48
      packages/nocodb/src/lib/models/SelectOption.ts
  8. 87
      packages/nocodb/src/lib/models/SingleSelectColumn.ts

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

@ -132,7 +132,12 @@
<date-options v-model="newColumn.meta" :column="newColumn" :meta="meta" />
</v-col>
<v-col v-if="isSelect" cols="12">
<custom-select-options v-model="newColumn.dtxp" @input="newColumn.altered = newColumn.altered || 2" />
<custom-select-options
ref="customselect"
:column="newColumn"
:meta="meta"
v-on="$listeners"
/>
</v-col>
<v-col v-else-if="isRating" cols="12">
<rating-options v-model="newColumn.meta" :column="newColumn" :meta="meta" />
@ -644,9 +649,18 @@ 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()
}
await this.$emit('saved')
return this.$emit('close')
}
if (this.editColumn) {
await this.$api.dbTableColumn.update(this.column.id, this.newColumn);
@ -678,18 +692,17 @@ 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';
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;
}
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) {
@ -722,11 +735,6 @@ export default {
this.newColumn.dtxp = this.sqlUi.getDefaultLengthForDatatype(this.newColumn.dt);
this.newColumn.dtxs = this.sqlUi.getDefaultScaleForDatatype(this.newColumn.dt);
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect];
if (this.column && selectTypes.includes(this.newColumn.uidt) && selectTypes.includes(this.column.uidt)) {
this.newColumn.dtxp = this.column.dtxp;
}
if (columnToValidate.includes(this.newColumn.uidt)) {
this.newColumn.meta = {
validate: this.newColumn.meta && this.newColumn.meta.validate,

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

@ -1,68 +1,173 @@
<template>
<v-container fluid class="wrapper">
<div v-for="(op, i) in localState" :key="i" class="d-flex py-1">
<v-icon :color="colors[i % colors.length]" class="mr-2" @click="localState.splice(i, 1)">
mdi-arrow-down-drop-circle
</v-icon>
<v-text-field
v-model="localState[i]"
:autofocus="true"
class="caption"
dense
<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>
</template>
<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>
</div>
<v-btn
slot="footer"
x-small
color="primary"
outlined
@input="listenForComma(i, $event)"
/>
<v-icon class="ml-2" color="error lighten-2" size="13" @click="localState.splice(i, 1)"> mdi-close </v-icon>
</div>
<v-btn x-small color="primary" outlined class="d-100 caption mt-2" @click="localState.push('')">
<v-icon x-small outlined color="primary" class="mr-2"> mdi-plus </v-icon>
Add option
</v-btn>
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>
</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;
},
},
watch: {
localState: {
handler(v) {
this.$emit('input', v.map(v => `'${v.replace(/'/g, "\\'")}'`).join(','));
},
deep: true,
},
value() {
this.syncState();
alias() {
return this.column?.column_name
},
isMigrated() {
return ['enum', 'set'].includes(this.column.dt)
}
},
mounted() {
this.syncState();
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: {
syncState() {
this.localState = (this.value || '').split(',').map(v => v.replace(/\\'/g, "'").replace(/^'|'$/g, ''));
addNewOption() {
const tempOption = {
title: '',
color: this.getNextColor()
}
this.options.push(tempOption)
},
async removeOption(option, index) {
this.options.splice(index, 1)
},
listenForComma(index, value) {
const normalisedValue = value.trim();
if (normalisedValue.endsWith(',')) {
this.localState.push('');
return;
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]
}
this.localState[index] = normalisedValue;
return tempColor
},
},
};
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)
}
},
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)
}
},
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
},
enumNoDuplicate(v) {
if (this.isMigrated) {
return this.options.filter(el => el.title === v).length === 1 || 'Migrated options can\'t have duplicates'
}
return true
}
}
}
</script>
<style scoped>
@ -74,4 +179,14 @@ export default {
/deep/ .v-input__control {
height: 33px;
}
.handle {
cursor: pointer;
}
/deep/ .v-text-field__details {
position: absolute;
margin-top: 10px;
margin-left: 25px;
}
</style>

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

@ -254,10 +254,10 @@ export class MysqlUi {
return '';
case 'enum':
return "'a','b'";
return '';
case 'set':
return "'a','b'";
return '';
case 'geometry':
return '';
@ -976,10 +976,10 @@ export class MysqlUi {
colProp.dtxp = 1;
break;
case 'MultiSelect':
colProp.dt = 'text';
colProp.dt = 'set';
break;
case 'SingleSelect':
colProp.dt = 'text';
colProp.dt = 'enum';
break;
case 'Collaborator':
colProp.dt = 'varchar';

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

@ -509,6 +509,23 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
// Duration column needs more that that
colBody.dtxs = '4';
}
if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody?.options.length)
? `${colBody.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
: '';
} else if (colBody.uidt === UITypes.MultiSelect){
colBody.dtxp = (colBody?.options.length)
? `${colBody.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,
@ -540,13 +557,6 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
const insertedColumnMeta =
columns.find((c) => c.cn === colBody.column_name) || ({} as any);
if (
colBody.uidt === UITypes.SingleSelect ||
colBody.uidt === UITypes.MultiSelect
) {
insertedColumnMeta.dtxp = colBody.dtxp;
}
await Column.insert({
...colBody,
...insertedColumnMeta,
@ -655,6 +665,23 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
);
} else {
colBody = getColumnPropsFromUIDT(colBody, base);
if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody?.options.length)
? `${colBody.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
: '';
} else if (colBody.uidt === UITypes.MultiSelect){
colBody.dtxp = (colBody?.options.length)
? `${colBody.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,
@ -706,12 +733,12 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
),
};
const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id });
await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody);
await Column.update(req.params.columnId, {
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/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, '')

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

@ -2,7 +2,7 @@ import FormulaColumn from './FormulaColumn';
import LinkToAnotherRecordColumn from './LinkToAnotherRecordColumn';
import LookupColumn from './LookupColumn';
import RollupColumn from './RollupColumn';
import SingleSelectColumn from './SingleSelectColumn';
import SelectOption from './SelectOption';
import MultiSelectColumn from './MultiSelectColumn';
import Model from './Model';
import NocoCache from '../cache/NocoCache';
@ -232,26 +232,54 @@ export default class Column<T = any> implements ColumnType {
break;
}
case UITypes.MultiSelect: {
for (const option of column.dtxp?.split(',') || []) {
await MultiSelectColumn.insert(
{
fk_column_id: colId,
title: option,
},
ncMeta
);
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,
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(
{
fk_column_id: colId,
title: option,
},
ncMeta
);
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,
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 +350,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);
@ -772,10 +800,11 @@ export default class Column<T = any> implements ColumnType {
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;
}

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