diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..df6f38ae13 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +**/tools/templates/crud/client-react/graphql/* +**/tools/templates/crud/server-ts/*.graphql \ No newline at end of file diff --git a/modules/core/client-react/crud/EditView.jsx b/modules/core/client-react/crud/EditView.jsx new file mode 100644 index 0000000000..d930f33cf4 --- /dev/null +++ b/modules/core/client-react/crud/EditView.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, Text, View } from 'react-native'; + +import FormView from './FormView'; + +const EditView = ({ loading, data, navigation, schema, updateEntry, createEntry }) => { + let dataObj = data; + + if (!dataObj && navigation.state) { + dataObj = navigation.state.params.data; + } + + if (loading && !dataObj) { + return ( + + Loading... + + ); + } else { + return ( + + ); + } +}; + +EditView.propTypes = { + loading: PropTypes.bool.isRequired, + data: PropTypes.object, + navigation: PropTypes.object.isRequired +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + alignItems: 'center', + justifyContent: 'center' + } +}); + +export default EditView; diff --git a/modules/core/client-react/crud/EditView.web.jsx b/modules/core/client-react/crud/EditView.web.jsx new file mode 100644 index 0000000000..2fa9c09270 --- /dev/null +++ b/modules/core/client-react/crud/EditView.web.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Helmet from 'react-helmet'; +import { Link } from 'react-router-dom'; + +import { PageLayout, Button } from '@gqlapp/look-client-react'; +import FormView from './FormView'; +import settings from '../../../../settings'; + +class EditView extends React.PureComponent { + static propTypes = { + loading: PropTypes.bool.isRequired, + data: PropTypes.object, + title: PropTypes.string.isRequired, + link: PropTypes.string.isRequired + }; + + renderMetaData = title => ( + + ); + + render() { + const { loading, data, title, link } = this.props; + + if (loading && !data) { + return ( + + {this.renderMetaData(title)} +
Loading...
+
+ ); + } else { + return ( + + {this.renderMetaData(title)} + + + +

+ {data ? 'Edit' : 'Create'} {title} +

+ +
+ ); + } + } +} + +export default EditView; diff --git a/modules/core/client-react/crud/FilterView.jsx b/modules/core/client-react/crud/FilterView.jsx new file mode 100644 index 0000000000..2d1ec23827 --- /dev/null +++ b/modules/core/client-react/crud/FilterView.jsx @@ -0,0 +1 @@ +export default () => {}; diff --git a/modules/core/client-react/crud/FilterView.web.jsx b/modules/core/client-react/crud/FilterView.web.jsx new file mode 100644 index 0000000000..a57fde5ce3 --- /dev/null +++ b/modules/core/client-react/crud/FilterView.web.jsx @@ -0,0 +1,123 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Formik } from 'formik'; +import { DebounceInput } from 'react-debounce-input'; +import { Form, FormItem, Input, Row, Col, Button, Icon } from '@gqlapp/look-client-react'; +import { createFormFields } from '@gqlapp/core-client-react'; +import { mapFormPropsToValues, pickInputFields } from '@gqlapp/core-common'; + +const { hasRole } = require('@gqlapp/user-client-react'); + +const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 8 } + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 } + } +}; + +class FilterView extends React.PureComponent { + static propTypes = { + schema: PropTypes.object.isRequired, + searchText: PropTypes.string, + onFilterChange: PropTypes.func.isRequired, + customFields: PropTypes.object, + currentUser: PropTypes.object, + currentUserLoading: PropTypes.bool, + data: PropTypes.object + }; + + state = { + expand: false + }; + + toggle = () => { + const { expand } = this.state; + this.setState({ expand: !expand }); + }; + + handleSearch = e => { + const { onFilterChange } = this.props; + onFilterChange({ searchText: e.target.value }); + }; + + render() { + const { schema, onFilterChange, customFields, currentUser, data } = this.props; + const { expand } = this.state; + + const showFilter = + customFields === null + ? false + : customFields && customFields.role + ? !!hasRole(customFields.role, currentUser) + : true; + + if (!showFilter) { + return null; + } + + return ( + { + onFilterChange(pickInputFields({ schema, values, formType: 'filter' })); + }} + onReset={(values, formikBag) => { + formikBag.resetForm(); + onFilterChange({}); + }} + render={({ values, handleChange, handleBlur, handleSubmit, handleReset }) => ( +
+ + + + + + + + + + + + Advanced{' '} + + + + + + + {createFormFields({ + handleChange, + handleBlur, + schema, + values, + formItemLayout, + prefix: '', + customFields, + formType: 'filter' + })} + +
+ )} + /> + ); + } +} + +export default FilterView; diff --git a/modules/core/client-react/crud/FormView.jsx b/modules/core/client-react/crud/FormView.jsx new file mode 100644 index 0000000000..c6a57ba949 --- /dev/null +++ b/modules/core/client-react/crud/FormView.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Formik } from 'formik'; +import DomainValidator from '@domain-schema/validation'; + +import { createFormFields, onSubmit, mapFormPropsToValues } from '@gqlapp/core-client-react'; +import { FormView, Button } from '@gqlapp/look-client-react-native'; + +const Form = ({ schema, data: { node }, updateEntry, createEntry }) => { + return ( + { + DomainValidator.validate(schema, values); + }} + onSubmit={async values => { + let title = node && node.__typename ? node.__typename : 'Model', + data = node || null; + + await onSubmit({ schema, values, updateEntry, createEntry, title, data }); + }} + render={({ handleSubmit, values, setFieldValue, setFieldTouched }) => ( + + {createFormFields(schema, values, setFieldValue, setFieldTouched)} + + + )} + /> + ); +}; + +Form.propTypes = { + schema: PropTypes.object.isRequired, + data: PropTypes.object, + updateEntry: PropTypes.func.isRequired, + createEntry: PropTypes.func.isRequired +}; + +export default Form; diff --git a/modules/core/client-react/crud/FormView.web.jsx b/modules/core/client-react/crud/FormView.web.jsx new file mode 100644 index 0000000000..1847bb31b0 --- /dev/null +++ b/modules/core/client-react/crud/FormView.web.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Formik } from 'formik'; +import DomainValidator from '@domain-schema/validation'; + +import { Form, FormItem, Button } from '@gqlapp/look-client-react'; +import { createFormFields } from '@gqlapp/core-client-react'; +import { onSubmit, mapFormPropsToValues } from '@gqlapp/core-common'; + +const tailFormItemLayout = { + wrapperCol: { + xs: { + span: 24, + offset: 0 + }, + sm: { + span: 16, + offset: 6 + } + } +}; + +const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 6 } + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 12 } + } +}; + +const FormView = ({ schema, updateEntry, createEntry, title, customFields, data }) => { + return ( + { + DomainValidator.validate(schema, values); + }} + onSubmit={async values => { + await onSubmit({ schema, values, updateEntry, createEntry, title, data: data ? data.node : null }); + }} + render={({ values, handleChange, handleBlur, handleSubmit }) => ( +
+ {createFormFields({ + handleChange, + handleBlur, + schema, + values, + formItemLayout, + customFields + })} + {/*errors && {errors}*/} + + + +
+ )} + /> + ); +}; + +FormView.propTypes = { + updateEntry: PropTypes.func.isRequired, + schema: PropTypes.object.isRequired, + createEntry: PropTypes.func.isRequired, + title: PropTypes.string.isRequired, + customFields: PropTypes.object, + data: PropTypes.object +}; + +export default FormView; diff --git a/modules/core/client-react/crud/ListView.jsx b/modules/core/client-react/crud/ListView.jsx new file mode 100644 index 0000000000..495f0169ae --- /dev/null +++ b/modules/core/client-react/crud/ListView.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, FlatList, Text, View } from 'react-native'; +import { SwipeAction } from '@gqlapp/look-client-react-native'; + +class ListView extends React.PureComponent { + static propTypes = { + loading: PropTypes.bool.isRequired, + data: PropTypes.object, + orderBy: PropTypes.object, + onOrderBy: PropTypes.func.isRequired, + deleteEntry: PropTypes.func.isRequired, + navigation: PropTypes.object.isRequired, + nativeLink: PropTypes.string.isRequired + }; + + keyExtractor = item => `${item.id}`; + + renderItem = ({ item: { id, name } }) => { + const { deleteEntry, navigation, nativeLink } = this.props; + return ( + navigation.navigate(nativeLink, { id })} + right={{ + text: 'Delete', + onPress: () => deleteEntry({ where: { id } }) + }} + > + {name} + + ); + }; + + render() { + const { loading, data } = this.props; + + if (loading && !data) { + return ( + + Loading... + + ); + } else { + return ; + } + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + alignItems: 'center', + justifyContent: 'center' + }, + element: { + paddingTop: 30 + }, + box: { + textAlign: 'center', + marginLeft: 15, + marginRight: 15 + } +}); + +export default ListView; diff --git a/modules/core/client-react/crud/ListView.web.jsx b/modules/core/client-react/crud/ListView.web.jsx new file mode 100644 index 0000000000..8d57774093 --- /dev/null +++ b/modules/core/client-react/crud/ListView.web.jsx @@ -0,0 +1,471 @@ +/* eslint-disable react/display-name */ +import React from 'react'; +import PropTypes from 'prop-types'; +import DomainValidator from '@domain-schema/validation'; +import { Link } from 'react-router-dom'; +import { Formik } from 'formik'; +import { DragDropContext, DragSource, DropTarget } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; +import { Table, Button, Popconfirm, Row, Col, Form, FormItem, Alert, Spin } from '@gqlapp/look-client-react'; +import { createColumnFields, createFormFields } from '@gqlapp/core-client-react'; +import { mapFormPropsToValues, pickInputFields } from '@gqlapp/core-common'; + +const { hasRole } = require('@gqlapp/user-client-react'); + +function dragDirection(dragIndex, hoverIndex, initialClientOffset, clientOffset, sourceClientOffset) { + const hoverMiddleY = (initialClientOffset.y - sourceClientOffset.y) / 2; + const hoverClientY = clientOffset.y - sourceClientOffset.y; + if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) { + return 'downward'; + } + if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) { + return 'upward'; + } +} + +let BodyRow = props => { + const { + isOver, + connectDragSource, + connectDropTarget, + moveRow, + dragRow, + clientOffset, + sourceClientOffset, + initialClientOffset, + ...restProps + } = props; + const style = { cursor: 'move' }; + + let className = restProps.className; + if (isOver && initialClientOffset) { + const direction = dragDirection( + dragRow.index, + restProps.index, + initialClientOffset, + clientOffset, + sourceClientOffset + ); + if (direction === 'downward') { + className += ' drop-over-downward'; + } + if (direction === 'upward') { + className += ' drop-over-upward'; + } + } + + return connectDragSource(connectDropTarget()); +}; + +const rowSource = { + beginDrag(props) { + return { + index: props.index + }; + } +}; + +const rowTarget = { + drop(props, monitor) { + const dragIndex = monitor.getItem().index; + const hoverIndex = props.index; + + // Don't replace items with themselves + if (dragIndex === hoverIndex) { + return; + } + + // Time to actually perform the action + props.moveRow(dragIndex, hoverIndex); + + // Note: we're mutating the monitor item here! + // Generally it's better to avoid mutations, + // but it's good here for the sake of performance + // to avoid expensive index searches. + monitor.getItem().index = hoverIndex; + } +}; + +BodyRow = DropTarget('row', rowTarget, (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + sourceClientOffset: monitor.getSourceClientOffset() +}))( + DragSource('row', rowSource, (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + dragRow: monitor.getItem(), + clientOffset: monitor.getClientOffset(), + initialClientOffset: monitor.getInitialClientOffset() + }))(BodyRow) +); + +class ListView extends React.Component { + static propTypes = { + loading: PropTypes.bool.isRequired, + data: PropTypes.object, + sortEntries: PropTypes.func.isRequired, + orderBy: PropTypes.object, + updateEntry: PropTypes.func.isRequired, + deleteEntry: PropTypes.func.isRequired, + onOrderBy: PropTypes.func.isRequired, + deleteManyEntries: PropTypes.func.isRequired, + updateManyEntries: PropTypes.func.isRequired, + loadMoreRows: PropTypes.func.isRequired, + schema: PropTypes.object.isRequired, + link: PropTypes.string.isRequired, + customColumnFields: PropTypes.object, + customColumnActions: PropTypes.object, + customBatchActions: PropTypes.object, + customBatchFields: PropTypes.object, + customActions: PropTypes.object, + tableScroll: PropTypes.object, + rowClassName: PropTypes.func, + currentUser: PropTypes.object, + currentUserLoading: PropTypes.bool, + parentWait: PropTypes.bool, + parentError: PropTypes.string, + parentSuccess: PropTypes.string, + tableHeaderColumnHeight: PropTypes.string + }; + + static defaultProps = { + tableHeaderColumnHeight: '38px' + }; + + static getDerivedStateFromProps(nextProps, prevState) { + if ( + prevState.lastParentWait !== nextProps.parentWait || + prevState.lastParentError !== nextProps.parentError || + prevState.lastParentSuccess !== nextProps.parentSuccess + ) { + return { + wait: nextProps.parentWait, + error: nextProps.parentError, + success: nextProps.parentSuccess, + lastParentWait: nextProps.parentWait, + lastParentError: nextProps.parentError, + lastParentSuccess: nextProps.parentSuccess + }; + } + + return null; + } + + state = { + selectedRowKeys: [], + loading: false, + wait: false, + error: null, + success: null, + lastParentWait: false, + lastParentError: null, + lastParentSuccess: null + }; + + components = { + body: { + row: BodyRow + } + }; + + moveRow = (dragIndex, hoverIndex) => { + const { data, sortEntries } = this.props; + const dragRow = data.edges[dragIndex]; + const dragReplace = data.edges[hoverIndex]; + + sortEntries([dragRow.id, dragReplace.id, dragReplace.rank, dragRow.rank]); + }; + + renderLoadMore = (data, loadMoreRows) => { + const leftToLoad = data.edges ? data.pageInfo.totalCount - data.edges.length : data.pageInfo.totalCount; + if (data.pageInfo.hasNextPage && leftToLoad > 0) { + return ( + + {leftToLoad > 25 && ( + + )} + {leftToLoad > 50 && ( + + )} + {leftToLoad > 100 && ( + + )} + + + ); + } + }; + + renderOrderByArrow = name => { + const { orderBy } = this.props; + + if (orderBy && orderBy.column === name) { + if (orderBy.order === 'desc') { + return ; + } else { + return ; + } + } else { + return ; + } + }; + + handleUpdate = async (data, id) => { + const { updateEntry } = this.props; + this.setState({ wait: true, error: null, success: null }); + const result = await updateEntry({ data, where: { id } }); + if (result && result.error) { + this.setState({ wait: false, error: result.error, success: null }); + } else { + this.setState({ wait: false, error: null, success: 'Successfully updated entry.' }); + } + }; + + handleDelete = async id => { + const { deleteEntry } = this.props; + this.setState({ wait: true, error: null, success: null }); + const result = await deleteEntry({ where: { id } }); + if (result && result.error) { + this.setState({ wait: false, error: result.error, success: null }); + } else { + this.setState({ wait: false, error: null, success: 'Successfully deleted entry.' }); + } + }; + + onCellChange = (key, id, updateEntry) => { + return value => { + updateEntry({ [key]: value }, id); + }; + }; + + handleDeleteMany = () => { + const { deleteManyEntries } = this.props; + const { selectedRowKeys } = this.state; + deleteManyEntries({ id_in: selectedRowKeys }); + this.setState({ selectedRowKeys: [] }); + }; + + handleUpdateMany = values => { + const { updateManyEntries } = this.props; + const { selectedRowKeys } = this.state; + updateManyEntries(values, { id_in: selectedRowKeys }); + this.setState({ selectedRowKeys: [] }); + }; + + orderBy = (e, name) => { + const { onOrderBy, orderBy } = this.props; + + e.preventDefault(); + + let order = 'asc'; + if (orderBy && orderBy.column === name) { + if (orderBy.order === 'asc') { + order = 'desc'; + } else if (orderBy.order === 'desc') { + return onOrderBy({ + column: '', + order: '' + }); + } + } + + return onOrderBy({ column: name, order }); + }; + + render() { + const { + loading, + data, + loadMoreRows, + schema, + link, + currentUser, + customColumnFields = {}, + customColumnActions, + customActions, + customBatchActions, + customBatchFields, + updateManyEntries, + tableScroll = null, + rowClassName = null, + tableHeaderColumnHeight + } = this.props; + const { selectedRowKeys, wait, error, success } = this.state; + const hasSelected = selectedRowKeys.length > 0; + + const showBatchFields = + customBatchFields === null + ? false + : customBatchActions && customBatchActions.role + ? !!hasRole(customBatchActions.role, currentUser) + : true; + + const showCustomActions = + customActions === null + ? false + : customActions && customActions.role + ? !!hasRole(customActions.role, currentUser) + : true; + + const rowSelection = showBatchFields + ? { + selectedRowKeys, + onChange: selectedRowKeys => { + this.setState({ selectedRowKeys }); + } + } + : null; + + const title = () => { + return showCustomActions && customActions && customActions.render ? ( + customActions.render + ) : showCustomActions ? ( + + + + ) : null; + }; + + const footer = () => { + return ( + + + {data && ( +
+
+ + ({data.edges ? data.edges.length : 0} / {data.pageInfo.totalCount}) + +
+
+ {hasSelected ? `(${selectedRowKeys.length} selected)` : ''} +
+
+ )} + + {showBatchFields && [ + + + + + , + + { + DomainValidator.validate(schema, values); + }} + onSubmit={async (values, { resetForm }) => { + const insertValues = pickInputFields({ schema, values, formType: 'batch' }); + if (selectedRowKeys && Object.keys(insertValues).length > 0) { + await updateManyEntries(insertValues, { id_in: selectedRowKeys }); + this.setState({ selectedRowKeys: [] }); + resetForm(); + } + }} + render={({ values, handleChange, handleBlur, handleSubmit }) => ( +
+ {createFormFields({ + handleChange, + handleBlur, + schema, + values, + formItemLayout: {}, + prefix: '', + formType: 'batch', + customFields: customBatchFields + })} + + + +
+ )} + /> + + ]} +
+ ); + }; + + const columns = createColumnFields({ + schema, + link, + currentUser, + orderBy: this.orderBy, + renderOrderByArrow: this.renderOrderByArrow, + handleUpdate: this.handleUpdate, + handleDelete: this.handleDelete, + onCellChange: this.onCellChange, + customFields: customColumnFields, + customActions: customColumnActions + }); + + let tableProps = { + dataSource: data ? data.edges : null, + columns: columns, + pagination: false, + size: 'small', + rowSelection: rowSelection, + loading: loading && !data, + title: title, + footer: footer + }; + + if (tableScroll) { + tableProps = { + ...tableProps, + scroll: tableScroll + }; + } + + if (rowClassName) { + tableProps = { + ...tableProps, + rowClassName + }; + } + + // only include this props if table includes rank, taht is used for sorting + if (schema.keys().includes('rank')) { + tableProps = { + ...tableProps, + components: this.components, + onRow: (record, index) => ({ + index, + moveRow: this.moveRow + }), + onHeaderRow: () => { + return { + style: { + height: tableHeaderColumnHeight + } + }; + } + }; + } + + return ( +
+ {wait && } + {error && } + {success && } + + {data && this.renderLoadMore(data, loadMoreRows)} + + ); + } +} + +export default DragDropContext(HTML5Backend)(ListView); diff --git a/modules/core/client-react/crud/index.jsx b/modules/core/client-react/crud/index.jsx new file mode 100644 index 0000000000..df236e0ec4 --- /dev/null +++ b/modules/core/client-react/crud/index.jsx @@ -0,0 +1,5 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as ListView } from './ListView'; +export { default as EditView } from './EditView'; +export { default as FormView } from './FormView'; +export { default as FilterView } from './FilterView'; diff --git a/modules/core/client-react/index.tsx b/modules/core/client-react/index.tsx index 9ad592f02f..95540fb788 100644 --- a/modules/core/client-react/index.tsx +++ b/modules/core/client-react/index.tsx @@ -1,4 +1,6 @@ import { PLATFORM } from '@gqlapp/core-common'; export { default as clientOnly } from './clientOnly'; +export * from './util'; +export * from './crud'; export default __CLIENT__ && PLATFORM === 'web' ? require('./app').default : {}; diff --git a/modules/core/client-react/util.jsx b/modules/core/client-react/util.jsx new file mode 100644 index 0000000000..0230090495 --- /dev/null +++ b/modules/core/client-react/util.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { pick, capitalize, startCase } from 'lodash'; +import { RenderField, RenderSwitch, RenderSelectQuery, RenderDatePicker } from '@gqlapp/look-client-react'; +import { FieldAdapter as Field } from '@gqlapp/forms-client-react'; + +export const createFormFields = (schema, values, setFieldValue, setFieldTouched) => { + let fields = []; + for (const key of schema.keys()) { + const value = schema.values[key]; + const type = Array.isArray(value.type) ? value.type[0] : value.type; + const hasTypeOf = targetType => type === targetType || type.prototype instanceof targetType; + const inputStyle = { fontSize: 16 }; + const switchStyle = { + itemContainer: { + borderBottomWidth: 1, + borderBottomColor: '#c9cccc' + }, + itemTitle: { + fontSize: 16 + } + }; + if (key === 'id') { + continue; + } + if (type.isSchema) { + fields.push( + { + setFieldValue(key, selectedValue); + setFieldTouched(key, true); + }} + /> + ); + } else if (hasTypeOf(String)) { + fields.push( + { + setFieldValue(key, selectedValue); + setFieldTouched(key, true); + }} + placeholder={startCase(key)} + /> + ); + } else if (hasTypeOf(Number)) { + fields.push( + { + setFieldValue(key, selectedValue); + setFieldTouched(key, true); + }} + placeholder={startCase(key)} + /> + ); + } else if (hasTypeOf(Boolean)) { + fields.push( + setFieldValue(key, selectedValue)} + style={switchStyle} + /> + ); + } else if (hasTypeOf(Date)) { + fields.push( + setFieldValue(key, selectedValue)} + placeholder={startCase(key)} + /> + ); + } + } + + return fields; +}; + +export const pickInputFields = ({ schema, values }) => { + return pick(values, schema.keys()); +}; diff --git a/modules/core/client-react/util.web.jsx b/modules/core/client-react/util.web.jsx new file mode 100644 index 0000000000..d11db855a2 --- /dev/null +++ b/modules/core/client-react/util.web.jsx @@ -0,0 +1,587 @@ +/* eslint-disable react/display-name */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { startCase, round } from 'lodash'; +import { Link } from 'react-router-dom'; +import { FieldArray } from 'formik'; +import moment from 'moment'; +import DomainSchema from '@domain-schema/core'; +import { mapFormPropsToValues } from '@gqlapp/core-common'; +import { FieldAdapter as Field } from '@gqlapp/forms-client-react'; + +import { + RenderField, + RenderNumber, + RenderTextArea, + RenderSelectQuery, + RenderSelectCountry, + RenderSelectFilterBoolean, + RenderDate, + RenderSwitch, + Popconfirm, + Switch, + InputNumber, + Icon, + Button, + FormItem, + //Col, + Input, + DatePicker, + RenderCellSelectQuery +} from '@gqlapp/look-client-react'; + +const { hasRole } = require('@gqlapp/user-client-react'); + +const dateFormat = 'YYYY-MM-DD'; + +export const createColumnFields = ({ + schema, + link, + currentUser, + orderBy, + renderOrderByArrow, + handleUpdate, + handleDelete, + onCellChange, + customFields = {}, + customActions +}) => { + let columns = []; + // use customFields if definded otherwise use schema.keys() + const keys = + customFields.constructor === Object && Object.keys(customFields).length !== 0 + ? Object.keys(customFields) + : schema.keys(); + + for (const key of keys) { + const role = customFields && customFields[key] && customFields[key].role ? customFields[key].role : false; + + const customFieldsHasRole = hasRole(role, currentUser); + + if (schema.values[key]) { + const value = schema.values[key]; + const hasTypeOf = targetType => + value.type === targetType || value.type.prototype instanceof targetType || value.type instanceof targetType; + const title = ( + orderBy(e, key)} href="#"> + {startCase(key)} {renderOrderByArrow(key)} + + ); + + if (customFieldsHasRole && value.show !== false && (key !== 'id' || customFields['id'])) { + if (value.type.isSchema) { + let column = 'name'; + for (const remoteKey of value.type.keys()) { + const remoteValue = value.type.values[remoteKey]; + if (remoteValue.sortBy) { + column = remoteKey; + } + } + const toString = value.type.__.__toString ? value.type.__.__toString : opt => opt[column]; + + columns.push( + createColumnField( + key, + customFields, + (text, record) => + customFields[key] && customFields[key]['render'] ? ( + customFields[key]['render'](text, record) + ) : ( + (text && text[column] ? toString(text) : '') + } + onChange={onCellChange(`${key}Id`, record.id, handleUpdate)} + /> + ), + title + ) + ); + } else if (hasTypeOf(Boolean)) { + columns.push( + createColumnField( + key, + customFields, + (text, record) => { + const data = {}; + data[key] = !text; + return handleUpdate(data, record.id)} onChange={() => {}} />; + }, + title + ) + ); + } else if ((hasTypeOf(String) || hasTypeOf(Number) || hasTypeOf(Date)) && key !== 'id') { + columns.push( + createColumnField( + key, + customFields, + (text, record) => { + let formatedText = text => text; + if (value.fieldInput === 'price') { + formatedText = text => (text > 0 ? `${round(text, 2).toFixed(2)} €` : ''); + } + return ( + + ); + }, + title + ) + ); + } else if (value.type.constructor !== Array) { + columns.push( + createColumnField( + key, + customFields, + (text, record) => { + return customFields[key] && customFields[key]['render'] + ? customFields[key]['render'](text, record) + : customFields[key] && customFields[key]['render'] + ? customFields[key]['render'](text, record) + : text; + }, + title + ) + ); + } + } + } else { + if (customFieldsHasRole) { + columns.push( + createColumnField( + key, + customFields, + (text, record) => + customFields[key] && customFields[key]['render'] ? customFields[key]['render'](text, record) : null, + startCase(key) + ) + ); + } + } + } + + const showColumnActions = + customActions === null + ? false + : customActions && customActions.role + ? !!hasRole(customActions.role, currentUser) + : true; + + if (showColumnActions) { + columns.push({ + title: 'Actions', + key: 'actions', + width: 150, + fixed: 'right', + render: (text, record) => { + return ( +
+ + + + handleDelete(record.id)} + key="delete" + target={`delete-button-${record.id}`} + className={'bootstrap-cell-delete-button'} + > + + +
+ ); + } + }); + } + return columns; +}; + +const createColumnField = (key, customFields, render, title) => { + return { + title: title, + dataIndex: key, + key: key, + fixed: customFields[key] && customFields[key]['fixed'] ? customFields[key]['fixed'] : null, + width: customFields[key] && customFields[key]['width'] ? customFields[key]['width'] : null, + align: customFields[key] && customFields[key]['align'] ? customFields[key]['align'] : null, + render: render + }; +}; + +export const createFormFields = ({ + handleChange, + handleBlur, + schema, + values = {}, + formItemLayout, + prefix = '', + customFields = {}, + formType = 'form' +}) => { + let fields = []; + // use customFields if definded otherwise use schema.keys() + const keys = + customFields.constructor === Object && Object.keys(customFields).length !== 0 + ? Object.keys(customFields) + : schema.keys(); + + for (const key of keys) { + const value = schema.values[key]; + const type = value.type.constructor === Array ? value.type[0] : value.type; + const hasTypeOf = targetType => value.type === targetType || value.type.prototype instanceof targetType; + + if (value.show !== false && value.type.constructor !== Array) { + let formField = createFormField(schema, key, type, values, formItemLayout, formType, hasTypeOf, prefix); + if (formField) { + if (Array.isArray(formField)) { + formField.forEach(field => fields.push(field)); + } else { + fields.push(formField); + } + } + } else { + if (value.type.constructor === Array) { + if (formType === 'form') { + if (values[key]) { + fields.push( + createFormFieldsArray( + key, + values, + value, + type, + handleChange, + handleBlur, + formItemLayout, + prefix, + formType + ) + ); + } + } else { + fields.push( + createFormFieldsArray(key, values, value, type, handleChange, handleBlur, formItemLayout, prefix, formType) + ); + } + } + } + } + + return fields; +}; + +const createFormField = (schema, key, type, values, formItemLayout, formType, hasTypeOf, prefix) => { + let component = RenderField; + let value = values ? values[key] : ''; + let style = {}; + let dateFields = []; + let inputType = 'text'; + let inputKey = key; + const optional = schema.values[key].optional; + + if (key === 'id' && formType !== 'filter') { + return false; + } + + if (type.isSchema) { + component = RenderSelectQuery; + if (formType === 'batch') { + style = { width: 100 }; + } + } else if (hasTypeOf(Boolean)) { + if (formType === 'filter') { + component = RenderSelectFilterBoolean; + } else { + component = RenderSwitch; + } + } else if (hasTypeOf(Date)) { + component = RenderDate; + if (formType === 'filter') { + dateFields.push( + + ); + dateFields.push( + + ); + } + } else if (hasTypeOf(Number)) { + inputType = 'number'; + component = RenderNumber; + } else if (hasTypeOf(String) && formType === 'filter') { + inputKey = `${key}_contains`; + value = values ? values[`${key}_contains`] : ''; + } else if (hasTypeOf(String) && schema.values[key].fieldInput === 'textarea' && formType !== 'filter') { + component = RenderTextArea; + } else if (hasTypeOf(String) && schema.values[key].fieldInput === 'country' && formType !== 'filter') { + component = RenderSelectCountry; + } + + let field = ( + + ); + + return dateFields.length === 2 ? dateFields : formType === 'filter' ? field : field; +}; + +const tailFormItemLayout = { + wrapperCol: { + xs: { + span: 24, + offset: 0 + }, + sm: { + span: 16, + offset: 6 + } + } +}; + +const createFormFieldsArray = ( + key, + values, + value, + type, + handleChange, + handleBlur, + formItemLayout, + prefix = '', + formType = 'form' +) => { + return ( + { + return ( + + {values[key].map((field, index) => ( +
+ {createFormFields({ + handleChange, + handleBlur, + schema: type, + values: mapFormPropsToValues({ schema: type, data: field }), + formItemLayout: formItemLayout, + prefix: `${prefix}${key}[${index}].`, + formType + })} + {!value.hasOne && formType === 'form' && ( + + + + )} +
+ ))} + {!value.hasOne && formType === 'form' && ( + + + + )} +
+ ); + }} + /> + ); +}; + +class EditableCell extends React.Component { + static propTypes = { + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]), + record: PropTypes.object, + role: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]), + currentUser: PropTypes.object, + render: PropTypes.func, + hasTypeOf: PropTypes.func, + schema: PropTypes.object, + onChange: PropTypes.func.isRequired + }; + + state = { + value: this.props.value, + searchText: '', + dirty: false, + editable: false + }; + UNSAFE_componentWillReceiveProps(nextProps) { + const { value } = this.props; + if (nextProps.value !== value) { + this.setState({ value: nextProps.value }); + } + } + handleChange = e => { + const value = e.target.value; + this.setState({ value }); + }; + handleNumberChange = e => { + let value = e; + if (e.target) { + value = e.target.value; + } + this.setState({ value }); + }; + handleDateChange = (date, dateString) => { + if (moment(date).isValid()) { + return this.setState({ value: moment(date).format(dateFormat) }); + } + this.setState({ value: dateString }); + }; + handleSelectChange = (value, edges) => { + this.setState({ value: edges.find(item => item.id === parseInt(value.key)) || '' }); + }; + check = () => { + const { hasTypeOf } = this.props; + + this.setState({ editable: false }); + if (this.props.onChange) { + if (hasTypeOf(DomainSchema)) { + this.props.onChange(this.state.value.id); + } else { + this.props.onChange(this.state.value); + } + } + }; + search = value => { + const { dirty } = this.state; + if ((value && value.length >= 2) || dirty) { + this.setState({ searchText: value, dirty: true }); + } + }; + edit = () => { + this.setState({ editable: true }); + }; + render() { + const { value, record, searchText, dirty, editable } = this.state; + const { render, hasTypeOf, schema, role, currentUser } = this.props; + + if (role) { + const hasRole = + currentUser && (!role || (Array.isArray(role) ? role : [role]).indexOf(currentUser.role) >= 0) ? true : false; + + if (!hasRole) { + return render(value, record); + } + } + let input = null; + if (hasTypeOf(DomainSchema)) { + input = ( + + ); + } else if (editable) { + let formatedValue = value; + input = ; + if (hasTypeOf(Number) || hasTypeOf(DomainSchema.Float)) { + let inputParms = { + value: formatedValue, + onChange: this.handleNumberChange, + onPressEnter: this.check + }; + if (hasTypeOf(DomainSchema.Float)) { + inputParms = { + step: '0.1', + ...inputParms + }; + } + input = ; + } else if (hasTypeOf(Date)) { + if (value !== null && value !== '') { + formatedValue = moment(value, dateFormat); + } else { + formatedValue = null; + } + + input = ( + + ); + } + } + return ( +
+ {editable ? ( +
+ {input} + +
+ ) : ( +
+ {render(value, record) || '\u00A0'} + +
+ )} +
+ ); + } +} diff --git a/modules/core/common/ISOCountries.js b/modules/core/common/ISOCountries.js new file mode 100644 index 0000000000..c0c3c4a075 --- /dev/null +++ b/modules/core/common/ISOCountries.js @@ -0,0 +1,248 @@ +export default { + AF: 'Afghanistan', + AX: 'Åland Islands', + AL: 'Albania', + DZ: 'Algeria', + AD: 'Andorra', + AO: 'Angola', + AG: 'Antigua and Barbuda', + AR: 'Argentina', + AM: 'Armenia', + AU: 'Australia', + AT: 'Austria', + AZ: 'Azerbaijan', + BS: 'The Bahamas', + BH: 'Bahrain', + BD: 'Bangladesh', + BB: 'Barbados', + BY: 'Belarus', + BE: 'Belgium', + BZ: 'Belize', + BJ: 'Benin', + BT: 'Bhutan', + BO: 'Bolivia', + BA: 'Bosnia and Herzegovina', + BW: 'Botswana', + BR: 'Brazil', + BN: 'Brunei', + BG: 'Bulgaria', + BF: 'Burkina', + BI: 'Burundi', + KH: 'Cambodia', + CM: 'Cameroon', + CA: 'Canada', + CV: 'Cape Verde', + CF: 'The Central African Republic', + TD: 'Chad', + CL: 'Chile', + CN: 'China', + CO: 'Colombia', + KM: 'The Comoros', + CG: 'The Congo', + CD: 'The Democratic Republic of the Congo', + CR: 'Costa Rica', + CI: "Côte d'Ivoire", + HR: 'Croatia', + CU: 'Cuba', + CY: 'Cyprus', + CZ: 'The Czech Republic', + DK: 'Denmark', + DJ: 'Djibouti', + DM: 'Dominica', + DO: 'The Dominican Republic', + TL: 'Timor-Leste', + EC: 'Ecuador', + EG: 'Egypt', + SV: 'El Salvador', + GQ: 'Equatorial Guinea', + ER: 'Eritrea', + EE: 'Estonia', + ET: 'Ethiopia', + FJ: 'Fiji', + FI: 'Finland', + FR: 'France', + GA: 'Gabon', + GM: 'The Gambia', + GE: 'Georgia', + DE: 'Germany', + GH: 'Ghana', + GR: 'Greece', + GD: 'Grenada', + GT: 'Guatemala', + GG: 'Guernsey', + GN: 'Guinea', + GW: 'Guinea-Bissau', + GY: 'Guyana', + HT: 'Haiti', + VA: 'The Holy See', + HN: 'Honduras', + HU: 'Hungary', + IS: 'Iceland', + IN: 'India', + ID: 'Indonesia', + IR: 'Iran', + IQ: 'Iraq', + IE: 'Ireland', + IM: 'Isle of Man', + IL: 'Israel', + IT: 'Italy', + JM: 'Jamaica', + JP: 'Japan', + JE: 'Jersey', + JO: 'Jordan', + KZ: 'Kazakhstan', + KE: 'Kenya', + KI: 'Kiribati', + KP: "The Democratic People's Republic of Korea", + KR: 'The Republic of Korea', + KW: 'Kuwait', + KG: 'Kyrgyzstan', + LA: 'Laos', + LV: 'Latvia', + LB: 'Lebanon', + LS: 'Lesotho', + LR: 'Liberia', + LY: 'Libya', + LI: 'Liechtenstein', + LT: 'Lithuania', + LU: 'Luxembourg', + MG: 'Madagascar', + MW: 'Malawi', + MY: 'Malaysia', + MV: 'Maldives', + ML: 'Mali', + MT: 'Malta', + MH: 'The Marshall Islands', + MR: 'Mauritania', + MU: 'Mauritius', + MX: 'Mexico', + FM: 'Micronesia', + MD: 'Moldova', + MC: 'Monaco', + MN: 'Mongolia', + ME: 'Montenegro', + MA: 'Morocco', + MZ: 'Mozambique', + MM: 'Myanmar', + NA: 'Namibia', + NR: 'Nauru', + NP: 'Nepal', + NL: 'The Netherlands', + NZ: 'New Zealand', + NI: 'Nicaragua', + NE: 'The Niger', + NG: 'Nigeria', + NO: 'Norway', + OM: 'Oman', + PK: 'Pakistan', + PW: 'Palau', + PA: 'Panama', + PG: 'Papua New Guinea', + PY: 'Paraguay', + PE: 'Peru', + PH: 'The Philippines', + PL: 'Poland', + PT: 'Portugal', + QA: 'Qatar', + RO: 'Romania', + RU: 'Russia', + RW: 'Rwanda', + KN: 'Saint Kitts and Nevis', + LC: 'Saint Lucia', + VC: 'Saint Vincent and The Grenadines', + WS: 'Samoa', + SM: 'San Marino', + ST: 'Sao Tome and Principe', + SA: 'Saudi Arabia', + SN: 'Senegal', + RS: 'Serbia', + SC: 'Seychelles', + SL: 'Sierra Leone', + SG: 'Singapore', + SK: 'Slovakia', + SI: 'Slovenia', + SB: 'Solomon Islands', + SO: 'Somalia', + ZA: 'South Africa', + ES: 'Spain', + LK: 'Sri Lanka', + SD: 'The Sudan', + SR: 'Suriname', + SZ: 'Swaziland', + SE: 'Sweden', + CH: 'Switzerland', + SY: 'Syria', + TJ: 'Tajikistan', + TZ: 'Tanzania', + TH: 'Thailand', + MK: 'Macedonia', + TG: 'Togo', + TO: 'Tonga', + TT: 'Trinidad and Tobago', + TN: 'Tunisia', + TR: 'Turkey', + TM: 'Turkmenistan', + TV: 'Tuvalu', + UG: 'Uganda', + UA: 'Ukraine', + AE: 'The United Arab Emirates', + GB: 'The United Kingdom', + US: 'The United States', + UY: 'Uruguay', + UZ: 'Uzbekistan', + VU: 'Vanuatu', + VE: 'Venezuela', + VN: 'Viet Nam', + YE: 'Yemen', + ZM: 'Zambia', + ZW: 'Zimbabwe', + AS: 'American Samoa', + AI: 'Anguilla', + AQ: 'Antarctica', + AW: 'Aruba', + BM: 'Bermuda', + BV: 'Bouvet Island', + IO: 'The British Indian Ocean Territory', + KY: 'Cayman Islands', + CX: 'Christmas Island', + CC: 'Cocos Islands', + CK: 'Cook Islands', + FK: 'Falkland Islands', + FO: 'Faroe Islands', + GF: 'French Guiana', + PF: 'French Polynesia', + TF: 'The French Southern Territories', + GI: 'Gibraltar', + GL: 'Greenland', + GP: 'Guadeloupe', + GU: 'Guam', + HM: 'Heard Island and McDonald Islands', + HK: 'Hong Kong', + MO: 'Macao', + MQ: 'Martinique', + YT: 'Mayotte', + MS: 'Montserrat', + AN: 'Netherlands Antilles', + NC: 'New Caledonia', + NU: 'Niue', + NF: 'Norfolk Island', + MP: 'Northern Mariana Islands', + PS: 'The Occupied Palestinian Territory', + PN: 'Pitcairn', + PR: 'Puerto Rico', + RE: 'Réunion', + BL: 'Saint Barthélemy', + SH: 'Saint Helena', + MF: 'Saint Martin', + PM: 'Saint Pierre and Miquelon', + GS: 'South Georgia and The South Sandwich Islands', + SJ: 'Svalbard and Jan Mayen', + TW: 'Taiwan', + TK: 'Tokelau', + TC: 'Turks and Caicos Islands', + UM: 'United States Minor Outlying Islands', + VG: 'British Virgin Islands', + VI: 'US Virgin Islands', + WF: 'Wallis and Futuna', + EH: 'Western Sahara' +}; diff --git a/modules/core/common/createApolloClient.ts b/modules/core/common/createApolloClient.ts index ea298d6b50..e2b3a73dc3 100644 --- a/modules/core/common/createApolloClient.ts +++ b/modules/core/common/createApolloClient.ts @@ -43,7 +43,7 @@ const createApolloClient = ({ // Pass all @client queries and @client defaults to localCache return localCache; } else { - // Pass all the other queries to netCache); + // Pass all the other queries to netCache return netCache; } } diff --git a/modules/core/common/crud.js b/modules/core/common/crud.js new file mode 100644 index 0000000000..aa1bd0272e --- /dev/null +++ b/modules/core/common/crud.js @@ -0,0 +1,256 @@ +import { isEqual, has } from 'lodash'; +import { flatten } from 'flat'; + +export const onSubmit = async ({ schema, values, updateEntry, createEntry, title, data = null }) => { + let result = null; + const insertValues = pickInputFields({ schema, values, data }); + + if (data) { + result = await updateEntry(insertValues, { id: data.id }); + } else { + result = await createEntry(insertValues); + } + + if (result && result.errors) { + let submitError = { + _error: `Edit ${title} failed!` + }; + result.errors.map(error => (submitError[error.field] = error.message)); + throw new submitError(); + } +}; + +export const mapFormPropsToValues = ({ schema, data = null, formType = 'form' }) => { + let fields = {}; + for (const key of schema.keys()) { + const value = schema.values[key]; + const hasTypeOf = targetType => value.type === targetType || value.type.prototype instanceof targetType; + if (formType === 'filter') { + if (value.show !== false && value.type.constructor !== Array) { + if (hasTypeOf(Date)) { + fields[`${key}_lte`] = data ? data[key] : ''; + fields[`${key}_gte`] = data ? data[key] : ''; + } else if (hasTypeOf(String)) { + fields[`${key}_contains`] = data ? data[key] : ''; + } else { + fields[key] = data ? data[key] : ''; + } + } else if (value.type.constructor === Array) { + fields[key] = data ? data[key] : [mapFormPropsToValues({ schema: value.type[0], formType })]; + } + } else { + if (value.show !== false) { + if (value.type.constructor === Array && value.type[0].isSchema) { + fields[key] = data ? data[key] : [mapFormPropsToValues({ schema: value.type[0], formType })]; + } else { + fields[key] = data ? data[key] : ''; + } + } + } + } + + return fields; +}; + +export const pickInputFields = ({ schema, values, data = null, formType = 'form' }) => { + let inputValues = {}; + //console.log('pickInputFields'); + //console.log('formType:', formType); + // console.log('values:', values); + // console.log('data:', data); + + for (const key of schema.keys()) { + const value = schema.values[key]; + const hasTypeOf = targetType => value.type === targetType || value.type.prototype instanceof targetType; + if (formType === 'filter') { + if (value.show !== false) { + if (value.type.constructor !== Array) { + if (value.type.isSchema && values[key]) { + inputValues[`${key}Id`] = Number(values[key].id ? values[key].id : values[key]); + } else if (hasTypeOf(Date)) { + if (values[`${key}_lte`]) { + inputValues[`${key}_lte`] = values[`${key}_lte`]; + } + if (values[`${key}_gte`]) { + inputValues[`${key}_gte`] = values[`${key}_gte`]; + } + } else if (hasTypeOf(Boolean)) { + if (values[key] === 'true') { + inputValues[key] = 'true'; + } else if (values[key] === 'false') { + inputValues[key] = 'false'; + } else { + inputValues[key] = ''; + } + } else if (hasTypeOf(String)) { + if (values[`${key}_contains`]) { + inputValues[`${key}_contains`] = values[`${key}_contains`]; + } + } else { + if (key in values && values[key]) { + inputValues[key] = values[key]; + } + } + } else if (value.type.constructor === Array) { + if (values && values[key] && values.hasOwnProperty(key)) { + values[key].forEach(item => { + inputValues[key] = pickInputFields({ schema: value.type[0], values: item, data: null, formType }); + }); + } + } + } + } else { + if (key in values && has(values, key)) { + if (value.type.isSchema) { + inputValues[`${key}Id`] = values[key] ? Number(values[key].id ? values[key].id : values[key]) : null; + } else if (key !== 'id' && value.type.constructor !== Array) { + inputValues[key] = values[key]; + } else if (value.type.constructor === Array) { + const keys1 = {}; + const keys2 = {}; + + let create = []; + let update = []; + let deleted = []; + + if (data && data.hasOwnProperty(key)) { + data[key].forEach(item => { + keys1[item.id] = item; + }); + } + + if (values && values.hasOwnProperty(key)) { + values[key].forEach(item => { + keys2[item.id] = item; + }); + } + + if (data && data.hasOwnProperty(key)) { + data[key].forEach(item => { + const obj = keys2[item.id]; + if (!obj) { + deleted.push({ id: item.id }); + } else { + if (!isEqual(obj, item)) { + const dataObj = keys1[item.id]; + update.push({ + where: { id: obj.id }, + data: pickInputFields({ schema: value.type[0], values: obj, data: dataObj, formType }) + }); + } + } + }); + } + + if (values && values.hasOwnProperty(key)) { + if (formType === 'batch') { + values[key].forEach(item => { + if (!keys1[item.id]) { + const dataObj = keys1[item.id]; + update.push({ + where: { id: 0 }, + data: pickInputFields({ schema: value.type[0], values: item, data: dataObj, formType }) + }); + } + }); + } else { + values[key].forEach(item => { + if (!keys1[item.id]) { + create.push(pickInputFields({ schema: value.type[0], values: item, data: item, formType })); + } + }); + } + } + + //console.log('created: ', create); + //console.log('updated: ', update); + //console.log('deleted: ', deleted); + + if (data || formType === 'batch') { + inputValues[key] = { create, update, delete: deleted }; + } else { + inputValues[key] = { create }; + } + } + } + } + } + + //console.log('inputValues:', inputValues); + + return inputValues; +}; + +export const updateEntry = async ({ ownProps: { refetch }, mutate }, { data, where }, updateEntryName) => { + try { + const { + data: { [updateEntryName]: updateEntry } + } = await mutate({ + variables: { data, where } + }); + + if (updateEntry.errors) { + return { errors: updateEntry.errors }; + } + + refetch(); + + return updateEntry; + } catch (e) { + console.log(e.graphQLErrors); + } +}; + +export const deleteEntry = async ({ ownProps: { refetch }, mutate }, { where }, deleteEntryName) => { + try { + const { + data: { [deleteEntryName]: deleteEntry } + } = await mutate({ + variables: { where } + }); + + if (deleteEntry.errors) { + return { errors: deleteEntry.errors }; + } + + refetch(); + + return deleteEntry; + } catch (e) { + console.log(e.graphQLErrors); + } +}; + +export const mergeFilter = (filter, defaults, schema) => { + let mergeFilter = filter; + if (!filter.hasOwnProperty('searchText')) { + const { searchText, ...restFilters } = defaults; + mergeFilter = { ...restFilters, ...filter }; + } + + for (const key of schema.keys()) { + const value = schema.values[key]; + const hasTypeOf = targetType => value.type === targetType || value.type.prototype instanceof targetType; + if (hasTypeOf(Boolean)) { + if (mergeFilter[key] === 'true') { + mergeFilter[key] = 'true'; + } else if (filter[key] === 'false') { + mergeFilter[key] = 'false'; + } + } + } + + // flatten objects with __ delimiter, because apollo state does not allow nested data + mergeFilter = flatten(mergeFilter, { + delimiter: '__', + overwrite: true + }); + // remove all empty objects + Object.keys(mergeFilter).map(item => { + if (typeof mergeFilter[item] === 'object') { + delete mergeFilter[item]; + } + }); + + return mergeFilter; +}; diff --git a/modules/core/common/index.ts b/modules/core/common/index.ts index 15fd627bbf..8c4e54d3ca 100644 --- a/modules/core/common/index.ts +++ b/modules/core/common/index.ts @@ -4,6 +4,8 @@ export * from './net'; export { default as log } from './log'; export { default as createApolloClient } from './createApolloClient'; export { default as createReduxStore } from './createReduxStore'; +export { default as countries } from './ISOCountries'; export * from './createReduxStore'; export * from './utils'; +export * from './crud'; export { flowRight as compose } from 'lodash'; diff --git a/modules/core/common/log.ts b/modules/core/common/log.ts index 86776ca598..cb7827594c 100644 --- a/modules/core/common/log.ts +++ b/modules/core/common/log.ts @@ -10,7 +10,7 @@ const log = minilog(loggerName); (log as any).suggest.defaultResult = false; (log as any).suggest.clear().allow(loggerName, settings.app.logging.level); -if (__DEV__ && __SERVER__ && !__TEST__) { +if (typeof __DEV__ !== 'undefined' && typeof __SERVER__ !== 'undefined' && typeof __TEST__ !== 'undefined') { const consoleLog = global.console.log; global.console.log = (...args: any[]) => { if (args.length === 1 && typeof args[0] === 'string' && args[0].match(/^\[(HMR|WDS)\]/)) { diff --git a/modules/core/common/net.ts b/modules/core/common/net.ts index 1c2155202f..755c8e80ba 100644 --- a/modules/core/common/net.ts +++ b/modules/core/common/net.ts @@ -1,17 +1,19 @@ import url from 'url'; import { PLATFORM } from './utils'; +const apiUrlDefine = typeof __API_URL__ !== 'undefined' ? __API_URL__ : '/graphql'; + export const serverPort = PLATFORM === 'server' && (process.env.PORT || (typeof __SERVER_PORT__ !== 'undefined' ? __SERVER_PORT__ : 8080)); -export const isApiExternal = !!url.parse(__API_URL__).protocol; +export const isApiExternal = !!url.parse(apiUrlDefine).protocol; const clientApiUrl = !isApiExternal && PLATFORM === 'web' ? `${window.location.protocol}//${window.location.hostname}${ __DEV__ ? ':8080' : window.location.port ? ':' + window.location.port : '' - }${__API_URL__}` - : __API_URL__; + }${apiUrlDefine}` + : apiUrlDefine; -const serverApiUrl = !isApiExternal ? `http://localhost:${serverPort}${__API_URL__}` : __API_URL__; +const serverApiUrl = !isApiExternal ? `http://localhost:${serverPort}${apiUrlDefine}` : apiUrlDefine; export const apiUrl = PLATFORM === 'server' ? serverApiUrl : clientApiUrl; diff --git a/modules/core/common/utils.ts b/modules/core/common/utils.ts index 545f3a2446..8e00a8a210 100644 --- a/modules/core/common/utils.ts +++ b/modules/core/common/utils.ts @@ -63,3 +63,60 @@ const getPlatform = () => { * Current platform */ export const PLATFORM = getPlatform(); + +const removeBySchema = (key: any, obj: any, schema: any) => { + if (schema === null) { + return obj[key] !== ''; + } else { + const schemaKey = key.endsWith('Id') ? key.substring(0, key.length - 2) : key; + if (schema && schema.values[schemaKey] && schema.values[schemaKey].type.isSchema) { + return obj[key] !== '' && obj[key] !== null; + } else { + return obj[key] !== ''; + } + } +}; + +export const removeEmpty = (obj: any, schema: any = null) => { + return Object.keys(obj) + .filter(key => removeBySchema(key, obj, schema)) + .reduce((redObj, key) => { + if (schema && schema.values[key]) { + const hasTypeOf = (targetType: any) => + schema.values[key].type === targetType || schema.values[key].type.prototype instanceof targetType; + if (schema.values[key].type.constructor === Array) { + if (obj[key].create && obj[key].create.length > 0) { + const tmpObj = obj[key]; + tmpObj.create[0] = removeEmpty(obj[key].create[0], schema.values[key].type[0]); + redObj[key] = tmpObj; + } else if (obj[key].update && obj[key].update.length > 0) { + const tmpObj = obj[key]; + tmpObj.update[0].data = removeEmpty(obj[key].update[0].data, schema.values[key].type[0]); + redObj[key] = tmpObj; + } else if (schema.values[key].type[0].isSchema) { + redObj[key] = removeEmpty(obj[key], schema.values[key].type[0]); + } else { + redObj[key] = obj[key]; + } + } else if (hasTypeOf(Number)) { + redObj[key] = Number(obj[key]); + } else if (hasTypeOf(Boolean)) { + redObj[key] = obj[key] === 'true' ? true : false; + } else { + redObj[key] = obj[key]; + } + } else { + redObj[key] = obj[key]; + } + + return redObj; + }, {}); +}; + +export const add3Dots = (str: any, limit: any) => { + const dots = '...'; + if (str.length > limit) { + str = str.substring(0, limit) + dots; + } + return str; +}; diff --git a/modules/core/server-ts/api/rootSchema.graphql b/modules/core/server-ts/api/rootSchema.graphql index 71d9551662..5533e213a6 100644 --- a/modules/core/server-ts/api/rootSchema.graphql +++ b/modules/core/server-ts/api/rootSchema.graphql @@ -3,6 +3,21 @@ type FieldError { message: String! } +type PageInfo { + totalCount: Int + hasNextPage: Boolean +} + +type BatchPayload { + count: Int + errors: [FieldError!] +} + +input OrderByInput { + column: String + order: String +} + type Query { dummy: Int } diff --git a/modules/core/server-ts/generatedSchemas.js b/modules/core/server-ts/generatedSchemas.js new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/modules/core/server-ts/generatedSchemas.js @@ -0,0 +1 @@ +export default {}; diff --git a/modules/core/server-ts/index.ts b/modules/core/server-ts/index.ts index 4574816de0..53ace60882 100644 --- a/modules/core/server-ts/index.ts +++ b/modules/core/server-ts/index.ts @@ -5,6 +5,8 @@ import { createServer } from './entry'; export { serverPromise } from './entry'; export { createSchema } from './api/schema'; +export { default as schemas } from './generatedSchemas'; + export default new ServerModule({ onAppCreate: [createServer] }); diff --git a/modules/database/server-ts/sql/crud.js b/modules/database/server-ts/sql/crud.js index ff3f59aa7e..e791502c87 100644 --- a/modules/database/server-ts/sql/crud.js +++ b/modules/database/server-ts/sql/crud.js @@ -1,14 +1,18 @@ import _ from 'lodash'; import uuidv4 from 'uuid'; -import { camelize, decamelizeKeys, camelizeKeys } from 'humps'; +import { decamelize, decamelizeKeys, camelize, camelizeKeys, pascalize } from 'humps'; import { log } from '@gqlapp/core-common'; +import knexnest from 'knexnest'; +import parseFields from 'graphql-parse-fields'; +import moment from 'moment'; +import { FieldError } from '@gqlapp/validation-common-react'; import knex from './connector'; - -import { orderedFor } from './helpers'; - +import { selectBy, orderedFor, removeTransient } from './helpers'; import selectAdapter from './select'; +const dateFormat = 'YYYY-MM-DD'; + export function createWithIdGenAdapter(options) { const T = options.table; let idGen = uuidv4; @@ -402,3 +406,651 @@ export function deleteRelationAdapter(options) { } }; } + +export class Crud { + getTableName() { + return decamelize(this.schema.__.tableName ? this.schema.__.tableName : this.schema.name); + } + + getFullTableName() { + return `${this.schema.__.tablePrefix}${this.getTableName()}`; + } + + isSortable() { + if (this.schema.__.sortable) { + return true; + } + return false; + } + + sortableField() { + if (this.schema.__.sortable) { + return this.schema.__.sortable; + } + return null; + } + + getSchema() { + return this.schema; + } + + getBaseQuery() { + return knex(`${this.getFullTableName()} as ${this.getTableName()}`); + } + + normalizeFields(data) { + //console.log('normalizeFields: ', data); + for (const key of _.keys(data)) { + if (this.schema.values.hasOwnProperty(key)) { + const value = this.schema.values[key]; + const hasTypeOf = targetType => value.type === targetType || value.type.prototype instanceof targetType; + if (hasTypeOf(Date)) { + data[key] = moment(data[key]).format('YYYY-MM-DD'); + } + } + } + //console.log('normalizeFields: ', data); + return data; + } + + _getList({ limit, offset, orderBy, filter }, info) { + const select = selectBy(this.schema, info, false); + const queryBuilder = select(this.getBaseQuery()); + + if (limit) { + queryBuilder.limit(limit); + } + + if (offset) { + queryBuilder.offset(offset); + } + + if (orderBy && orderBy.column) { + let column = orderBy.column; + const order = orderBy.order ? orderBy.order : 'asc'; + + for (const key of this.schema.keys()) { + if (column !== key) { + continue; + } + const value = this.schema.values[key]; + if (value.type.isSchema) { + const tableName = decamelize(value.type.__.tableName ? value.type.__.tableName : value.type.name); + const foundValue = value.type.keys().find(key => { + return value.type.values[key].sortBy; + }); + column = `${tableName}.${foundValue ? foundValue : 'name'}`; + } else { + column = `${this.getTableName()}.${decamelize(column)}`; + } + } + queryBuilder.orderBy(column, order); + queryBuilder.groupBy(`${this.getTableName()}.id`); + } else { + queryBuilder.orderBy(`${this.getTableName()}.id`); + } + + this._filter(filter, this.schema, queryBuilder, this.getTableName()); + + return knexnest(queryBuilder); + } + + _filter(filter, schema, queryBuilder, tableName) { + if (!_.isEmpty(filter)) { + const addFilterWhere = this.schemaIterator( + (filterKey, value, isSchema, isArray, _this, tableName, schema, filter) => { + const hasTypeOf = targetType => value.type === targetType || value.type.prototype instanceof targetType; + if (hasTypeOf(Date)) { + if (filter[`${filterKey}_lte`]) { + const filterValue_lte = moment(filter[`${filterKey}_lte`]).format(dateFormat); + _this.andWhere(`${tableName}.${decamelize(filterKey)}`, '<=', `${filterValue_lte}`); + } + if (filter[`${filterKey}_gte`]) { + const filterValue_gte = moment(filter[`${filterKey}_gte`]).format(dateFormat); + _this.andWhere(`${tableName}.${decamelize(filterKey)}`, '>=', `${filterValue_gte}`); + } + } else if (hasTypeOf(Boolean)) { + if (_.has(filter, filterKey)) { + _this.andWhere(`${tableName}.${decamelize(filterKey)}`, '=', `${+filter[filterKey]}`); + } + } else if (isArray) { + // do nothing + } else { + const tableColumn = isSchema ? `${decamelize(filterKey)}_id` : decamelize(filterKey); + + const filterValue = isSchema ? filter[`${filterKey}Id`] : filter[filterKey]; + if (filterValue) { + _this.andWhere(`${tableName}.${tableColumn}`, '=', `${filterValue}`); + } + + const filterValueIn = isSchema ? filter[`${filterKey}Id_in`] : filter[`${filterKey}_in`]; + if (filterValueIn) { + _this.whereIn(`${tableName}.${tableColumn}`, filterValueIn); + } + + const filterValueContains = isSchema ? filter[`${filterKey}Id_contains`] : filter[`${filterKey}_contains`]; + if (filterValueContains) { + _this.andWhere(`${tableName}.${tableColumn}`, 'like', `%${filterValueContains}%`); + } + + const filterValueGt = isSchema ? filter[`${filterKey}Id_gt`] : filter[`${filterKey}_gt`]; + if (_.isString(filterValueGt) || _.isNumber(filterValueGt)) { + _this.andWhere(`${tableName}.${tableColumn}`, '>', filterValueGt); + } + } + } + ); + + for (const key of schema.keys()) { + const value = schema.values[key]; + const isArray = value.type.constructor === Array; + if (isArray && !_.isEmpty(filter[key])) { + const type = value.type[0]; + const fieldName = decamelize(key); + const prefix = type.__.tablePrefix ? type.__.tablePrefix : ''; + const foreignTableName = decamelize(type.__.tableName ? type.__.tableName : type.name); + const baseTableName = decamelize(schema.name); + const suffix = value.noIdSuffix ? '' : '_id'; + + queryBuilder.leftJoin( + `${prefix}${foreignTableName} as ${fieldName}`, + `${baseTableName}.id`, + `${fieldName}.${baseTableName}${suffix}` + ); + + this._filter(filter[key], value.type[0], queryBuilder, fieldName); + } + } + + queryBuilder.where(function() { + addFilterWhere(this, tableName, schema, filter); + }); + if (filter.searchText) { + const addSearchTextWhere = this.schemaIterator( + (key, value, isSchema, isArray, _this, tableName, schema, filter) => { + if (value.searchText) { + _this.orWhere(`${tableName}.${decamelize(key)}`, 'like', `%${filter.searchText}%`); + } + } + ); + queryBuilder.where(function() { + addSearchTextWhere(this, tableName, schema, filter); + }); + } + } + } + + schemaIterator = fn => { + return (_this, tableName, schema, filter) => { + for (const key of schema.keys()) { + const value = schema.values[key]; + const isSchema = value.type.isSchema; + const isArray = value.type.constructor === Array; + fn(key, value, isSchema, isArray, _this, tableName, schema, filter); + } + }; + }; + + async getPaginated(args, info) { + const edges = await this._getList(args, parseFields(info).edges); + const { count } = await this.getTotal(args); + + return { + edges, + pageInfo: { + totalCount: count, + hasNextPage: edges && edges.length === args.limit + } + }; + } + + getList(args, info) { + return this._getList(args, parseFields(info)); + } + + getTotal(args = {}) { + const queryBuilder = knex(`${this.getFullTableName()} as ${this.getTableName()}`) + .countDistinct(`${this.getTableName()}.id as count`) + .first(); + + if (args.filter) { + this._filter(args.filter, this.schema, queryBuilder, this.getTableName()); + } + return queryBuilder; + } + + _get({ where }, info) { + const baseQuery = knex(`${this.getFullTableName()} as ${this.getTableName()}`); + const select = selectBy(this.schema, info, true); + + const tableName = this.getTableName(); + baseQuery.where(function() { + Object.keys(where).map(key => { + if (key.endsWith('_in')) { + const keyIn = key.substring(0, key.length - 3); + this.whereIn(`${tableName}.${decamelize(keyIn)}`, where[key]); + } else { + this.andWhere(`${tableName}.${decamelize(key)}`, '=', where[key]); + } + }); + }); + + return knexnest(select(baseQuery)); + } + + _getMany( + { + where: { id_in } + }, + info + ) { + const baseQuery = knex(`${this.getFullTableName()} as ${this.getTableName()}`); + const select = selectBy(this.schema, info); + + baseQuery.whereIn('id', [...id_in]); + + return knexnest(select(baseQuery)); + } + + async get(args, info) { + const node = await this._get(args, parseFields(info).node); + return { node }; + } + + _create(data) { + return knex(this.getFullTableName()) + .insert(decamelizeKeys(this.normalizeFields(data))) + .returning('id'); + } + + async create({ data }, ctx, info) { + try { + const e = new FieldError(); + e.throwIf(); + + // extract nested entries from data + let nestedEntries = []; + for (const key of this.schema.keys()) { + const value = this.schema.values[key]; + if (value.type.constructor === Array && data[key]) { + nestedEntries.push({ key, data: data[key] }); + delete data[key]; + } + data = removeTransient(data, key, value); + } + + if (this.isSortable()) { + const total = await this.getTotal(); + data[this.sortableField()] = total.count + 1; + } + + const [id] = await this._create(data); + + // create nested entries + if (nestedEntries.length > 0) { + nestedEntries.map(nested => { + if (nested.data.create) { + nested.data.create.map(async create => { + create[`${camelize(this.schema.name)}Id`] = id; + await ctx[pascalize(nested.key)]._create(create); + }); + } + }); + } + + return await this.get({ where: { id } }, info); + } catch (e) { + await ctx.Log.create({ + type: 'error', + module: this.getTableName(), + action: 'create', + message: JSON.stringify(e), + userId: ctx.auth.isAuthenticated.id + }); + return { errors: e }; + } + } + + _update({ data, where }, ctx) { + // extract nested entries from data + let nestedEntries = []; + for (const key of this.schema.keys()) { + const value = this.schema.values[key]; + if (value.type.constructor === Array && data[key]) { + nestedEntries.push({ key: value.type[0].name, data: data[key] }); + delete data[key]; + } + data = removeTransient(data, key, value); + } + + // create, update, delete nested entries + if (nestedEntries.length > 0) { + nestedEntries.map(nested => { + if (nested.data.create) { + nested.data.create.map(async create => { + create[`${this.getTableName()}Id`] = where.id; + await ctx[pascalize(nested.key)]._create(create); + }); + } + if (nested.data.update) { + nested.data.update.map(async update => { + await ctx[pascalize(nested.key)]._update(update, ctx); + }); + } + if (nested.data.delete) { + nested.data.delete.map(async where => { + await ctx[pascalize(nested.key)]._delete({ where }); + }); + } + }); + } + + return knex(this.getFullTableName()) + .update(decamelizeKeys(this.normalizeFields(data))) + .where(decamelizeKeys(where)); + } + + async update(args, ctx, info) { + try { + const e = new FieldError(); + e.throwIf(); + + await this._update(args, ctx); + + return await this.get(args, info); + } catch (e) { + await ctx.Log.create({ + type: 'error', + module: this.getTableName(), + action: 'update', + message: JSON.stringify(e), + userId: ctx.auth.isAuthenticated.id + }); + return { errors: e }; + } + } + + _delete({ where }) { + return knex(this.getFullTableName()) + .where(decamelizeKeys(where)) + .del(); + } + + async delete(args, info) { + try { + const e = new FieldError(); + + const node = await this.get(args, info); + + if (!node) { + e.setError('delete', 'Node does not exist.'); + e.throwIf(); + } + if (this.isSortable()) { + const object = await knex(this.getFullTableName()) + .select(`${this.sortableField()} as rank`) + .where(args.where) + .first(); + + await knex(this.getFullTableName()) + .decrement(this.sortableField(), 1) + .where(this.sortableField(), '>', object.rank); + } + + const isDeleted = await this._delete(args); + + if (isDeleted) { + return node; + } else { + e.setError('delete', 'Could not delete Node. Please try again later.'); + e.throwIf(); + } + } catch (e) { + return { errors: e }; + } + } + + async _sort({ data }) { + // console.log('_sort, data:', data); + const oldId = data[0]; + // const newId = data[1]; + + const oldPosition = data[3]; + const newPosition = data[2]; + + if (oldPosition === newPosition) { + return 0; + } + + const fullTableName = this.getFullTableName(); + const sortableField = this.sortableField(); + const total = await this.getTotal(); + return knex.transaction(async function(trx) { + try { + // Move the object away + await knex(fullTableName) + .update({ [sortableField]: total.count + 1 }) + .where({ id: oldId }) + .transacting(trx); + // Shift the objects between the old and the new position + const baseQuery = knex(fullTableName); + if (oldPosition < newPosition) { + baseQuery.decrement(sortableField, 1); + } else { + baseQuery.increment(sortableField, 1); + } + let count = await baseQuery + .whereBetween(sortableField, [Math.min(oldPosition, newPosition), Math.max(oldPosition, newPosition)]) + .transacting(trx); + // Move the object back in + count += await knex(fullTableName) + .update({ [sortableField]: newPosition }) + .where({ id: oldId }) + .transacting(trx); + await trx.commit; + return count; + } catch (e) { + trx.rollback; + return 0; + } + }); + } + + async sort(args) { + try { + const e = new FieldError(); + e.throwIf(); + + const count = await this._sort(args); + if (count > 1) { + return { count }; + } else { + e.setError('sort', 'Could not sort Node. Please try again later.'); + e.throwIf(); + } + } catch (e) { + return { errors: e }; + } + } + + async _updateMany({ data, where: { id_in } }) { + // console.log('_updateMany:', data); + let normalizedData = {}; + for (const key of Object.keys(data)) { + const schemaKey = key.endsWith('Id') ? key.substring(0, key.length - 2) : key; + const value = this.schema.values[schemaKey]; + // add fields from one to one relation + if (value.type.constructor === Array && value.hasOne) { + let normalizedNestedData = {}; + const type = value.type[0]; + const fieldName = decamelize(key); + const prefix = type.__.tablePrefix ? type.__.tablePrefix : ''; + const suffix = value.noIdSuffix ? '' : '_id'; + const foreignTableName = decamelize(type.__.tableName ? type.__.tableName : type.name); + + // get ids of one to one entity + const ids = await knex + .select('id') + .from(`${prefix}${foreignTableName}`) + .whereIn(`${this.getTableName()}${suffix}`, id_in) + .reduce((array, value) => { + array.push(value.id); + return array; + }, []); + // console.log('ids:', ids); + + for (const nestedKey of Object.keys(data[key].update[0].data)) { + const nestedSchemaKey = nestedKey.endsWith('Id') ? nestedKey.substring(0, nestedKey.length - 2) : nestedKey; + if (type.values[nestedSchemaKey].type.constructor !== Array) { + normalizedNestedData[nestedKey] = data[key].update[0].data[nestedKey]; + } else { + // add new entries for many to many properties + if (Object.keys(data[key].update[0].data[nestedKey].update[0].data).length > 0) { + const nestedType = type.values[nestedSchemaKey].type[0]; + + // console.log('nestedKey:', nestedKey); + // console.log('type:', nestedType.name); + // console.log('tableName:', nestedType.__.tableName); + + const nestedTableName = `${nestedType.__.tablePrefix}${decamelize( + nestedType.__.tableName ? nestedType.__.tableName : nestedType.name + )}`; + + // get related field + let nestedChildId = null; + let nestedChildValue = null; + for (const nestedChildKey of Object.keys(data[key].update[0].data[nestedKey].update[0].data)) { + if (nestedChildKey.endsWith('Id')) { + nestedChildId = decamelize(nestedChildKey); + nestedChildValue = data[key].update[0].data[nestedKey].update[0].data[nestedChildKey]; + } + } + + if (nestedChildId) { + // console.log('nestedTableName:', nestedTableName); + // console.log('nestedChildId:', nestedChildId); + // console.log('nestedChildValue:', nestedChildValue); + // console.log('field name:', `${fieldName}${suffix}`); + // console.log('ids:', ids); + // delete all existing records for this property + await knex(nestedTableName) + .whereIn(`${fieldName}${suffix}`, ids) + .andWhere(nestedChildId, '=', nestedChildValue) + .del(); + + // insert new entries for all selected records + for (const id of ids) { + let insertFields = decamelizeKeys(data[key].update[0].data[nestedKey].update[0].data); + insertFields[`${fieldName}${suffix}`] = id; + // console.log('insertFields:', insertFields); + await knex(nestedTableName).insert(insertFields); + } + } + } + } + } + + if (Object.keys(normalizedNestedData).length > 0) { + normalizedNestedData = decamelizeKeys(this.normalizeFields(normalizedNestedData)); + // console.log('normalizedNestedData:', normalizedNestedData); + return knex(`${prefix}${foreignTableName}`) + .update(normalizedNestedData) + .whereIn('id', ids); + } + } else { + if (value.type.constructor === Array) { + // TODO: Array + } else { + normalizedData[`${this.getTableName()}.${key}`] = data[key]; + } + } + } + + if (Object.keys(normalizedData).length > 0) { + normalizedData = decamelizeKeys(this.normalizeFields(normalizedData)); + return this.getBaseQuery() + .update(normalizedData) + .whereIn(`${this.getTableName()}.id`, id_in); + } else { + return 1; + } + } + + async updateMany(args) { + try { + const e = new FieldError(); + const updateCount = await this._updateMany(args); + + if (updateCount > 0) { + return { count: updateCount }; + } else { + e.setError('update', 'Could not update any of selected Node. Please try again later.'); + e.throwIf(); + } + } catch (e) { + console.error(`Error in ${this.getFullTableName()}.updateMany()`, e); + return { errors: e }; + } + } + + _deleteMany({ where: { id_in } }) { + return knex(this.getFullTableName()) + .whereIn('id', id_in) + .del(); + } + + async deleteMany(args) { + try { + const e = new FieldError(); + + if (this.isSortable()) { + // for every deleted object decrease rank acordingly + for (const id of args.where.id_in) { + const object = await knex(this.getFullTableName()) + .select(`${this.sortableField()} as rank`) + .where({ id }) + .first(); + + await knex(this.getFullTableName()) + .decrement(this.sortableField(), 1) + .where(this.sortableField(), '>', object.rank); + } + } + + const deleteCount = await this._deleteMany(args); + + if (deleteCount > 0) { + return { count: deleteCount }; + } else { + e.setError('delete', 'Could not delete any of selected Node. Please try again later.'); + e.throwIf(); + } + } catch (e) { + return { errors: e }; + } + } + + async getByIds(ids, by, Obj, info, infoCustom = null, args = null) { + info = infoCustom === null ? parseFields(info) : infoCustom; + const remoteId = by !== 'id' ? `${by}Id` : by; + info[remoteId] = true; + const baseQuery = knex(`${Obj.getFullTableName()} as ${Obj.getTableName()}`); + const select = selectBy(Obj.getSchema(), info, false); + + const res = await knexnest( + select(baseQuery) + .whereIn(`${Obj.getTableName()}.${decamelize(remoteId)}`, ids) + // add additional filter options on joined table + .andWhere(function() { + if (!_.isEmpty(args)) { + Object.keys(args).forEach(key => { + if (key.endsWith('_in')) { + this.whereIn(`${Obj.getTableName()}.${decamelize(key.substring(0, key.length - 3))}`, args[key]); + } + }); + } + }) + ); + return orderedFor(res, ids, remoteId, false); + } +} diff --git a/modules/database/server-ts/sql/helpers.js b/modules/database/server-ts/sql/helpers.js index 3bcdca0b9d..a8e40dbbe7 100644 --- a/modules/database/server-ts/sql/helpers.js +++ b/modules/database/server-ts/sql/helpers.js @@ -1,4 +1,5 @@ -import { groupBy } from 'lodash'; +import { groupBy, findIndex } from 'lodash'; +import { decamelize } from 'humps'; import settings from '@gqlapp/config'; @@ -19,14 +20,153 @@ export const truncateTables = async (knex, Promise, tables) => { } }; -export const orderedFor = (rows, collection, field, singleObject) => { +export const orderedFor = (rows, collection, field, singleObject, singleField = false) => { // return the rows ordered for the collection const inGroupsOfField = groupBy(rows, field); return collection.map(element => { const elementArray = inGroupsOfField[element]; if (elementArray) { - return singleObject ? elementArray[0] : elementArray; + return singleObject + ? singleField + ? elementArray[0][Object.keys(elementArray[0])[0]] + : elementArray[0] + : elementArray; } return singleObject ? {} : []; }); }; + +export const orderedForArray = (rows, collection, field, arrayElement) => { + // return the rows ordered for the collection + const inGroupsOfField = groupBy(rows, field); + return collection.map(element => { + const elementArray = inGroupsOfField[element]; + if (elementArray) { + return inGroupsOfField[element].map(elm => { + return elm[arrayElement]; + }); + } + return []; + }); +}; + +/** + * Collecting selects and joins + * @param graphqlFields + * @param domainSchema + * @param selectItems + * @param joinNames + * @param single + * @param parentField + * @private + */ +const _getSelectFields = (graphqlFields, domainSchema, selectItems, joinNames, single, parentKey, parentPath) => { + for (const fieldName of Object.keys(graphqlFields)) { + if (fieldName === '__typename') { + continue; + } + const value = domainSchema.values[fieldName]; + if (graphqlFields[fieldName] === true) { + if (value && value.transient) { + continue; + } + selectItems.push(_getSelectField(fieldName, parentPath, domainSchema, single, parentKey)); + } else { + if (Array.isArray(value.type) || findIndex(joinNames, { fieldName: decamelize(fieldName) }) > -1) { + continue; + } + if (!value.type.__.transient) { + joinNames.push(_getJoinEntity(fieldName, value, domainSchema)); + } + + parentPath.push(fieldName); + + _getSelectFields( + graphqlFields[fieldName], + value.type, + selectItems, + joinNames, + single, + decamelize(fieldName), + parentPath + ); + + parentPath.pop(); + } + } +}; + +/** + * Computing select field + * @param fieldName + * @param parentField + * @param domainSchema + * @param single + * @returns {string} + * @private + */ +const _getSelectField = (fieldName, parentPath, domainSchema, single, parentKey) => { + const alias = parentPath.length > 0 ? `${parentPath.join('_')}_${fieldName}` : fieldName; + const tableName = `${decamelize(domainSchema.__.tableName ? domainSchema.__.tableName : domainSchema.name)}`; + const fullTableName = parentKey !== null && parentKey !== tableName ? `${parentKey}_${tableName}` : tableName; + // returning object would be array or no + const arrayPrefix = single ? '' : '_'; + return `${fullTableName}.${decamelize(fieldName)} as ${arrayPrefix}${alias}`; +}; + +/** + * Computing join entity object + * @param fieldName + * @param value + * @param domainSchema + * @returns {{fieldName: *, prefix: string, suffix: string, baseTableName: *, foreignTableName: *}} + * @private + */ +const _getJoinEntity = (fieldName, value, domainSchema) => { + return { + fieldName: decamelize(fieldName), + prefix: value.type.__.tablePrefix ? value.type.__.tablePrefix : '', + suffix: value.noIdSuffix ? '' : '_id', + baseTableName: decamelize(domainSchema.name), + foreignTableName: decamelize(value.type.__.tableName ? value.type.__.tableName : value.type.name) + }; +}; + +/** + * Computing query with selects and joins + * @param schema + * @param fields + * @param single + * @returns {function(*): *} + */ +export const selectBy = (schema, fields, single = false) => { + // select fields and joins + const parentPath = []; + const selectItems = []; + const joinNames = []; + _getSelectFields(fields, schema, selectItems, joinNames, single, null, parentPath); + + return query => { + // join table names + joinNames.map(({ fieldName, prefix, suffix, baseTableName, foreignTableName }) => { + // if fieldName (schema key) diff with table name than make proper table alias + const tableNameAlias = + fieldName !== null && fieldName !== foreignTableName ? `${fieldName}_${foreignTableName}` : foreignTableName; + query.leftJoin( + `${prefix}${foreignTableName} as ${tableNameAlias}`, + `${tableNameAlias}.id`, + `${baseTableName}.${fieldName}${suffix}` + ); + }); + + return query.select(selectItems); + }; +}; + +export const removeTransient = (data, key, value) => { + // remove transient field + if (value && value.transient) { + delete data[key]; + } + return data; +}; diff --git a/modules/database/server-ts/sql/index.ts b/modules/database/server-ts/sql/index.ts index 797cc2df05..b3ff47c0ad 100644 --- a/modules/database/server-ts/sql/index.ts +++ b/modules/database/server-ts/sql/index.ts @@ -2,3 +2,4 @@ export { default as knex } from './connector'; export { default as populateTestDb } from './populateTestDb'; export { default as createTransaction } from './createTransaction'; export * from './helpers'; +export * from './crud'; diff --git a/modules/forms/client-react/FieldAdapter.jsx b/modules/forms/client-react/FieldAdapter.jsx index 8cff823f60..6532ce5f91 100644 --- a/modules/forms/client-react/FieldAdapter.jsx +++ b/modules/forms/client-react/FieldAdapter.jsx @@ -13,7 +13,7 @@ class FieldAdapter extends Component { onChange: PropTypes.func, onBlur: PropTypes.func, name: PropTypes.string.isRequired, - value: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.number, PropTypes.object]), defaultValue: PropTypes.string, checked: PropTypes.bool, defaultChecked: PropTypes.bool, @@ -85,6 +85,8 @@ class FieldAdapter extends Component { return React.createElement(component, { ...this.props, input, + setFieldValue: formik.setFieldValue, + setFieldTouched: formik.setFieldTouched, meta }); } diff --git a/modules/look/client-react-native/ui-native-base/components/DatePickerAndroid.jsx b/modules/look/client-react-native/ui-native-base/components/DatePickerAndroid.jsx new file mode 100644 index 0000000000..410d857989 --- /dev/null +++ b/modules/look/client-react-native/ui-native-base/components/DatePickerAndroid.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { View, TouchableOpacity, StyleSheet, DatePickerAndroid as DatePickerAndroidNative, Text } from 'react-native'; +import DatePickerStyles from '../styles/DatePicker'; + +class DatePickerAndroid extends React.Component { + static propTypes = { + onChange: PropTypes.func, + value: PropTypes.string, + style: PropTypes.object, + placeholder: PropTypes.string + }; + + constructor() { + super(); + this.openDatePicker = this.openDatePicker.bind(this); + } + + async openDatePicker() { + try { + const datepickerResult = await DatePickerAndroidNative.open({ + date: this.props.value ? new Date(this.props.value) : new Date() + }); + + if (datepickerResult.action === DatePickerAndroidNative.dateSetAction) { + let validMonth = datepickerResult.month + 1; + let month = validMonth > 9 ? validMonth : '0' + validMonth; + let day = datepickerResult.day > 9 ? datepickerResult.day : '0' + datepickerResult.day; + this.props.onChange(`${datepickerResult.year}-${month}-${day}`); + } + } catch ({ code, message }) { + console.warn('Cannot open date picker', message); + } + } + + render() { + let { value, style, placeholder } = this.props; + return ( + + + + {value ? value : placeholder} + + + + ); + } +} + +DatePickerAndroid.propsTypes = { + onChange: PropTypes.func, + value: PropTypes.string, + style: PropTypes.object, + placeholder: PropTypes.string +}; + +const styles = StyleSheet.create(DatePickerStyles); + +export default DatePickerAndroid; diff --git a/modules/look/client-react-native/ui-native-base/components/DatePickerIOS.jsx b/modules/look/client-react-native/ui-native-base/components/DatePickerIOS.jsx new file mode 100644 index 0000000000..f323f78faf --- /dev/null +++ b/modules/look/client-react-native/ui-native-base/components/DatePickerIOS.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { View, TouchableOpacity, StyleSheet, DatePickerIOS as DatePickerIOSNative, Text } from 'react-native'; +import DatePickerStyles from '../styles/DatePicker'; + +class DatePickerIOS extends React.Component { + static propTypes = { + onChange: PropTypes.func, + value: PropTypes.string, + style: PropTypes.object, + placeholder: PropTypes.string + }; + + constructor() { + super(); + this.setDate = this.setDate.bind(this); + this.toggleDatePicker = this.toggleDatePicker.bind(this); + this.state = { + showDatePicker: false + }; + } + + setDate(newDate) { + if (!(newDate instanceof Date)) { + return false; + } + let validMonth = newDate.getMonth() + 1; + let month = validMonth > 9 ? validMonth : '0' + validMonth; + let day = newDate.getDate(); + if (day < 10) { + day = '0' + day; + } + this.props.onChange(`${newDate.getFullYear()}-${month}-${day}`); + } + + toggleDatePicker() { + if (this.state.showDatePicker) { + return this.setState({ showDatePicker: false }); + } + this.setState({ showDatePicker: true }); + } + + render() { + let { value, style, placeholder } = this.props; + let datePicker = ; + if (this.state.showDatePicker) { + datePicker = ( + + + + ); + } + return ( + + + + {value ? value : placeholder} + + + {datePicker} + + ); + } +} + +const styles = StyleSheet.create(DatePickerStyles); + +export default DatePickerIOS; diff --git a/modules/look/client-react-native/ui-native-base/components/RenderDatePicker.jsx b/modules/look/client-react-native/ui-native-base/components/RenderDatePicker.jsx new file mode 100644 index 0000000000..8f396bde52 --- /dev/null +++ b/modules/look/client-react-native/ui-native-base/components/RenderDatePicker.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Platform } from 'react-native'; +import DatePickerIOS from './DatePickerIOS'; +import DatePickerAndroid from './DatePickerAndroid'; + +const RenderDatePicker = ({ ...props }) => { + return Platform.OS === 'ios' ? : ; +}; + +export default RenderDatePicker; diff --git a/modules/look/client-react-native/ui-native-base/components/RenderField.jsx b/modules/look/client-react-native/ui-native-base/components/RenderField.jsx index f2df7589a4..7e7a5ac916 100644 --- a/modules/look/client-react-native/ui-native-base/components/RenderField.jsx +++ b/modules/look/client-react-native/ui-native-base/components/RenderField.jsx @@ -23,7 +23,9 @@ RenderField.propTypes = { input: PropTypes.object, label: PropTypes.string, type: PropTypes.string, - meta: PropTypes.object + meta: PropTypes.object, + touched: PropTypes.any, + error: PropTypes.string }; export default RenderField; diff --git a/modules/look/client-react-native/ui-native-base/components/RenderSelectQuery.jsx b/modules/look/client-react-native/ui-native-base/components/RenderSelectQuery.jsx new file mode 100644 index 0000000000..4cc925fdc0 --- /dev/null +++ b/modules/look/client-react-native/ui-native-base/components/RenderSelectQuery.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { pascalize } from 'humps'; +import { View, Text, StyleSheet } from 'react-native'; +import schemaQueries from '../../../client-react/generatedContainers'; +import RenderSelect from './RenderSelect'; +import InputItemStyles from '../styles/InputItem'; + +const LIMIT = 20; + +const handleSelect = (selectedValue, edges, onChange) => { + let selectedItem = edges && Array.isArray(edges) ? edges.find(item => item.value == selectedValue) : ''; + onChange(selectedItem ? selectedItem : ''); +}; + +const RenderSelectQuery = ({ + input: { name }, + meta: { error }, + label, + schema, + customStyles, + onChange, + value, + ...props +}) => { + const pascalizeSchemaName = pascalize(schema.name); + const formatedValue = value && value != '' && typeof value !== 'undefined' ? value.id : '0'; + const defaultOption = { label: 'Select Item', value: '0' }; + const Query = schemaQueries[`${pascalizeSchemaName}Query`]; + + let defaultStyle = { + container: { + paddingLeft: 0, + flex: 1 + }, + itemContainer: { + flex: 1 + }, + itemTitle: {}, + itemAction: { + flexDirection: 'row', + flex: 1, + justifyContent: 'flex-end', + alignItems: 'center' + } + }; + + if (customStyles) { + defaultStyle = customStyles; + } + + return ( + + {({ loading, data }) => { + if (!loading || data) { + let computedData = null; + if (Array.isArray(data.edges) && data.edges.length > 0) { + computedData = data.edges.map(item => { + return { ...item, label: item.name, value: item.id }; + }); + if (formatedValue) { + computedData.push(defaultOption); + } + } + + let computedProps = { + ...props, + renderSelectStyles: defaultStyle, + value: formatedValue, + data: computedData, + name, + error, + onChange: selectedValue => handleSelect(selectedValue, computedData, onChange) + }; + return ( + + + {!!error && ( + + {error} + + )} + + ); + } else { + return ; + } + }} + + ); +}; + +const styles = StyleSheet.create(InputItemStyles); + +RenderSelectQuery.propTypes = { + input: PropTypes.object, + label: PropTypes.string, + schema: PropTypes.object, + customStyles: PropTypes.object, + formType: PropTypes.string, + onChange: PropTypes.any, + value: PropTypes.any, + meta: PropTypes.object, + error: PropTypes.string +}; + +export default RenderSelectQuery; diff --git a/modules/look/client-react-native/ui-native-base/components/Select.jsx b/modules/look/client-react-native/ui-native-base/components/Select.jsx index a0fae26ff4..13dd85b1e1 100644 --- a/modules/look/client-react-native/ui-native-base/components/Select.jsx +++ b/modules/look/client-react-native/ui-native-base/components/Select.jsx @@ -18,10 +18,11 @@ const Select = ({ style, itemStyle, placeholder = '...', + error, ...props }) => { return Platform.OS === 'ios' ? ( - + {icon && ( )} @@ -38,7 +39,7 @@ const Select = ({ ) : ( - + { + return {children}; +}; + +ButtonGroup.propTypes = { + children: PropTypes.node +}; + +export default ButtonGroup; diff --git a/modules/look/client-react/ui-antd/components/Collapse.jsx b/modules/look/client-react/ui-antd/components/Collapse.jsx new file mode 100644 index 0000000000..becb4670bd --- /dev/null +++ b/modules/look/client-react/ui-antd/components/Collapse.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Collapse as ADCollapse } from 'antd'; + +const Collapse = ({ children, ...props }) => { + return {children}; +}; + +Collapse.propTypes = { + children: PropTypes.node +}; + +export default Collapse; diff --git a/modules/look/client-react/ui-antd/components/DatePicker.jsx b/modules/look/client-react/ui-antd/components/DatePicker.jsx new file mode 100644 index 0000000000..1b4c0f75a1 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/DatePicker.jsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { DatePicker as ADDatePicker } from 'antd'; + +const DatePicker = props => { + return ; +}; +export default DatePicker; diff --git a/modules/look/client-react/ui-antd/components/Icon.jsx b/modules/look/client-react/ui-antd/components/Icon.jsx new file mode 100644 index 0000000000..c003d20354 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/Icon.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Icon as ADIcon } from 'antd'; + +const Icon = ({ children, ...props }) => { + return {children}; +}; + +Icon.propTypes = { + children: PropTypes.node +}; + +export default Icon; diff --git a/modules/look/client-react/ui-antd/components/InputNumber.jsx b/modules/look/client-react/ui-antd/components/InputNumber.jsx new file mode 100644 index 0000000000..99482918ef --- /dev/null +++ b/modules/look/client-react/ui-antd/components/InputNumber.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { InputNumber as ADInputNumber } from 'antd'; + +const InputNumber = ({ children, ...props }) => { + return {children}; +}; + +InputNumber.propTypes = { + children: PropTypes.node +}; + +export default InputNumber; diff --git a/modules/look/client-react/ui-antd/components/PageLayout.jsx b/modules/look/client-react/ui-antd/components/PageLayout.jsx index ca3f3041e4..f1860dd9c6 100644 --- a/modules/look/client-react/ui-antd/components/PageLayout.jsx +++ b/modules/look/client-react/ui-antd/components/PageLayout.jsx @@ -1,45 +1,115 @@ import React from 'react'; import Helmet from 'react-helmet'; import PropTypes from 'prop-types'; -import { Layout } from 'antd'; +import { withRouter, NavLink } from 'react-router-dom'; +import { Layout, Menu } from 'antd'; import settings from '@gqlapp/config'; -import NavBar from './NavBar'; +import { Row, Col, MenuItem, Icon } from './index'; import styles from '../styles/styles.less'; -const { Header, Content, Footer } = Layout; +const ref = { modules: null }; + +export const onAppCreate = modules => (ref.modules = modules); + +const { Header, Content, Footer, Sider } = Layout; class PageLayout extends React.Component { + static propTypes = { + children: PropTypes.node, + navBar: PropTypes.bool, + location: PropTypes.object.isRequired + }; + + state = { + collapsed: false, + current: '/' + }; + + toggle = () => { + this.setState({ + collapsed: !this.state.collapsed + }); + }; + + handleClick = e => { + this.setState({ + current: e.key + }); + }; + render() { const { children, navBar } = this.props; return ( - - {navBar !== false && ( -
- -
- )} - {__SERVER__ && __DEV__ && ( - - - - )} - - {children} - -
- © {new Date().getFullYear()}. {settings.app.name}. -
+ + + + + + {settings.app.name} + + + {ref.modules.navItems} + + + + {navBar !== false && ( +
+ +
+ + + + + + + + + {ref.modules.navItemsRight} + {__DEV__ && ( + + GraphiQL + + )} + + + + + )} + {__SERVER__ && __DEV__ && ( + + + + )} + + {children} + +
+ © {new Date().getFullYear()}. {settings.app.name}. +
+ ); } } -PageLayout.propTypes = { - children: PropTypes.node, - navBar: PropTypes.bool -}; - -export default PageLayout; +export default withRouter(PageLayout); diff --git a/modules/look/client-react/ui-antd/components/Panel.jsx b/modules/look/client-react/ui-antd/components/Panel.jsx new file mode 100644 index 0000000000..3d19da81e0 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/Panel.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Collapse } from 'antd'; + +const ADPanel = Collapse.Panel; + +const Panel = ({ children, ...props }) => { + return {children}; +}; + +Panel.propTypes = { + children: PropTypes.node +}; + +export default Panel; diff --git a/modules/look/client-react/ui-antd/components/Popconfirm.jsx b/modules/look/client-react/ui-antd/components/Popconfirm.jsx new file mode 100644 index 0000000000..e2d9be04c6 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/Popconfirm.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Popconfirm as ADPopconfirm } from 'antd'; + +const Popconfirm = ({ children, ...props }) => { + return {children}; +}; + +Popconfirm.propTypes = { + children: PropTypes.node +}; + +export default Popconfirm; diff --git a/modules/look/client-react/ui-antd/components/Progress.jsx b/modules/look/client-react/ui-antd/components/Progress.jsx new file mode 100644 index 0000000000..5d8861b219 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/Progress.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Progress as ADProgress } from 'antd'; + +const Progress = ({ children, ...props }) => { + return {children}; +}; + +Progress.propTypes = { + children: PropTypes.node +}; + +export default Progress; diff --git a/modules/look/client-react/ui-antd/components/RadioButton.jsx b/modules/look/client-react/ui-antd/components/RadioButton.jsx new file mode 100644 index 0000000000..2280ed8646 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/RadioButton.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Radio } from 'antd'; + +const ADRadioButton = Radio.Button; + +class RadioButton extends React.Component { + render() { + const { children, color, type, size, ...props } = this.props; + + let buttonSize = 'default'; + + if (size === 'sm') { + buttonSize = 'small'; + } else if (size === 'lg') { + buttonSize = 'large'; + } + + return ( + + {children} + + ); + } +} + +RadioButton.propTypes = { + children: PropTypes.node, + color: PropTypes.string, + type: PropTypes.string, + size: PropTypes.string +}; + +export default RadioButton; diff --git a/modules/look/client-react/ui-antd/components/RadioGroup.jsx b/modules/look/client-react/ui-antd/components/RadioGroup.jsx new file mode 100644 index 0000000000..32a37931e9 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/RadioGroup.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Radio } from 'antd'; + +const ADRadioGroup = Radio.Group; + +const RadioGroup = ({ children, ...props }) => { + return {children}; +}; + +RadioGroup.propTypes = { + children: PropTypes.node +}; + +export default RadioGroup; diff --git a/modules/look/client-react/ui-antd/components/RenderCellSelectQuery.jsx b/modules/look/client-react/ui-antd/components/RenderCellSelectQuery.jsx new file mode 100644 index 0000000000..4f05db5bc3 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/RenderCellSelectQuery.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Select, Spin } from 'antd'; +import { pascalize } from 'humps'; + +import schemaQueries from '../../generatedContainers'; + +const Option = Select.Option; +const LIMIT = 20; + +export default class RenderCellSelectQuery extends React.Component { + static propTypes = { + schema: PropTypes.object, + value: PropTypes.object, + searchText: PropTypes.any, + handleOnChange: PropTypes.func.isRequired, + handleSearch: PropTypes.func.isRequired, + style: PropTypes.object, + dirty: PropTypes.any + }; + + handleChange = data => value => { + console.log('valuevaluevaluevalue', value); + this.props.handleOnChange(value, data); + }; + + handleSearch = () => value => this.props.handleSearch(value); + + render() { + const { value, schema, style = { width: '100%' }, searchText, dirty } = this.props; + const column = schema.keys().find(key => !!schema.values[key].sortBy) || 'name'; + const orderBy = () => { + const foundOrderBy = schema.keys().find(key => !!schema.values[key].orderBy); + return foundOrderBy ? { column: foundOrderBy } : null; + }; + const toString = schema.__.__toString ? schema.__.__toString : opt => opt[column]; + const formattedValue = value ? { key: `${value.id}`, label: toString(value) } : { key: '', label: '' }; + const Query = schemaQueries[`${pascalize(schema.name)}Query`]; + + return ( + + {({ loading, data }) => { + if (loading || !data) { + return ; + } + const { + edges, + pageInfo: { totalCount } + } = data; + const isEdgesNotIncludeValue = value && edges && !edges.find(({ id }) => id === value.id); + const renderOptions = () => { + const defaultOption = formattedValue + ? [] + : [ + + ]; + return edges + ? edges.reduce((acc, opt) => { + acc.push( + + ); + return acc; + }, defaultOption) + : defaultOption; + }; + + const getSearchProps = () => { + return { + filterOption: false, + onSearch: this.handleSearch(), + value: isEdgesNotIncludeValue && dirty ? { key: `${edges[0].id}` } : formattedValue + }; + }; + + const getChildrenProps = () => { + return { + optionFilterProp: 'children', + filterOption: (input, { props: { children } }) => children.toLowerCase().includes(input.toLowerCase()), + value: formattedValue + }; + }; + + const basicProps = { + showSearch: true, + labelInValue: true, + dropdownMatchSelectWidth: false, + style, + onChange: this.handleChange(edges || null) + }; + const filterProps = totalCount > LIMIT ? getSearchProps() : getChildrenProps(); + const props = { ...basicProps, ...filterProps }; + return ; + }} + + ); + } +} diff --git a/modules/look/client-react/ui-antd/components/RenderCheckBox.jsx b/modules/look/client-react/ui-antd/components/RenderCheckBox.jsx index f8a0684c12..582fe3ced3 100644 --- a/modules/look/client-react/ui-antd/components/RenderCheckBox.jsx +++ b/modules/look/client-react/ui-antd/components/RenderCheckBox.jsx @@ -4,7 +4,7 @@ import { Checkbox, Form } from 'antd'; const FormItem = Form.Item; -const RenderCheckBox = ({ input, label, meta: { touched, error } }) => { +const RenderCheckBox = ({ input, label, formItemLayout, meta: { touched, error } }) => { let validateStatus = ''; if (touched && error) { validateStatus = 'error'; @@ -13,7 +13,9 @@ const RenderCheckBox = ({ input, label, meta: { touched, error } }) => { return (
- {label} + + {label} +
); @@ -23,6 +25,7 @@ RenderCheckBox.propTypes = { input: PropTypes.object, label: PropTypes.string, type: PropTypes.string, + formItemLayout: PropTypes.object, meta: PropTypes.object }; diff --git a/modules/look/client-react/ui-antd/components/RenderDate.jsx b/modules/look/client-react/ui-antd/components/RenderDate.jsx new file mode 100644 index 0000000000..72b630ee47 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/RenderDate.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { FormItem, DatePicker } from './index'; + +const dateFormat = 'YYYY-MM-DD'; + +export default class RenderDate extends React.Component { + static propTypes = { + input: PropTypes.object, + setFieldValue: PropTypes.func.isRequired, + setFieldTouched: PropTypes.func.isRequired, + label: PropTypes.string, + type: PropTypes.string, + formItemLayout: PropTypes.object, + meta: PropTypes.object + }; + + handleChange = (date, dateString) => { + const { + input: { name }, + setFieldValue + } = this.props; + setFieldValue(name, dateString); + }; + + handleBlur = () => { + const { + input: { name }, + setFieldTouched + } = this.props; + setFieldTouched(name, true); + }; + + render() { + const { + input: { value, onChange, onBlur, ...inputRest }, + label, + formItemLayout, + meta: { touched, error } + } = this.props; + + const formattedValue = value ? moment(value, dateFormat) : null; + + return ( + +
+ +
+
+ ); + } +} diff --git a/modules/look/client-react/ui-antd/components/RenderField.jsx b/modules/look/client-react/ui-antd/components/RenderField.jsx index ff97963354..aa72a7e69f 100644 --- a/modules/look/client-react/ui-antd/components/RenderField.jsx +++ b/modules/look/client-react/ui-antd/components/RenderField.jsx @@ -4,16 +4,16 @@ import { Form, Input } from 'antd'; const FormItem = Form.Item; -const RenderField = ({ input, label, type, meta: { touched, error }, placeholder }) => { +const RenderField = ({ input, label, type, inputMode, formItemLayout, meta: { touched, error }, placeholder }) => { let validateStatus = ''; if (touched && error) { validateStatus = 'error'; } return ( - +
- +
); @@ -24,7 +24,9 @@ RenderField.propTypes = { label: PropTypes.string, placeholder: PropTypes.string, type: PropTypes.string, - meta: PropTypes.object + inputMode: PropTypes.string, + meta: PropTypes.object, + formItemLayout: PropTypes.object }; export default RenderField; diff --git a/modules/look/client-react/ui-antd/components/RenderNumber.jsx b/modules/look/client-react/ui-antd/components/RenderNumber.jsx new file mode 100644 index 0000000000..0a7a472d5b --- /dev/null +++ b/modules/look/client-react/ui-antd/components/RenderNumber.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import DomainSchema from '@domain-schema/core'; +import { InputNumber } from 'antd'; + +import { FormItem } from './index'; + +export default class RenderNumber extends React.Component { + static propTypes = { + input: PropTypes.object, + setFieldValue: PropTypes.func.isRequired, + setFieldTouched: PropTypes.func.isRequired, + label: PropTypes.string, + type: PropTypes.string, + formItemLayout: PropTypes.object, + hasTypeOf: PropTypes.func, + meta: PropTypes.object + }; + + handleChange = value => { + const { + input: { name }, + setFieldValue, + hasTypeOf + } = this.props; + + setFieldValue(name, hasTypeOf && hasTypeOf(DomainSchema.Int) ? parseInt(value) || '' : value); + }; + + render() { + const { + input: { onChange, ...inputRest }, + label, + formItemLayout, + hasTypeOf, + meta: { touched, error } + } = this.props; + + const input = { + onChange: this.handleChange, + ...inputRest + }; + + if (hasTypeOf && hasTypeOf(DomainSchema.Int)) { + input.step = '1'; + } else if (hasTypeOf && hasTypeOf(DomainSchema.Float)) { + input.step = '0.1'; + } + + return ( + +
+ +
+
+ ); + } +} diff --git a/modules/look/client-react/ui-antd/components/RenderSelectCountry.jsx b/modules/look/client-react/ui-antd/components/RenderSelectCountry.jsx new file mode 100644 index 0000000000..63356c59bd --- /dev/null +++ b/modules/look/client-react/ui-antd/components/RenderSelectCountry.jsx @@ -0,0 +1,93 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Select } from 'antd'; +import { countries } from '@gqlapp/core-common'; + +import { FormItem } from './index'; + +const Option = Select.Option; + +export default class RenderSelectCountry extends React.Component { + static propTypes = { + input: PropTypes.object, + setFieldValue: PropTypes.func.isRequired, + setFieldTouched: PropTypes.func.isRequired, + label: PropTypes.string, + formItemLayout: PropTypes.object, + meta: PropTypes.object, + style: PropTypes.object, + formType: PropTypes.string.isRequired + }; + + handleChange = value => { + const { + input: { name }, + setFieldValue + } = this.props; + setFieldValue(name, value); + }; + + handleBlur = () => { + const { + input: { name }, + setFieldTouched + } = this.props; + setFieldTouched(name, true); + }; + + render() { + const { + input: { value, onChange, onBlur, ...inputRest }, + label, + style, + formItemLayout, + meta: { touched, error }, + formType + } = this.props; + + let validateStatus = ''; + if (touched && error) { + validateStatus = 'error'; + } + + const options = []; + Object.entries(countries).forEach(([key, value]) => + options.push( + + ) + ); + + let defaultStyle = { width: '100%' }; + if (style) { + defaultStyle = style; + } + + let defaultValue = 'defaultValue'; + if (formType === 'filter') { + defaultValue = 'value'; + } + + let props = { + allowClear: formType !== 'form' ? true : false, + showSearch: true, + dropdownMatchSelectWidth: false, + style: defaultStyle, + onChange: this.handleChange, + onBlur: this.handleBlur, + ...inputRest, + [defaultValue]: value, + optionFilterProp: 'children', + filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }; + + return ( + +
+ +
+
+ ); + } +} diff --git a/modules/look/client-react/ui-antd/components/RenderSelectFilterBoolean.jsx b/modules/look/client-react/ui-antd/components/RenderSelectFilterBoolean.jsx new file mode 100644 index 0000000000..19cf3e0656 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/RenderSelectFilterBoolean.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Form, Select } from 'antd'; + +const FormItem = Form.Item; +const Option = Select.Option; + +export default class RenderSelectFilterBoolean extends React.Component { + static propTypes = { + input: PropTypes.object, + setFieldValue: PropTypes.func.isRequired, + setFieldTouched: PropTypes.func.isRequired, + label: PropTypes.string, + type: PropTypes.string, + style: PropTypes.object, + formItemLayout: PropTypes.object, + meta: PropTypes.object, + children: PropTypes.node + }; + + handleChange = value => { + const { + input: { name }, + setFieldValue + } = this.props; + setFieldValue(name, value); + }; + + handleBlur = () => { + const { + input: { name }, + setFieldTouched + } = this.props; + setFieldTouched(name, true); + }; + + render() { + const { + input: { value, onChange, onBlur, ...inputRest }, + label, + style, + formItemLayout, + meta: { touched, error } + } = this.props; + + let validateStatus = ''; + if (touched && error) { + validateStatus = 'error'; + } + + let defaultStyle = { width: '100%' }; + if (style) { + defaultStyle = style; + } + + let props = { + style: defaultStyle, + name: inputRest.name, + onChange: this.handleChange, + onBlur: this.handleBlur, + value: value + }; + + return ( + +
+ +
+
+ ); + } +} diff --git a/modules/look/client-react/ui-antd/components/RenderSelectQuery.jsx b/modules/look/client-react/ui-antd/components/RenderSelectQuery.jsx new file mode 100644 index 0000000000..f8d01f43cf --- /dev/null +++ b/modules/look/client-react/ui-antd/components/RenderSelectQuery.jsx @@ -0,0 +1,149 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Select, Spin } from 'antd'; +import { pascalize } from 'humps'; + +import { FormItem } from './index'; +import schemaQueries from '../../generatedContainers'; + +const Option = Select.Option; +const LIMIT = 20; + +export default class RenderSelectQuery extends React.Component { + static propTypes = { + input: PropTypes.object, + setFieldValue: PropTypes.func.isRequired, + setFieldTouched: PropTypes.func.isRequired, + label: PropTypes.string, + formItemLayout: PropTypes.object, + meta: PropTypes.object, + schema: PropTypes.object, + style: PropTypes.object, + formType: PropTypes.string.isRequired, + optional: PropTypes.bool + }; + + state = { + searchText: '', + dirty: false + }; + + handleChange = edges => value => { + const { + input: { name }, + setFieldValue + } = this.props; + + const key = value && value.key ? parseInt(value.key) : ''; + + setFieldValue(name, edges.find(item => item.id === key) || ''); + }; + + handleBlur = () => { + const { + input: { name }, + setFieldTouched + } = this.props; + setFieldTouched(name, true); + }; + + search = value => { + const { dirty } = this.state; + if ((value && value.length >= 1) || dirty) { + this.setState({ searchText: value, dirty: true }); + } + }; + + render() { + const { + input: { value }, + schema, + style = { width: '80%' }, + formItemLayout, + meta: { touched, error }, + formType, + label, + optional + } = this.props; + const { searchText, dirty } = this.state; + const column = schema.keys().find(key => !!schema.values[key].sortBy) || 'name'; + const orderBy = () => { + const foundOrderBy = schema.keys().find(key => !!schema.values[key].orderBy); + return foundOrderBy ? { column: foundOrderBy } : null; + }; + const toString = schema.__.__toString ? schema.__.__toString : opt => opt[column]; + const formattedValue = value ? { key: `${value.id}`, label: toString(value) } : { key: '0', label: '' }; + const Query = schemaQueries[`${pascalize(schema.name)}Query`]; + + return ( + +
+ + {({ loading, data }) => { + if (!loading || data) { + const { + edges, + pageInfo: { totalCount } + } = data; + const isEdgesNotIncludeValue = value && edges && !edges.find(({ id }) => id === value.id); + const renderOptions = () => { + const defaultValue = `Select ${pascalize(schema.name)}`; + const defaultOption = + parseInt(formattedValue.key) === 0 + ? [ + + ] + : []; + return edges + ? edges.reduce((acc, opt) => { + acc.push( + + ); + return acc; + }, defaultOption) + : defaultOption; + }; + + const getSearchProps = () => { + return { + filterOption: false, + value: isEdgesNotIncludeValue && dirty ? { key: `${edges[0].id}` } : formattedValue + }; + }; + + const getChildrenProps = () => { + return { + optionFilterProp: 'children', + filterOption: (input, { props: { children } }) => + children.toLowerCase().includes(input.toLowerCase()), + value: formattedValue + }; + }; + + const basicProps = { + allowClear: formType !== 'form' ? true : optional, + showSearch: true, + labelInValue: true, + dropdownMatchSelectWidth: false, + style, + onSearch: this.search, + onChange: this.handleChange(edges || null), + onBlur: this.handleBlur + }; + const filterProps = totalCount > LIMIT ? getSearchProps() : getChildrenProps(); + const props = { ...basicProps, ...filterProps }; + return ; + } else { + return ; + } + }} + +
+
+ ); + } +} diff --git a/modules/look/client-react/ui-antd/components/RenderSwitch.jsx b/modules/look/client-react/ui-antd/components/RenderSwitch.jsx new file mode 100644 index 0000000000..679ddb4f26 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/RenderSwitch.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormItem, Switch } from './index'; + +export default class RenderSwitch extends React.Component { + static propTypes = { + input: PropTypes.object, + setFieldValue: PropTypes.func.isRequired, + setFieldTouched: PropTypes.func.isRequired, + label: PropTypes.string, + type: PropTypes.string, + formItemLayout: PropTypes.object, + meta: PropTypes.object + }; + + handleChange = value => { + const { + input: { name }, + setFieldValue + } = this.props; + setFieldValue(name, value); + }; + + render() { + const { + input: { value }, + label, + formItemLayout, + meta: { touched, error } + } = this.props; + + return ( + +
+ +
+
+ ); + } +} diff --git a/modules/look/client-react/ui-antd/components/RenderTextArea.jsx b/modules/look/client-react/ui-antd/components/RenderTextArea.jsx new file mode 100644 index 0000000000..78a9c3b237 --- /dev/null +++ b/modules/look/client-react/ui-antd/components/RenderTextArea.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Input } from 'antd'; + +import { FormItem } from './index'; + +const { TextArea } = Input; + +export default class RenderTextArea extends React.Component { + static propTypes = { + input: PropTypes.object, + label: PropTypes.string, + type: PropTypes.string, + formItemLayout: PropTypes.object, + hasTypeOf: PropTypes.func.isRequired, + meta: PropTypes.object + }; + + render() { + const { + input, + label, + formItemLayout, + meta: { touched, error } + } = this.props; + + return ( + +
+