mirror of https://github.com/nocodb/nocodb
Browse Source
* feat: created inferTypes.ts * chore: switch to sdk types * feat: convert string to given type * feat: convert string to rating * fix: handle the case with multiple . * feat: numeric decimal type conversion in postgres * feat: add cast for non text fields * refactor: move type casts to separate file * feat: add casts for date, date-time and time * doc: added function docs * feat: added cast for year and rating * feat: added cast for duration * fix: cast for multi-select * fix: cast for multi-select * fix: cast for year * feat: date conversion on best effort basis * fix: single line text to select * fix: any field to select * lint: simplified expressions * fix: user conversion * fix: user conversion * fix: date time conversion * test: added test cases for type casts * fix: SLT to User field * fix: SLT to Long text, single select and multiselect * chore: handle True/False & TRUE/FALSE in checkbox * lint: fixed eslint issues * chore: remove system fields as destination type when converting a field * feat: show warning when changing column type * fix: toned down edit modal * test: click on update button during warning popup * test: update selector * test: fix type change flag * fix: handle date format * chore: auto focus update button * fix: parameterize columnName and other values * chore: removed number of digits limit for hour * test: fix add-edit modal label * fix: fixed missing column reference * fix: handle missing date format * fix: handle missing date format * test: fix save routine mux * test: fix barCode & QRCode save * refactor: combined uiType filters * fix: sanitise column name * refactor: switch to some instead of find * feat: created inferTypes.ts * chore: switch to sdk types * feat: convert string to given type * feat: numeric decimal type conversion in postgres * feat: add cast for non text fields * refactor: move type casts to separate file * feat: add casts for date, date-time and time * doc: added function docs * feat: added cast for year and rating * feat: added cast for duration * fix: cast for multi-select * fix: cast for multi-select * fix: cast for year * feat: date conversion on best effort basis * fix: single line text to select * fix: user conversion * fix: date time conversion * fix: SLT to User field * fix: SLT to Long text, single select and multiselect * chore: handle True/False & TRUE/FALSE in checkbox * lint: fixed eslint issues * feat: show warning when changing column type * fix: toned down edit modal * test: click on update button during warning popup * fix: handle date format * chore: auto focus update button * fix: parameterize columnName and other values * chore: removed number of digits limit for hour * fix: handle missing date format * fix: handle missing date format * test: fix save routine mux * fix: revert removing verify * fix: sanitise column name * fix: pass context * tests: remove duplicate statement * fix: add context bypass for list method * fix: disable type conversion for Formula, BarCode, QrCode * fix: render confirm modal sing useDialog to avoid accidental closing * refactor: construct context using column while getting colOptions data --------- Co-authored-by: rohittp <tprohit9@gmail.com> Co-authored-by: Pranav C <pranavxc@gmail.com>pull/8706/head
Raju Udava
4 months ago
committed by
GitHub
18 changed files with 753 additions and 153 deletions
@ -0,0 +1,46 @@
|
||||
<script setup lang="ts"> |
||||
const props = defineProps<{ |
||||
visible?: boolean |
||||
saving?: boolean |
||||
}>() |
||||
|
||||
const emit = defineEmits(['submit', 'cancel', 'update:visible']) |
||||
|
||||
const visible = useVModel(props, 'visible', emit) |
||||
</script> |
||||
|
||||
<template> |
||||
<GeneralModal v-model:visible="visible" size="small"> |
||||
<div class="flex flex-col p-6" @click.stop> |
||||
<div class="flex flex-row pb-2 mb-4 font-medium text-lg border-b-1 border-gray-50 text-gray-800">Field Type Change</div> |
||||
|
||||
<div class="mb-3 text-gray-800"> |
||||
<div class="flex item-center gap-2 mb-4"> |
||||
<component :is="iconMap.warning" id="nc-selected-item-icon" class="text-yellow-500 w-10 h-10" /> |
||||
This action cannot be undone. Converting data types may result in data loss. Proceed with caution! |
||||
</div> |
||||
</div> |
||||
|
||||
<slot name="entity-preview"></slot> |
||||
|
||||
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end"> |
||||
<NcButton type="secondary" @click="visible = false"> |
||||
{{ $t('general.cancel') }} |
||||
</NcButton> |
||||
|
||||
<NcButton |
||||
key="submit" |
||||
autofocus |
||||
type="primary" |
||||
html-type="submit" |
||||
:loading="saving" |
||||
data-testid="nc-delete-modal-delete-btn" |
||||
@click="emit('submit')" |
||||
> |
||||
Update |
||||
<template #loading> Saving... </template> |
||||
</NcButton> |
||||
</div> |
||||
</div> |
||||
</GeneralModal> |
||||
</template> |
@ -0,0 +1,72 @@
|
||||
import { UITypes } from 'nocodb-sdk' |
||||
import { getCheckboxValue } from './parsers/parserHelpers' |
||||
|
||||
/* |
||||
* @param {string} str - string with numbers |
||||
* @returns {number} - number extracted from string |
||||
* |
||||
* @example abc123 -> 123 |
||||
* @example 12.3abc -> 12.3 |
||||
* @example 12.3.2 -> 12.32 |
||||
*/ |
||||
function extractNumbers(str: string): number { |
||||
const parts = str.replace(/[^\d.]/g, '').split('.') |
||||
if (parts.length > 1) parts[0] += '.' |
||||
|
||||
return parseFloat(parts.join('')) |
||||
} |
||||
|
||||
/* |
||||
* @param {string} value - string value of duration |
||||
* @returns {number} - duration in seconds |
||||
* |
||||
* @example 1:30:00 -> 5400 |
||||
* @example 1:30 -> 5400 |
||||
* @example 90 -> 90 |
||||
*/ |
||||
function toDuration(value: string) { |
||||
if (value.includes(':')) { |
||||
const [hours, minutes, seconds] = value.split(':').map((v) => parseInt(v) || 0) |
||||
return hours * 3600 + minutes * 60 + seconds |
||||
} |
||||
|
||||
return Math.floor(extractNumbers(value)) |
||||
} |
||||
|
||||
/* |
||||
* @param {string} value - string value to convert |
||||
* @param {string} type - type of the field to convert to |
||||
* @returns {number|string|boolean|array} - converted value |
||||
* |
||||
* @example convert('12.3', 'Number') -> 12 |
||||
* @example convert('1a23', 'SingleLineText') -> '1a23' |
||||
* @example convert('1', 'Checkbox') -> true |
||||
*/ |
||||
export function convert(value: string, type: string, limit = 100): unknown { |
||||
switch (type) { |
||||
case UITypes.SingleLineText: |
||||
case UITypes.SingleSelect: |
||||
case UITypes.LongText: |
||||
case UITypes.Email: |
||||
case UITypes.URL: |
||||
return value |
||||
case UITypes.Number: |
||||
return Math.floor(extractNumbers(value)) |
||||
case UITypes.Decimal: |
||||
case UITypes.Currency: |
||||
return extractNumbers(value) |
||||
case UITypes.Percent: |
||||
case UITypes.Rating: |
||||
return Math.min(limit, Math.max(0, extractNumbers(value))) |
||||
case UITypes.Checkbox: |
||||
return getCheckboxValue(value) |
||||
case UITypes.Date: |
||||
case UITypes.DateTime: |
||||
case UITypes.Time: |
||||
return new Date(value) |
||||
case UITypes.Duration: |
||||
return toDuration(value) |
||||
case UITypes.MultiSelect: |
||||
return value.split(',').map((v) => v.trim()) |
||||
} |
||||
} |
@ -0,0 +1,79 @@
|
||||
const MONTHS = [ |
||||
'January', |
||||
'February', |
||||
'March', |
||||
'April', |
||||
'May', |
||||
'June', |
||||
'July', |
||||
'August', |
||||
'September', |
||||
'October', |
||||
'November', |
||||
'December', |
||||
]; |
||||
const MONTHS_SHORT = MONTHS.map((m) => m.slice(0, 3)); |
||||
const MONTHS_SHORT_LOWER = MONTHS_SHORT.map((m) => m.toLowerCase()); |
||||
const MONTHS_SHORT_UPPER = MONTHS_SHORT.map((m) => m.toUpperCase()); |
||||
|
||||
const MONTHS_LOWER = MONTHS.map((m) => m.toLowerCase()); |
||||
const MONTHS_UPPER = MONTHS.map((m) => m.toUpperCase()); |
||||
|
||||
const MONTH_FORMATS = { |
||||
MM: '[0-9]{1,2}', |
||||
Mon: '(' + MONTHS_SHORT.join('|') + ')', |
||||
MON: '(' + MONTHS_SHORT_UPPER.join('|') + ')', |
||||
mon: '(' + MONTHS_SHORT_LOWER.join('|') + ')', |
||||
Month: '(' + MONTHS.join('|') + ')', |
||||
MONTH: '(' + MONTHS_UPPER.join('|') + ')', |
||||
month: '(' + MONTHS_LOWER.join('|') + ')', |
||||
}; |
||||
/* |
||||
* Map of date formats to their respective regex patterns. |
||||
*/ |
||||
export const DATE_FORMATS = { |
||||
ymd: Object.keys(MONTH_FORMATS).map((format) => [ |
||||
`Y-${format}-DD`, |
||||
`^\\d{1,4}[:\\- /]+${MONTH_FORMATS[format]}[:\\- /]+\\d{1,2}$`, |
||||
]), |
||||
dmy: Object.keys(MONTH_FORMATS).map((format) => [ |
||||
`DD-${format}-Y`, |
||||
`^\\d{1,2}[:\\- /]+${MONTH_FORMATS[format]}[:\\- /]+\\d{1,4}$`, |
||||
]), |
||||
mdy: Object.keys(MONTH_FORMATS).map((format) => [ |
||||
`${format}-DD-Y`, |
||||
`^${MONTH_FORMATS[format]}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,4}$`, |
||||
]), |
||||
empty: [['', '^.*$']], |
||||
}; |
||||
|
||||
/* |
||||
* Map of date time formats to their respective regex patterns. |
||||
*/ |
||||
export const TIME_FORMATS = [ |
||||
[ |
||||
'HH24:MI:SS:MS:US', |
||||
'^\\d{1,2}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,3}[:\\- /]+\\d*$', |
||||
], |
||||
[ |
||||
'HH24:MI:SS:MS', |
||||
'^\\d{1,2}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,3}$', |
||||
], |
||||
['HH24:MI:SS', '^\\d{1,2}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,2}$'], |
||||
['HH24:MI', '^\\d{1,2}[:\\- /]+\\d{1,2}$'], |
||||
['HH24', '^\\d{1,2}$'], |
||||
[ |
||||
'HH12:MI:SS:MS:US (AM|PM)', |
||||
'^\\d{1,2}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,3}[:\\- /]+\\d* (AM|PM)$', |
||||
], |
||||
[ |
||||
'HH12:MI:SS:MS (AM|PM)', |
||||
'^\\d{1,2}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,3} (AM|PM)$', |
||||
], |
||||
[ |
||||
'HH12:MI:SS (AM|PM)', |
||||
'^\\d{1,2}[:\\- /]+\\d{1,2}[:\\- /]+\\d{1,2} (AM|PM)$', |
||||
], |
||||
['HH12:MI (AM|PM)', '^\\d{1,2}[:\\- /]+\\d{1,2} (AM|PM)$'], |
||||
['HH12 (AM|PM)', '^\\d{1,2} (AM|PM)$'], |
||||
]; |
@ -0,0 +1,227 @@
|
||||
import { UITypes } from 'nocodb-sdk'; |
||||
import { DATE_FORMATS, TIME_FORMATS } from '~/db/sql-client/lib/pg/constants'; |
||||
|
||||
/* |
||||
* Generate query to extract number from a string. The number is extracted by |
||||
* removing all non-numeric characters from the string. Decimal point is allowed. |
||||
* If there are more than one decimal points, only the first one is considered, the rest are ignored. |
||||
* |
||||
* @param {String} source - source column name |
||||
* @returns {String} - query to extract number from a string |
||||
*/ |
||||
function extractNumberQuery(source: string) { |
||||
return ` |
||||
CAST( |
||||
NULLIF( |
||||
REPLACE( |
||||
REPLACE( |
||||
REGEXP_REPLACE( |
||||
REGEXP_REPLACE(${source}, '[^0-9.]', '', 'g'),
|
||||
'(\\d)\\.', '\\1-' |
||||
),
|
||||
'.', '' |
||||
),
|
||||
'-', '.' |
||||
), '' |
||||
) AS DECIMAL |
||||
) |
||||
`;
|
||||
} |
||||
|
||||
/* |
||||
* Generate query to cast a value to boolean. The boolean value is determined based on the given mappings. |
||||
* |
||||
* @param {String} columnName - Source column name |
||||
* @returns {String} - query to cast value to boolean |
||||
*/ |
||||
function generateBooleanCastQuery(columnName: string): string { |
||||
return ` |
||||
CASE |
||||
WHEN LOWER(${columnName}) IN ('checked', 'x', 'yes', 'y', '1', '[x]', '☑', '✅', '✓', '✔', 'enabled', 'on', 'done', 'true') THEN true |
||||
WHEN LOWER(${columnName}) IN ('unchecked', '', 'no', 'n', '0', '[]', '[ ]', 'disabled', 'off', 'false') THEN false |
||||
ELSE null |
||||
END; |
||||
`;
|
||||
} |
||||
|
||||
/* |
||||
* Generate query to cast a value to date time based on the given date and time formats. |
||||
* |
||||
* @param {String} source - Source column name |
||||
* @param {String} dateFormat - Date format |
||||
* @param {String} timeFormat - Time format |
||||
* @param {String} functionName - Function name to cast value to date time |
||||
* @returns {String} - query to cast value to date time |
||||
*/ |
||||
function generateDateTimeCastQuery(source: string, dateFormat: string) { |
||||
if (!(dateFormat in DATE_FORMATS)) { |
||||
throw new Error(`Invalid date format: ${dateFormat}`); |
||||
} |
||||
|
||||
const timeFormats = |
||||
dateFormat === 'empty' ? TIME_FORMATS : [...TIME_FORMATS, ['', '^$']]; |
||||
|
||||
const cases = DATE_FORMATS[dateFormat].map(([format, regex]) => |
||||
timeFormats |
||||
.map( |
||||
([timeFormat, timeRegex]) => |
||||
`WHEN ${source} ~ '${regex.slice(0, -1)}\\s*${timeRegex.slice( |
||||
1, |
||||
)}' THEN to_date_time_safe(${source}, '${format} ${timeFormat}')`,
|
||||
) |
||||
.join('\n'), |
||||
); |
||||
|
||||
return `CASE
|
||||
${cases.join('\n')} |
||||
ELSE NULL |
||||
END;`;
|
||||
} |
||||
|
||||
/* |
||||
* Generate SQL query to extract a number from a string and make out-of-bounds values NULL. |
||||
* |
||||
* @param {String} source - Source column name. |
||||
* @param {Number} minValue - Minimum allowed value. |
||||
* @param {Number} maxValue - Maximum allowed value. |
||||
* @returns {String} - SQL query to extract number and handle out-of-bounds values. |
||||
*/ |
||||
function generateNumberBoundingQuery( |
||||
source: string, |
||||
minValue: number, |
||||
maxValue: number, |
||||
) { |
||||
return ` |
||||
NULLIF( |
||||
NULLIF( |
||||
LEAST( |
||||
${maxValue + 1}, GREATEST(${minValue - 1}, ${source}) |
||||
), ${minValue - 1} |
||||
), ${maxValue + 1} |
||||
); |
||||
`;
|
||||
} |
||||
|
||||
/* |
||||
* Generate query to cast a value to duration. |
||||
* |
||||
* @param {String} source - Source column name |
||||
* @returns {String} - query to cast value to duration |
||||
*/ |
||||
function generateToDurationQuery(source: string) { |
||||
return ` |
||||
CASE |
||||
WHEN ${source} ~ '^\\d+:\\d{1,2}$' THEN 60 * CAST(SPLIT_PART(${source}, ':', 1) AS INT) + CAST(SPLIT_PART(${source}, ':', 2) AS INT) |
||||
ELSE ${extractNumberQuery(source)} |
||||
END; |
||||
`;
|
||||
} |
||||
|
||||
function getDateFormat(format: string) { |
||||
const y = format.indexOf('Y'); |
||||
const m = format.indexOf('M'); |
||||
const d = format.indexOf('D'); |
||||
|
||||
if (y < m) { |
||||
if (m < d) return 'ymd'; |
||||
else if (y < d) return 'ydm'; |
||||
else return 'dym'; |
||||
} else if (y < d) return 'myd'; |
||||
else if (m < d) return 'mdy'; |
||||
else return 'dmy'; |
||||
} |
||||
|
||||
/* |
||||
* Generate query to cast a column to a specific data type based on the UI data type. |
||||
* |
||||
* @param {UITypes} uidt - UI data type |
||||
* @param {String} dt - DB Data type |
||||
* @param {String} source - Source column name |
||||
* @param {Number} limit - Limit for the data type |
||||
* @param {String} dateFormat - Date format |
||||
* @param {String} timeFormat - Time format |
||||
* @returns {String} - query to cast column to a specific data type |
||||
*/ |
||||
export function generateCastQuery( |
||||
uidt: UITypes, |
||||
dt: string, |
||||
source: string, |
||||
limit: number, |
||||
format: string, |
||||
) { |
||||
switch (uidt) { |
||||
case UITypes.SingleLineText: |
||||
case UITypes.MultiSelect: |
||||
case UITypes.SingleSelect: |
||||
case UITypes.Email: |
||||
case UITypes.PhoneNumber: |
||||
case UITypes.URL: |
||||
return `${source}::VARCHAR(${limit || 255});`; |
||||
case UITypes.LongText: |
||||
return `${source}::TEXT;`; |
||||
case UITypes.Number: |
||||
return `CAST(${extractNumberQuery(source)} AS BIGINT);`; |
||||
case UITypes.Year: |
||||
return generateNumberBoundingQuery( |
||||
extractNumberQuery(source), |
||||
1000, |
||||
9999, |
||||
); |
||||
case UITypes.Decimal: |
||||
case UITypes.Currency: |
||||
return `${extractNumberQuery(source)};`; |
||||
case UITypes.Percent: |
||||
return `LEAST(100, GREATEST(0, ${extractNumberQuery(source)}));`; |
||||
case UITypes.Rating: |
||||
return `LEAST(${limit || 5}, GREATEST(0, ${extractNumberQuery( |
||||
source, |
||||
)}));`;
|
||||
case UITypes.Checkbox: |
||||
return generateBooleanCastQuery(source); |
||||
case UITypes.Date: |
||||
return `CAST(${generateDateTimeCastQuery( |
||||
source, |
||||
getDateFormat(format), |
||||
).slice(0, -1)} AS DATE);`;
|
||||
case UITypes.DateTime: |
||||
return generateDateTimeCastQuery(source, getDateFormat(format)); |
||||
case UITypes.Time: |
||||
return generateDateTimeCastQuery(source, 'empty'); |
||||
case UITypes.Duration: |
||||
return generateToDurationQuery(source); |
||||
default: |
||||
return `null::${dt};`; |
||||
} |
||||
} |
||||
|
||||
/* |
||||
* Generate query to format a column based on the UI data type. |
||||
* |
||||
* @param {String} columnName - Column name |
||||
* @param {UITypes} uiDataType - UI data type |
||||
* @returns {String} - query to format a column |
||||
*/ |
||||
export function formatColumn(columnName: string, uiDataType: UITypes) { |
||||
switch (uiDataType) { |
||||
case UITypes.LongText: |
||||
case UITypes.SingleLineText: |
||||
case UITypes.MultiSelect: |
||||
case UITypes.Email: |
||||
case UITypes.URL: |
||||
case UITypes.SingleSelect: |
||||
case UITypes.PhoneNumber: |
||||
return columnName; |
||||
case UITypes.Number: |
||||
case UITypes.Decimal: |
||||
case UITypes.Currency: |
||||
case UITypes.Percent: |
||||
case UITypes.Rating: |
||||
case UITypes.Duration: |
||||
case UITypes.Year: |
||||
return `CAST(${columnName} AS VARCHAR(255))`; |
||||
case UITypes.Checkbox: |
||||
return `CAST(CASE WHEN ${columnName} THEN '1' ELSE '0' END AS TEXT)`; |
||||
default: |
||||
return `CAST(${columnName} AS TEXT)`; |
||||
} |
||||
} |
@ -0,0 +1,133 @@
|
||||
import 'mocha'; |
||||
import { UITypes } from 'nocodb-sdk'; |
||||
import { expect } from 'chai'; |
||||
import init from '../../init'; |
||||
import { createProject } from '../../factory/base'; |
||||
import { createTable } from '../../factory/table'; |
||||
import { createBulkRows, rowMixedValue } from '../../factory/row'; |
||||
import { updateColumn } from '../../factory/column'; |
||||
import type Model from '../../../../src/models/Model'; |
||||
import type Base from '../../../../src/models/Base'; |
||||
import type Column from '../../../../src/models/Column'; |
||||
|
||||
const TEST_TYPES = [ |
||||
UITypes.LongText, |
||||
UITypes.Attachment, |
||||
UITypes.Checkbox, |
||||
UITypes.MultiSelect, |
||||
UITypes.SingleSelect, |
||||
UITypes.Date, |
||||
UITypes.Year, |
||||
UITypes.Time, |
||||
UITypes.PhoneNumber, |
||||
UITypes.GeoData, |
||||
UITypes.Email, |
||||
UITypes.URL, |
||||
UITypes.Number, |
||||
UITypes.Decimal, |
||||
UITypes.Currency, |
||||
UITypes.Percent, |
||||
UITypes.Duration, |
||||
UITypes.Rating, |
||||
UITypes.Count, |
||||
UITypes.DateTime, |
||||
UITypes.Geometry, |
||||
UITypes.JSON, |
||||
UITypes.User, |
||||
]; |
||||
|
||||
async function setup(context, base: Base, type: UITypes) { |
||||
const table = await createTable(context, base, { |
||||
table_name: 'sampleTable', |
||||
title: 'sampleTable', |
||||
columns: [ |
||||
{ |
||||
column_name: 'Test', |
||||
title: 'Test', |
||||
uidt: type, |
||||
}, |
||||
], |
||||
}); |
||||
|
||||
const column = (await table.getColumns({ |
||||
workspace_id: base.fk_workspace_id, |
||||
base_id: base.id, |
||||
}))[0]; |
||||
|
||||
const rowAttributes = []; |
||||
for (let i = 0; i < 100; i++) { |
||||
const row = { |
||||
Title: rowMixedValue(column, i), |
||||
}; |
||||
rowAttributes.push(row); |
||||
} |
||||
|
||||
await createBulkRows(context, { |
||||
base, |
||||
table, |
||||
values: rowAttributes, |
||||
}); |
||||
|
||||
return { table, column }; |
||||
} |
||||
|
||||
function typeCastTests() { |
||||
let context; |
||||
let base: Base; |
||||
|
||||
beforeEach(async function () { |
||||
context = await init(); |
||||
base = await createProject(context); |
||||
}); |
||||
|
||||
describe('Single Line Text to ', () => { |
||||
let table: Model; |
||||
let column: Column; |
||||
|
||||
beforeEach(async function () { |
||||
const data = await setup(context, base, UITypes.SingleLineText); |
||||
table = data.table; |
||||
column = data.column; |
||||
}); |
||||
|
||||
for (const type of TEST_TYPES) |
||||
it(type, async () => { |
||||
if (context.dbConfig.client !== 'pg') return; |
||||
|
||||
const updatedColumn = await updateColumn(context, { |
||||
table, |
||||
column: column, |
||||
attr: { |
||||
...column, |
||||
uidt: type, |
||||
}, |
||||
}); |
||||
|
||||
expect(updatedColumn.uidt).to.equal(type); |
||||
}); |
||||
}); |
||||
|
||||
describe('Convert to Single Line Text from ', () => { |
||||
for (const type of TEST_TYPES) |
||||
it(type, async () => { |
||||
if (context.dbConfig.client !== 'pg') return; |
||||
|
||||
const data = await setup(context, base, type); |
||||
|
||||
const updatedColumn = await updateColumn(context, { |
||||
table: data.table, |
||||
column: data.column, |
||||
attr: { |
||||
...data.column, |
||||
uidt: UITypes.SingleLineText, |
||||
}, |
||||
}); |
||||
|
||||
expect(updatedColumn.uidt).to.equal(UITypes.SingleLineText); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
export default function () { |
||||
describe('Field type conversion ', typeCastTests); |
||||
} |
Loading…
Reference in new issue