mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-13 19:33:55 +03:00
console: allow manual edit of column types and handle array data types (close #2544, #3335, #2583) (#4546)
This commit is contained in:
parent
268aa46df2
commit
f2428e3984
@ -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`
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)');
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
|
17
console/package-lock.json
generated
17
console/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
@ -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;
|
@ -1,5 +1,6 @@
|
||||
import 'react-toggle/style.css';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import './Toggle.css';
|
||||
|
||||
export default Toggle;
|
||||
|
@ -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 = '') {
|
||||
|
@ -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) => {
|
||||
|
@ -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}`}>
|
||||
|
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user