console: allow manual edit of column types and handle array data types (close #2544, #3335, #2583) (#4546)

This commit is contained in:
Aleksandra Sikora 2020-06-18 12:43:19 +02:00 committed by GitHub
parent 268aa46df2
commit f2428e3984
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 344 additions and 163 deletions

View File

@ -7,6 +7,7 @@
(Add entries here in the order of: server, console, cli, docs, others)
- console: allow manual edit of column types and handle array data types (close #2544, #3335, #2583) (#4546)
## `v1.3.0-beta.2`

View File

@ -126,6 +126,8 @@
"react/jsx-filename-extension": 0,
"no-unused-vars": "off",
"camelcase": "off",
"no-param-reassign": "off",
"consistent-return": "off",
"@typescript-eslint/no-unused-vars": 2,
"@typescript-eslint/indent": 0,
"@typescript-eslint/explicit-function-return-type": 0,

View File

@ -55,9 +55,7 @@ const clickSaveOrInsert = (firstIndex: number, currentIndex: number) => {
const checkQuerySuccess = () => {
// Expect only 4 rows i.e. expect fifth element to not exist
cy.get('[role=gridcell]').contains('filter-text');
cy.get('[role=row]')
.eq(2)
.should('not.exist');
cy.get('[role=row]').eq(2).should('not.exist');
};
const checkOrder = (order: string) => {
@ -178,11 +176,7 @@ export const passBIInsert20Rows = () => {
);
cy.get(getElementFromAlias(`typed-input-${textIndex}`))
.type('{selectall}{del}')
.type(
Math.random()
.toString(36)
.substring(7)
);
.type(Math.random().toString(36).substring(7));
cy.get(
getElementFromAlias(`typed-input-default-${textIndex + 1}`)
).check();
@ -266,9 +260,7 @@ export const passBIFilterQueryEq = () => {
// Select operator as `eq`
cy.get(getElementFromAlias('filter-op-0')).select('$eq');
// Type value as "filter-text"
cy.get("input[placeholder='-- value --']")
.last()
.type('filter-text');
cy.get("input[placeholder='-- value --']").last().type('filter-text');
// Run query
cy.get(getElementFromAlias('run-query')).click();
cy.wait(2000);
@ -291,9 +283,7 @@ export const deleteBITestTable = () => {
// Click on delete
cy.get(getElementFromAlias('delete-table')).click();
// Confirm
cy.window()
.its('prompt')
.should('be.called');
cy.window().its('prompt').should('be.called');
cy.wait(7000);
// Match the URL
cy.url().should('eq', `${baseUrl}/data/schema/public`);
@ -307,9 +297,7 @@ export const deleteBITestTable = () => {
// Click on delete
cy.get(getElementFromAlias('delete-table')).click();
// Confirm
cy.window()
.its('prompt')
.should('be.called');
cy.window().its('prompt').should('be.called');
cy.wait(7000);
// Match the URL
cy.url().should('eq', `${baseUrl}/data/schema/public`);
@ -323,9 +311,7 @@ export const deleteBITestTable = () => {
// Click on delete
cy.get(getElementFromAlias('delete-table')).click();
// Confirm
cy.window()
.its('prompt')
.should('be.called');
cy.window().its('prompt').should('be.called');
cy.wait(7000);
// Match the URL
@ -435,9 +421,7 @@ export const checkViewRelationship = () => {
// Add relationship
cy.get(getElementFromAlias('table-relationships')).click();
cy.get(getElementFromAlias('obj-rel-add-0')).click();
cy.get(getElementFromAlias('suggested-rel-name'))
.clear()
.type('someRel');
cy.get(getElementFromAlias('suggested-rel-name')).clear().type('someRel');
cy.get(getElementFromAlias('obj-rel-save-0')).click();
cy.wait(2000);
// Insert a row
@ -448,15 +432,10 @@ export const checkViewRelationship = () => {
cy.get(getElementFromAlias('table-browse-rows')).click();
cy.wait(1000);
cy.get('.rt-table').within(() => {
cy.get('a')
.contains('View')
.click();
cy.get('a').contains('View').click();
cy.wait(1000);
});
cy.get('a')
.contains('Close')
.first()
.click();
cy.get('a').contains('Close').first().click();
};
export const passDeleteRow = () => {
@ -506,3 +485,27 @@ export const passBulkDeleteAllRows = () => {
cy.get(getElementFromAlias('table-browse-rows')).contains('(8)');
cy.wait(14000);
};
export const passArrayDataType = () => {
// create new column
cy.get(getElementFromAlias('table-modify')).click();
cy.wait(1000);
cy.get(getElementFromAlias('column-name')).type('array_column');
cy.get(getElementFromAlias('col-type-0'))
.children('div')
.click()
.find('input')
.type('text[]', { force: true });
cy.get(getElementFromAlias('add-column-button')).click();
// insert new row
cy.get(getElementFromAlias('table-insert-rows')).click();
cy.wait(1000);
cy.get(getElementFromAlias('typed-input-11')).type('["a", "b"]');
cy.get(getElementFromAlias('insert-save-button')).click();
// go to browse rows and check if row was added
cy.get(getElementFromAlias('table-browse-rows')).click();
cy.wait(1000);
cy.get(getElementFromAlias('table-browse-rows')).contains('(9)');
};

View File

@ -19,6 +19,7 @@ import {
passDeleteRow,
passBulkDeleteRows,
passBulkDeleteAllRows,
passArrayDataType,
} from './spec';
import { setMetaData } from '../../validators/validators';
@ -52,6 +53,7 @@ export const runInsertBrowseTests = () => {
it('Delete the row', passDeleteRow);
it('Bulk delete rows', passBulkDeleteRows);
it('Bulk delete all rows', passBulkDeleteAllRows);
it('Handle array data types', passArrayDataType);
it('Check view relationship', checkViewRelationship);
it('Delete test table', deleteBITestTable);
});

View File

@ -3298,9 +3298,9 @@
}
},
"@types/react-router": {
"version": "3.0.22",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-3.0.22.tgz",
"integrity": "sha512-MUjQtRC4vS+rTqQKag3r0Pf6wSy0Y2h/hfk4OE03STgNmtRWGfyjckHLHgESbdqz2VnL12xS6pdncsCDiudC2g==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-3.0.8.tgz",
"integrity": "sha1-zsDyuQt81TBaBIGotWURUoR5bLg=",
"dev": true,
"requires": {
"@types/history": "^3",
@ -3331,6 +3331,17 @@
}
}
},
"@types/react-select": {
"version": "3.0.12",
"resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.0.12.tgz",
"integrity": "sha512-3NVEc1sbaNtI1b06smzr9dlNKTkYWttL27CdEsorMvd2EgTOM/PJmrzkClaVQmBDg52MzQO05xVwNZruEUKpHw==",
"dev": true,
"requires": {
"@types/react": "*",
"@types/react-dom": "*",
"@types/react-transition-group": "*"
}
},
"@types/react-table": {
"version": "6.8.7",
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.7.tgz",

View File

@ -124,8 +124,9 @@
"@types/react-hot-loader": "4.1.1",
"@types/react-notification-system-redux": "1.1.6",
"@types/react-redux": "7.1.7",
"@types/react-router": "3.0.22",
"@types/react-router": "3.0.8",
"@types/react-router-redux": "4.0.44",
"@types/react-select": "3.0.12",
"@types/react-toggle": "4.0.2",
"@types/redux-devtools": "3.0.47",
"@types/redux-devtools-dock-monitor": "1.1.33",

View File

@ -1,103 +0,0 @@
import React from 'react';
import Select, { components, createFilter } from 'react-select';
import PropTypes from 'prop-types';
import { isArray, isObject } from '../utils/jsUtils';
/*
* Wrap the option generated by react-select and adds utility properties
* */
const CustomOption = props => {
return (
<div
title={props.data.description || ''}
data-test={`data_test_column_type_value_${props.data.value}`}
>
<components.Option {...props} />
</div>
);
};
const getPrefixFilter = () => {
const prefixFilterOptions = {
matchFrom: 'start',
};
return createFilter(prefixFilterOptions);
};
/*
* Searchable select box component
* 1) options: Accepts options
* 2) value: selectedValue
* 3) onChange: function to call on change of value
* 4) bsClass: Wrapper class
* 5) customStyle: Custom style
* */
const SearchableSelectBox = ({
options,
onChange,
value,
bsClass,
styleOverrides,
placeholder,
filterOption,
}) => {
/* Select element style customization */
const customStyles = {};
if (styleOverrides) {
Object.keys(styleOverrides).forEach(comp => {
customStyles[comp] = provided => {
return {
...provided,
...styleOverrides[comp],
};
};
});
}
let customFilter;
switch (filterOption) {
case 'prefix':
customFilter = getPrefixFilter();
break;
default:
customFilter = {};
}
// handle simple options
if (isArray(options) && !isObject(options[0])) {
options = options.map(op => {
return { value: op, label: op };
});
}
if (value && !isObject(value)) {
value = { value: value, label: value };
}
return (
<Select
isSearchable
components={{ Option: CustomOption }}
classNamePrefix={`${bsClass}`}
placeholder={placeholder}
options={options}
onChange={onChange}
value={value}
styles={customStyles}
filterOption={customFilter}
/>
);
};
SearchableSelectBox.propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
options: PropTypes.array.isRequired,
bsClass: PropTypes.string,
customStyle: PropTypes.object,
filterOption: PropTypes.object,
};
export default SearchableSelectBox;

View File

@ -0,0 +1,129 @@
import React, { ReactText, useState, useMemo } from 'react';
import Select, {
components,
createFilter,
OptionProps,
OptionTypeBase,
ValueType,
} from 'react-select';
import { isArray, isObject } from '../utils/jsUtils';
const { Option } = components;
const CustomOption: React.FC<OptionProps<OptionTypeBase>> = props => {
return (
<div
title={props.data.description || ''}
data-test={`data_test_column_type_value_${props.data.value}`}
>
<Option {...props} />
</div>
);
};
type Option = { label: string; value: string };
export interface SearchableSelectProps {
options: OptionTypeBase | ReactText[];
onChange: (value: ValueType<OptionTypeBase> | string) => void;
value?: Option | string;
bsClass?: string;
styleOverrides?: Record<PropertyKey, any>;
placeholder: string;
filterOption: 'prefix' | 'fulltext';
isCreatable?: boolean;
createNewOption?: (v: string) => ValueType<OptionTypeBase>;
}
const SearchableSelect: React.FC<SearchableSelectProps> = ({
options,
onChange,
value,
bsClass,
styleOverrides,
placeholder,
filterOption,
isCreatable,
createNewOption,
}) => {
const [searchValue, setSearchValue] = useState<string | null>(null);
const [isFocused, setIsFocused] = useState(false);
const inputValue = useMemo(() => {
// if input is not focused we don't want to show inputValue
if (!isFocused) return;
// if user is searching we don't want to control the input
if (searchValue !== null) return;
// otherwise we display last selected option
// this way typing after selecting an options is allowed
return typeof value === 'string' ? value : value?.label;
}, [searchValue, value, isFocused]);
const onMenuClose = () => {
if (searchValue && createNewOption) onChange(createNewOption(searchValue));
setSearchValue(null);
};
const customStyles: Record<string, any> = {};
if (styleOverrides) {
Object.keys(styleOverrides).forEach(comp => {
customStyles[comp] = (provided: object) => ({
...provided,
...styleOverrides[comp],
});
});
}
let customFilter;
switch (filterOption) {
case 'prefix':
customFilter = createFilter({ matchFrom: 'start' });
break;
case 'fulltext':
customFilter = createFilter({ matchFrom: 'any' });
break;
default:
customFilter = null;
}
// handle simple options
if (isArray(options) && !isObject((options as unknown[])[0])) {
options = options.map((op: string) => {
return { value: op, label: op };
});
}
if (value && !isObject(value)) {
value = { value: value as string, label: value as string };
}
if (isCreatable && createNewOption) {
if (searchValue) {
options = [createNewOption(searchValue), ...(options as Option[])];
}
}
return (
<Select
isSearchable
blurInputOnSelect
components={{ Option: CustomOption }}
classNamePrefix={bsClass}
placeholder={placeholder}
options={options as Option[]}
onChange={onChange}
value={value as Option}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
inputValue={inputValue}
onInputChange={s => setSearchValue(s)}
onMenuClose={onMenuClose}
styles={customStyles}
filterOption={searchValue ? customFilter : null}
/>
);
};
export default SearchableSelect;

View File

@ -1,5 +1,6 @@
import 'react-toggle/style.css';
import Toggle from 'react-toggle';
import './Toggle.css';
export default Toggle;

View File

@ -135,6 +135,15 @@ export const arrayDiff = (arr1: unknown[], arr2: unknown[]) => {
return arr1.filter(v => !arr2.includes(v));
};
export const isStringArray = (str: string): boolean => {
try {
const arr = JSON.parse(str);
return Array.isArray(arr);
} catch {
return false;
}
};
/* JSON utils */
export function getAllJsonPaths(json: any, leafKeys: any[], prefix = '') {

View File

@ -291,6 +291,10 @@ export const isColumnAutoIncrement = (column: TableColumn) => {
);
};
export const arrayToPostgresArray = (arr: unknown[]) => {
return `{${arr.join(',')}}`;
};
/** * Table/View relationship utils ** */
export const getTableRelationships = (table: Table) => {

View File

@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import SearchableSelectBox from '../../../Common/SearchableSelect/SearchableSelect';
import { commonDataTypes } from '../utils';
import { getDataOptions, inferDefaultValues } from '../Common/utils';
import TableColumnDefault from './TableColumnDefault';
import { ColumnTypeSelector } from '../Common/Components/ColumnTypeSelector';
/* Custom style object for searchable select box */
const customSelectBoxStyles = {
@ -99,14 +99,15 @@ const TableColumn = props => {
className={`${styles.inputDefault} ${styles.defaultWidth}`}
data-test={`col-type-${i}`}
>
<SearchableSelectBox
<ColumnTypeSelector
options={columnDataTypes}
onChange={handleColTypeChange}
value={column.type && columnTypeValueMap[column.type]}
value={
(column.type && columnTypeValueMap[column.type]) || column.type
}
colIdentifier={i}
bsClass={`col-type-${i} add_table_column_selector`}
styleOverrides={customSelectBoxStyles}
filterOption={'prefix'}
placeholder="column_type"
/>
</span>
<span className={`${styles.inputDefault} ${styles.defaultWidth}`}>

View File

@ -0,0 +1,52 @@
import React, { useCallback } from 'react';
import SearchableSelect from '../../../../Common/SearchableSelect/SearchableSelect';
type Option = {
value: string;
label: string;
description?: string;
key: number;
colIdentifier: number;
};
export interface ColumnTypeSelectorProps {
options: Array<{ label: string; options?: Option[] }>;
onChange: (option: any) => void;
value: Option | string;
bsClass: string;
styleOverrides: Record<string, any>;
colIdentifier: number;
}
export const ColumnTypeSelector: React.FC<ColumnTypeSelectorProps> = ({
options,
onChange,
value,
bsClass,
styleOverrides,
colIdentifier,
}) => {
const createOpt = useCallback(
(prevValue: string) => ({
value: prevValue,
label: prevValue,
colIdentifier,
}),
[colIdentifier]
);
return (
<SearchableSelect
isCreatable
createNewOption={createOpt}
options={options}
onChange={onChange}
value={value}
bsClass={bsClass}
styleOverrides={styleOverrides}
filterOption="prefix"
placeholder="column_type"
/>
);
};

View File

@ -12,8 +12,11 @@ import {
getColumnType,
getTableColumn,
getEnumColumnMappings,
arrayToPostgresArray,
} from '../../../Common/utils/pgUtils';
import { getEnumOptionsQuery } from '../../../Common/utils/v1QueryUtils';
import { ARRAY } from '../utils';
import { isStringArray } from '../../../Common/utils/jsUtils';
const E_SET_EDITITEM = 'EditItem/E_SET_EDITITEM';
const E_ONGOING_REQ = 'EditItem/E_ONGOING_REQ';
@ -79,6 +82,14 @@ const editItem = (tableName, colValues) => {
colValue +
' as a valid JSON object/array';
}
} else if (colType === ARRAY && isStringArray(colValue)) {
try {
const arr = JSON.parse(colValue);
_setObject[colName] = arrayToPostgresArray(arr);
} catch {
errorMessage =
colName + ' :: could not read ' + colValue + ' as a valid array';
}
} else {
_setObject[colName] = colValue;
}

View File

@ -46,6 +46,7 @@ import {
getRelationshipRefTable,
getTableName,
getTableSchema,
arrayToPostgresArray,
} from '../../../Common/utils/pgUtils';
import { updateSchemaInfo } from '../DataActions';
import {
@ -235,6 +236,12 @@ const ViewRows = ({
});
}
Object.keys(pkClause).forEach(key => {
if (Array.isArray(pkClause[key])) {
pkClause[key] = arrayToPostgresArray(pkClause[key]);
}
});
return pkClause;
};

View File

@ -7,8 +7,13 @@ import {
showSuccessNotification,
} from '../../Common/Notification';
import dataHeaders from '../Common/Headers';
import { getEnumColumnMappings } from '../../../Common/utils/pgUtils';
import {
getEnumColumnMappings,
arrayToPostgresArray,
} from '../../../Common/utils/pgUtils';
import { getEnumOptionsQuery } from '../../../Common/utils/v1QueryUtils';
import { ARRAY } from '../utils';
import { isStringArray } from '../../../Common/utils/jsUtils';
const I_SET_CLONE = 'InsertItem/I_SET_CLONE';
const I_RESET = 'InsertItem/I_RESET';
@ -62,6 +67,17 @@ const insertItem = (tableName, colValues) => {
' as a valid JSON object/array';
error = true;
}
} else if (colType === ARRAY && isStringArray(colValues[colName])) {
try {
const arr = JSON.parse(colValues[colName]);
insertObject[colName] = arrayToPostgresArray(arr);
} catch {
errorMessage =
colName +
' :: could not read ' +
colValues[colName] +
' as a valid array';
}
} else {
insertObject[colName] = colValues[colName];
}

View File

@ -3,7 +3,6 @@ import { showErrorNotification } from '../../Common/Notification';
import gqlPattern, { gqlColumnErrorNotif } from '../Common/GraphQLValidation';
import { commonDataTypes } from '../utils';
import SearchableSelectBox from '../../../Common/SearchableSelect/SearchableSelect';
import CustomInputAutoSuggest from '../../../Common/CustomInputAutoSuggest/CustomInputAutoSuggest';
import {
@ -17,6 +16,7 @@ import { addColSql } from '../TableModify/ModifyActions';
import styles from './ModifyTable.scss';
import FrequentlyUsedColumnSelector from '../Common/Components/FrequentlyUsedColumnSelector';
import { ColumnTypeSelector } from '../Common/Components/ColumnTypeSelector';
const useColumnEditor = (dispatch, tableName) => {
const initialState = {
@ -178,15 +178,14 @@ const ColumnCreator = ({
};
return (
<span className={`${styles.select}`} data-test="col-type-0">
<SearchableSelectBox
<span className={styles.select} data-test="col-type-0">
<ColumnTypeSelector
options={columnDataTypes}
onChange={colType.onChange}
value={colType.value && columnTypeValueMap[colType.value]}
value={columnTypeValueMap[colType.value] || colType.value}
colIdentifier={0}
bsClass={`col-type-${0} modify_select`}
styleOverrides={customSelectBoxStyles}
filterOption={'prefix'}
placeholder="column_type"
/>
</span>
);

View File

@ -1,10 +1,11 @@
import React from 'react';
import SearchableSelectBox from '../../../Common/SearchableSelect/SearchableSelect';
import CustomInputAutoSuggest from '../../../Common/CustomInputAutoSuggest/CustomInputAutoSuggest';
import { getValidAlterOptions } from './utils';
import { getValidAlterOptions, convertToArrayOptions } from './utils';
import Tooltip from '../../../Common/Tooltip/Tooltip';
import { ColumnTypeSelector } from '../Common/Components/ColumnTypeSelector';
import { ARRAY } from '../utils';
const ColumnEditor = ({
onSubmit,
@ -15,7 +16,7 @@ const ColumnEditor = ({
alterTypeOptions,
defaultOptions,
}) => {
const colName = columnProperties.name;
const { name: colName, isArrayDataType } = columnProperties;
if (!selectedProperties[colName]) {
return null;
@ -30,7 +31,10 @@ const ColumnEditor = ({
selectedProperties[colName].type
);
};
const columnTypePG = getColumnType();
let columnTypePG = getColumnType();
if (columnProperties.display_type_name === ARRAY) {
columnTypePG = columnTypePG.replace('_', '') + '[]';
}
const customSelectBoxStyles = {
dropdownIndicator: {
@ -47,11 +51,16 @@ const ColumnEditor = ({
},
};
const { alterOptions, alterOptionsValueMap } = getValidAlterOptions(
// eslint-disable-next-line prefer-const
let { alterOptions, alterOptionsValueMap } = getValidAlterOptions(
alterTypeOptions,
colName
);
if (isArrayDataType) {
alterOptions = convertToArrayOptions(alterOptions);
}
const updateColumnName = e => {
dispatch(editColumn(colName, 'name', e.target.value));
};
@ -134,14 +143,13 @@ const ColumnEditor = ({
<div className={`${styles.display_flex} form-group`}>
<label className={'col-xs-4'}>Type</label>
<div className="col-xs-6">
<SearchableSelectBox
<ColumnTypeSelector
options={alterOptions}
onChange={updateColumnType}
value={columnTypePG && alterOptionsValueMap[columnTypePG]}
value={alterOptionsValueMap[columnTypePG] || columnTypePG}
colIdentifier={0}
bsClass={`col-type-${0} modify_select`}
styleOverrides={customSelectBoxStyles}
filterOption={'prefix'}
placeholder="column_type"
/>
</div>
</div>

View File

@ -10,7 +10,7 @@ import {
editColumn,
isColumnUnique,
} from '../TableModify/ModifyActions';
import { ordinalColSort } from '../utils';
import { ordinalColSort, ARRAY } from '../utils';
import { defaultDataTypeToCast } from '../constants';
import {
@ -59,14 +59,28 @@ const ColumnEditorList = ({
* */
return columns.map((col, i) => {
const colName = col.column_name;
const isArrayDataType = col.data_type === ARRAY;
const getDisplayName = () => {
if (isArrayDataType) {
return col.udt_name.replace('_', '') + '[]';
}
if (col.data_type === 'USER-DEFINED') {
return col.udt_name;
}
return col.data_type;
};
const getType = () =>
isArrayDataType ? col.udt_name.replace('_', '') + '[]' : col.udt_name;
const columnProperties = {
name: colName,
tableName: col.table_name,
schemaName: col.table_schema,
display_type_name:
col.data_type !== 'USER-DEFINED' ? col.data_type : col.udt_name,
type: col.udt_name,
display_type_name: getDisplayName(),
type: getType(),
isArrayDataType,
isNullable: col.is_nullable === 'YES',
isIdentity: col.is_identity === 'YES',
pkConstraint: columnPKConstraints[colName],
@ -172,6 +186,9 @@ const ColumnEditorList = ({
* */
const getValidTypeCasts = udtName => {
if (isArrayDataType) {
udtName = udtName.replace('_', '');
}
const lowerUdtName = udtName.toLowerCase();
if (lowerUdtName in validTypeCasts) {
return validTypeCasts[lowerUdtName];
@ -215,7 +232,7 @@ const ColumnEditorList = ({
const colEditorExpanded = () => {
return (
<ColumnEditor
alterTypeOptions={getValidTypeCasts(col.udt_name)}
alterTypeOptions={getValidTypeCasts(col.udt_name, isArrayDataType)}
defaultOptions={getValidDefaultTypes(col.udt_name)}
column={col}
onSubmit={onSubmit}

View File

@ -42,6 +42,13 @@ const getValidAlterOptions = (alterTypeOptions, colName) => {
};
};
export const convertToArrayOptions = options => {
return options.map(opt => ({
value: opt.value + '[]',
label: opt.label + '[]',
}));
};
const fetchColumnCastsQuery = `
SELECT ts.typname AS "Source Type",
pg_catalog.format_type(castsource, NULL) AS "Source Info",

View File

@ -20,6 +20,7 @@ export const DATE = 'date';
export const TIMETZ = 'timetz';
export const BOOLEAN = 'boolean';
export const TEXT = 'text';
export const ARRAY = 'ARRAY';
export const getPlaceholder = type => {
switch (type) {
@ -36,6 +37,8 @@ export const getPlaceholder = type => {
return '{"name": "foo"} or [12, "bar"]';
case BOOLEAN:
return '';
case ARRAY:
return '{"foo", "bar"} or ["foo", "bar"]';
default:
return type;
}