From 56fea18501d8e72163c434583f8d2aabc98600b7 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 27 Apr 2021 18:13:26 +0200 Subject: [PATCH 01/14] ra-no-code - Introduce Resource Configuration --- .../src/inference/inferTypeFromValues.ts | 4 +- packages/ra-no-code/src/Admin.tsx | 12 ++ .../FieldConfiguration.tsx | 45 +++++ .../FieldConfigurationTab.tsx | 40 ++++ .../ResourceConfiguration.tsx | 186 ++++++++++++++++++ .../src/ResourceConfiguration/index.ts | 1 + packages/ra-no-code/src/ui/Layout.tsx | 2 +- packages/ra-no-code/src/ui/Menu.tsx | 20 +- .../ra-no-code/src/ui/ResourceMenuItem.tsx | 57 ++++++ 9 files changed, 350 insertions(+), 17 deletions(-) create mode 100644 packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration.tsx create mode 100644 packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationTab.tsx create mode 100644 packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx create mode 100644 packages/ra-no-code/src/ui/ResourceMenuItem.tsx diff --git a/packages/ra-core/src/inference/inferTypeFromValues.ts b/packages/ra-core/src/inference/inferTypeFromValues.ts index 88107508946..c8fc75f4c31 100644 --- a/packages/ra-core/src/inference/inferTypeFromValues.ts +++ b/packages/ra-core/src/inference/inferTypeFromValues.ts @@ -18,7 +18,7 @@ import { valuesAreEmail, } from './assertions'; -const types = [ +export const InferenceTypes = [ 'array', 'boolean', 'date', @@ -35,7 +35,7 @@ const types = [ 'url', ] as const; -export type PossibleInferredElementTypes = typeof types[number]; +export type PossibleInferredElementTypes = typeof InferenceTypes[number]; export interface InferredElementDescription { type: PossibleInferredElementTypes; diff --git a/packages/ra-no-code/src/Admin.tsx b/packages/ra-no-code/src/Admin.tsx index 803e33a0ff7..ae9431b0ac0 100644 --- a/packages/ra-no-code/src/Admin.tsx +++ b/packages/ra-no-code/src/Admin.tsx @@ -8,12 +8,23 @@ import localStorageDataProvider from 'ra-data-local-storage'; import { Create, Edit, List } from './builders'; import { useResourcesConfiguration, + ResourceConfigurationPage, ResourceConfigurationProvider, } from './ResourceConfiguration'; import { Layout, Ready } from './ui'; +import { Route } from 'react-router'; const dataProvider = localStorageDataProvider(); +const customRoutes = [ + ( + + )} + />, +]; + export const Admin = (props: AdminProps) => ( @@ -28,6 +39,7 @@ const InnerAdmin = (props: AdminProps) => { dataProvider={dataProvider} ready={Ready} layout={Layout} + customRoutes={customRoutes} {...props} > {hasResources diff --git a/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration.tsx b/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration.tsx new file mode 100644 index 00000000000..f4a8adf86f4 --- /dev/null +++ b/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { + getFieldLabelTranslationArgs, + InferenceTypes, + useTranslate, +} from 'ra-core'; +import { SelectInput, TextInput } from 'ra-ui-materialui'; +import { CardContent } from '@material-ui/core'; +import { textChangeRangeIsUnchanged } from 'typescript'; + +export const FieldConfiguration = props => { + const { index, field, resource } = props; + const translate = useTranslate(); + const labelArgs = getFieldLabelTranslationArgs({ + source: field.props.source, + resource, + label: field.props.label, + }); + + return ( + + + + ({ + id: type, + name: type, + }))} + /> + + ); +}; diff --git a/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationTab.tsx b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationTab.tsx new file mode 100644 index 00000000000..7a72eb7eb7c --- /dev/null +++ b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationTab.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { Tab } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { getFieldLabelTranslationArgs, useTranslate } from 'ra-core'; + +export const FieldConfigurationTab = ({ field, resource, ...props }) => { + const classes = useStyles(); + const translate = useTranslate(); + const labelArgs = getFieldLabelTranslationArgs({ + source: field.props.source, + resource, + label: field.props.label, + }); + + return ( + + ); +}; + +const useStyles = makeStyles(theme => ({ + root: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + textTransform: 'none', + minHeight: 0, + fontWeight: 'normal', + }, + selected: { + fontWeight: 'bold', + }, +})); diff --git a/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx b/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx new file mode 100644 index 00000000000..00b2d1c64fb --- /dev/null +++ b/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx @@ -0,0 +1,186 @@ +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import { + Avatar, + Card, + CardActions, + CardContent, + CardHeader, + Divider, + IconButton, + Tabs, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import { + FormWithRedirect, + InferredElementDescription, + RecordContextProvider, + SaveContextProvider, +} from 'ra-core'; +import { SaveButton, TextInput } from 'ra-ui-materialui'; +import { ResourceConfiguration } from './ResourceConfigurationContext'; +import { useResourceConfiguration } from './useResourceConfiguration'; +import { FieldConfiguration } from './FieldConfiguration'; +import { FieldConfigurationTab } from './FieldConfigurationTab'; + +export const ResourceConfigurationPage = ({ + resource, +}: { + resource: string; +}) => { + const [resourceConfiguration, actions] = useResourceConfiguration(resource); + const [activeField, setActiveField] = useState< + InferredElementDescription + >(); + const classes = useStyles(); + + const save = (values: ResourceConfiguration) => { + actions.update(values); + }; + const saveContext = { + save, + setOnFailure: () => {}, + setOnSuccess: () => {}, + }; + + const handleTabChange = (event, newValue) => { + const newField = resourceConfiguration.fields.find( + f => f.props.source === newValue + ); + setActiveField(newField); + }; + + useEffect(() => { + if (resourceConfiguration && resourceConfiguration.fields) { + setActiveField(resourceConfiguration.fields[0]); + } + }, [resourceConfiguration]); + + if (!resourceConfiguration || !activeField) { + return null; + } + + return ( + + + ( + + + { + // Here will go an icon selector + ( + resourceConfiguration.label || + resourceConfiguration.name + ).substr(0, 1) + } + + } + action={ + // Will display a menu to delete the resource maybe ? + + + + } + title={`Configuration of ${ + resourceConfiguration.label || + resourceConfiguration.name + }`} + /> + + + + + +
+ + {resourceConfiguration.fields.map(field => ( + + ))} + + {resourceConfiguration.fields.map( + (field, index) => ( + + ) + )} +
+ + + +
+ )} + /> +
+
+ ); +}; + +const useStyles = makeStyles(theme => ({ + fields: { + display: 'flex', + }, + fieldList: { + backgroundColor: theme.palette.background.default, + }, + fieldTitle: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + textTransform: 'none', + minHeight: 0, + }, + fieldPanel: { + flexGrow: 1, + }, + actions: { + backgroundColor: theme.palette.background.default, + }, +})); diff --git a/packages/ra-no-code/src/ResourceConfiguration/index.ts b/packages/ra-no-code/src/ResourceConfiguration/index.ts index 6145f4ee7f3..75fde27f687 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/index.ts +++ b/packages/ra-no-code/src/ResourceConfiguration/index.ts @@ -1,4 +1,5 @@ export * from './getFieldDefinitionsFromRecords'; +export * from './ResourceConfiguration'; export * from './useResourceConfiguration'; export * from './useResourcesConfiguration'; export * from './ResourceConfigurationContext'; diff --git a/packages/ra-no-code/src/ui/Layout.tsx b/packages/ra-no-code/src/ui/Layout.tsx index ade1235d912..77851e22255 100644 --- a/packages/ra-no-code/src/ui/Layout.tsx +++ b/packages/ra-no-code/src/ui/Layout.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Layout as RaLayout, LayoutProps } from 'react-admin'; -import Menu from './Menu'; +import { Menu } from './Menu'; export const Layout = (props: LayoutProps) => ( diff --git a/packages/ra-no-code/src/ui/Menu.tsx b/packages/ra-no-code/src/ui/Menu.tsx index af30ae85bb8..5721b85bdd4 100644 --- a/packages/ra-no-code/src/ui/Menu.tsx +++ b/packages/ra-no-code/src/ui/Menu.tsx @@ -6,18 +6,18 @@ import lodashGet from 'lodash/get'; // @ts-ignore import { useMediaQuery, Theme } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; -import DefaultIcon from '@material-ui/icons/ViewList'; import classnames from 'classnames'; -import { useGetResourceLabel, ReduxState } from 'ra-core'; +import { ReduxState } from 'ra-core'; -import { DashboardMenuItem, MenuItemLink } from 'react-admin'; +import { DashboardMenuItem } from 'react-admin'; import { NewResourceMenuItem } from './NewResourceMenuItem'; import { useResourcesConfiguration } from '../ResourceConfiguration'; +import { ResourceMenuItem } from './ResourceMenuItem'; export const MENU_WIDTH = 240; export const CLOSED_MENU_WIDTH = 55; -const Menu = (props: MenuProps) => { +export const Menu = (props: MenuProps) => { const { classes: classesOverride, className, @@ -34,7 +34,6 @@ const Menu = (props: MenuProps) => { ); const open = useSelector((state: ReduxState) => state.admin.ui.sidebarOpen); const [resources] = useResourcesConfiguration(); - const getResourceLabel = useGetResourceLabel(); return ( <> @@ -57,14 +56,9 @@ const Menu = (props: MenuProps) => { /> )} {Object.keys(resources).map(resource => ( - } + resource={resources[resource]} onClick={onMenuClick} dense={dense} sidebarIsOpen={open} @@ -126,5 +120,3 @@ Menu.propTypes = { Menu.defaultProps = { onMenuClick: () => null, }; - -export default Menu; diff --git a/packages/ra-no-code/src/ui/ResourceMenuItem.tsx b/packages/ra-no-code/src/ui/ResourceMenuItem.tsx new file mode 100644 index 00000000000..d8816c9988b --- /dev/null +++ b/packages/ra-no-code/src/ui/ResourceMenuItem.tsx @@ -0,0 +1,57 @@ +import React, { forwardRef } from 'react'; +import { MenuItemLink, MenuItemLinkProps } from 'react-admin'; +import { IconButton } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import SettingsIcon from '@material-ui/icons/Settings'; +import DefaultIcon from '@material-ui/icons/ViewList'; +import { NavLink, NavLinkProps } from 'react-router-dom'; +import { ResourceConfiguration } from '../ResourceConfiguration'; + +export const ResourceMenuItem = ( + props: Omit & { + resource: ResourceConfiguration; + } +) => { + const { resource, ...rest } = props; + const classes = useStyles(props); + return ( +
+ } + {...rest} + /> + + + +
+ ); +}; + +const NavLinkRef = forwardRef((props, ref) => ( + +)); + +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + }, + resource: { + flexGrow: 1, + }, + settings: { + marginLeft: 'auto', + }, +})); From 8d487e372aecffa436ba77801bd8b787096c2cd5 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 29 Apr 2021 10:07:09 +0200 Subject: [PATCH 02/14] Introduce View Selection For Fields --- ....tsx => FieldConfigurationFormSection.tsx} | 43 ++++++++++++++++--- .../ResourceConfiguration.tsx | 14 +++--- .../ResourceConfigurationContext.ts | 10 ++++- .../getFieldDefinitionsFromRecords.ts | 17 +++----- packages/ra-no-code/src/builders/Create.tsx | 6 +-- packages/ra-no-code/src/builders/Edit.tsx | 6 +-- packages/ra-no-code/src/builders/List.tsx | 6 +-- 7 files changed, 67 insertions(+), 35 deletions(-) rename packages/ra-no-code/src/ResourceConfiguration/{FieldConfiguration.tsx => FieldConfigurationFormSection.tsx} (55%) diff --git a/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration.tsx b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx similarity index 55% rename from packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration.tsx rename to packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx index f4a8adf86f4..090e5e177ed 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration.tsx +++ b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx @@ -4,11 +4,10 @@ import { InferenceTypes, useTranslate, } from 'ra-core'; -import { SelectInput, TextInput } from 'ra-ui-materialui'; +import { CheckboxGroupInput, SelectInput, TextInput } from 'ra-ui-materialui'; import { CardContent } from '@material-ui/core'; -import { textChangeRangeIsUnchanged } from 'typescript'; -export const FieldConfiguration = props => { +export const FieldConfigurationFormSection = props => { const { index, field, resource } = props; const translate = useTranslate(); const labelArgs = getFieldLabelTranslationArgs({ @@ -35,11 +34,41 @@ export const FieldConfiguration = props => { source={`fields[${index}].type`} label="Type" fullWidth - choices={InferenceTypes.map(type => ({ - id: type, - name: type, - }))} + choices={INFERENCE_TYPES} + /> + ); }; + +const INFERENCE_TYPES = InferenceTypes.map(type => ({ + id: type, + name: type, +})); + +const VIEWS = [ + { + id: 'list', + name: 'List', + }, + { + id: 'edit', + name: 'Edit', + }, + { + id: 'create', + name: 'Create', + }, + { + id: 'show', + name: 'Show', + }, +]; + +const VIEWS_INITIAL_VALUE = ['list', 'edit', 'create', 'show']; diff --git a/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx b/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx index 00b2d1c64fb..04d187e7b9b 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx +++ b/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx @@ -14,14 +14,16 @@ import { makeStyles } from '@material-ui/core/styles'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { FormWithRedirect, - InferredElementDescription, RecordContextProvider, SaveContextProvider, } from 'ra-core'; import { SaveButton, TextInput } from 'ra-ui-materialui'; -import { ResourceConfiguration } from './ResourceConfigurationContext'; +import { + ResourceConfiguration, + FieldConfiguration, +} from './ResourceConfigurationContext'; import { useResourceConfiguration } from './useResourceConfiguration'; -import { FieldConfiguration } from './FieldConfiguration'; +import { FieldConfigurationFormSection } from './FieldConfigurationFormSection'; import { FieldConfigurationTab } from './FieldConfigurationTab'; export const ResourceConfigurationPage = ({ @@ -30,9 +32,7 @@ export const ResourceConfigurationPage = ({ resource: string; }) => { const [resourceConfiguration, actions] = useResourceConfiguration(resource); - const [activeField, setActiveField] = useState< - InferredElementDescription - >(); + const [activeField, setActiveField] = useState(); const classes = useStyles(); const save = (values: ResourceConfiguration) => { @@ -133,7 +133,7 @@ export const ResourceConfigurationPage = ({ > {activeField.props.source === field.props.source ? ( - { +): FieldConfiguration[] => { const values = getValuesFromRecords(records); - return Object.keys(values).map(key => - inferTypeFromValues(key, values[key]) - ); + return Object.keys(values).map(key => ({ + ...inferTypeFromValues(key, values[key]), + views: ['list', 'create', 'edit', 'show'], + })); }; diff --git a/packages/ra-no-code/src/builders/Create.tsx b/packages/ra-no-code/src/builders/Create.tsx index 299ca2853fd..dd87f57dd84 100644 --- a/packages/ra-no-code/src/builders/Create.tsx +++ b/packages/ra-no-code/src/builders/Create.tsx @@ -21,9 +21,9 @@ export const CreateForm = (props: Omit) => { return ( - {resourceConfiguration.fields.map(definition => - getInputFromFieldDefinition(definition) - )} + {resourceConfiguration.fields + .filter(definition => definition.views.includes('create')) + .map(definition => getInputFromFieldDefinition(definition))} ); }; diff --git a/packages/ra-no-code/src/builders/Edit.tsx b/packages/ra-no-code/src/builders/Edit.tsx index 50a3bea6036..b3550c3ae65 100644 --- a/packages/ra-no-code/src/builders/Edit.tsx +++ b/packages/ra-no-code/src/builders/Edit.tsx @@ -21,9 +21,9 @@ export const EditForm = (props: Omit) => { return ( - {resourceConfiguration.fields.map(definition => - getInputFromFieldDefinition(definition) - )} + {resourceConfiguration.fields + .filter(definition => definition.views.includes('edit')) + .map(definition => getInputFromFieldDefinition(definition))} ); }; diff --git a/packages/ra-no-code/src/builders/List.tsx b/packages/ra-no-code/src/builders/List.tsx index d9dcacb0e2a..9d066c49d82 100644 --- a/packages/ra-no-code/src/builders/List.tsx +++ b/packages/ra-no-code/src/builders/List.tsx @@ -22,9 +22,9 @@ export const Datagrid = (props: Omit) => { return ( - {resourceConfiguration.fields.map(definition => - getFieldFromFieldDefinition(definition) - )} + {resourceConfiguration.fields + .filter(definition => definition.views.includes('list')) + .map(definition => getFieldFromFieldDefinition(definition))} ); }; From eb47868eb574a3071d27f1eda8e6db806788326b Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 29 Apr 2021 10:07:22 +0200 Subject: [PATCH 03/14] Introduce Show View --- packages/ra-no-code/src/Admin.tsx | 3 ++- packages/ra-no-code/src/builders/Show.tsx | 29 +++++++++++++++++++++++ packages/ra-no-code/src/builders/index.ts | 1 + 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 packages/ra-no-code/src/builders/Show.tsx diff --git a/packages/ra-no-code/src/Admin.tsx b/packages/ra-no-code/src/Admin.tsx index ae9431b0ac0..784094f51f3 100644 --- a/packages/ra-no-code/src/Admin.tsx +++ b/packages/ra-no-code/src/Admin.tsx @@ -5,7 +5,7 @@ import { Resource, } from 'react-admin'; import localStorageDataProvider from 'ra-data-local-storage'; -import { Create, Edit, List } from './builders'; +import { Create, Edit, List, Show } from './builders'; import { useResourcesConfiguration, ResourceConfigurationPage, @@ -51,6 +51,7 @@ const InnerAdmin = (props: AdminProps) => { list={List} edit={Edit} create={Create} + show={Show} /> )) : undefined} diff --git a/packages/ra-no-code/src/builders/Show.tsx b/packages/ra-no-code/src/builders/Show.tsx new file mode 100644 index 00000000000..4f07246d063 --- /dev/null +++ b/packages/ra-no-code/src/builders/Show.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { useResourceContext } from 'ra-core'; +import { + Show as RaShow, + ShowProps, + SimpleShowLayout, + SimpleShowLayoutProps, +} from 'ra-ui-materialui'; +import { useResourceConfiguration } from '../ResourceConfiguration'; +import { getFieldFromFieldDefinition } from './getFieldFromFieldDefinition'; + +export const Show = (props: ShowProps) => ( + + + +); + +export const ShowForm = (props: Omit) => { + const resource = useResourceContext(props); + const [resourceConfiguration] = useResourceConfiguration(resource); + + return ( + + {resourceConfiguration.fields + .filter(definition => definition.views.includes('show')) + .map(definition => getFieldFromFieldDefinition(definition))} + + ); +}; diff --git a/packages/ra-no-code/src/builders/index.ts b/packages/ra-no-code/src/builders/index.ts index aada1d13318..c9663ead7f4 100644 --- a/packages/ra-no-code/src/builders/index.ts +++ b/packages/ra-no-code/src/builders/index.ts @@ -3,3 +3,4 @@ export * from './Edit'; export * from './List'; export * from './getFieldFromFieldDefinition'; export * from './getInputFromFieldDefinition'; +export * from './Show'; From 1347e04bb841f816d23f0ca015b0a9e8b25fa521 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 29 Apr 2021 16:58:01 +0200 Subject: [PATCH 04/14] Fix date inference and add more detailed object inference --- packages/ra-core/package.json | 2 +- packages/ra-core/src/inference/assertions.ts | 5 +++-- .../src/inference/inferTypeFromValues.ts | 17 +++++++++++++---- packages/ra-no-code/package.json | 2 +- yarn.lock | 7 ++++++- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 0bbac672e67..424f4fdad7e 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -66,7 +66,7 @@ }, "dependencies": { "classnames": "~2.2.5", - "date-fns": "^1.29.0", + "date-fns": "^2.21.1", "eventemitter3": "^3.0.0", "inflection": "~1.12.0", "lodash": "~4.17.5", diff --git a/packages/ra-core/src/inference/assertions.ts b/packages/ra-core/src/inference/assertions.ts index fb66f18cabd..2bc884a260b 100644 --- a/packages/ra-core/src/inference/assertions.ts +++ b/packages/ra-core/src/inference/assertions.ts @@ -1,4 +1,5 @@ -import parseDate from 'date-fns/parse'; +import isValid from 'date-fns/isValid'; +import parseDate from 'date-fns/parseISO'; export const isNumeric = (value: any) => !isNaN(parseFloat(value)) && isFinite(value); @@ -45,7 +46,7 @@ export const isDate = (value: any) => !value || value instanceof Date; export const valuesAreDate = (values: any[]) => values.every(isDate); export const isDateString = (value: any) => - !value || (typeof value === 'string' && !isNaN(parseDate(value).getDate())); + !value || (typeof value === 'string' && isValid(parseDate(value))); export const valuesAreDateString = (values: any[]) => values.every(isDateString); diff --git a/packages/ra-core/src/inference/inferTypeFromValues.ts b/packages/ra-core/src/inference/inferTypeFromValues.ts index c8fc75f4c31..56cbdfff33a 100644 --- a/packages/ra-core/src/inference/inferTypeFromValues.ts +++ b/packages/ra-core/src/inference/inferTypeFromValues.ts @@ -33,6 +33,7 @@ export const InferenceTypes = [ 'richText', 'string', 'url', + 'object', ] as const; export type PossibleInferredElementTypes = typeof InferenceTypes[number]; @@ -170,10 +171,18 @@ export const inferTypeFromValues = ( } if (valuesAreObject(values)) { // we need to go deeper - // Arbitrarily, choose the first prop of the first object - const propName = Object.keys(values[0]).shift(); - const leafValues = values.map(v => v[propName]); - return inferTypeFromValues(`${name}.${propName}`, leafValues); + // Arbitrarily, choose the first object + // FIXME bad visual representation + return { + type: 'object', + props: { source: name }, + children: Object.keys(values[0]).map(leafName => + inferTypeFromValues( + leafName, + values.map(value => value[leafName]) + ) + ), + }; } return { type: 'string', props: { source: name } }; }; diff --git a/packages/ra-no-code/package.json b/packages/ra-no-code/package.json index 86d867250cb..230cb0a7fe6 100644 --- a/packages/ra-no-code/package.json +++ b/packages/ra-no-code/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "classnames": "~2.2.5", - "date-fns": "^1.29.0", + "date-fns": "^2.21.1", "inflection": "~1.12.0", "lodash": "~4.17.5", "react-admin": "^3.14.5", diff --git a/yarn.lock b/yarn.lock index 3b24a0f8521..d937813357c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6592,7 +6592,7 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -date-fns@^1.27.2, date-fns@^1.29.0: +date-fns@^1.27.2: version "1.30.1" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== @@ -6602,6 +6602,11 @@ date-fns@^2.0.1: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.9.0.tgz#d0b175a5c37ed5f17b97e2272bbc1fa5aec677d2" integrity sha512-khbFLu/MlzLjEzy9Gh8oY1hNt/Dvxw3J6Rbc28cVoYWQaC1S3YI4xwkF9ZWcjDLscbZlY9hISMr66RFzZagLsA== +date-fns@^2.21.1: + version "2.21.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0" + integrity sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA== + date-fns@~1.29.0: version "1.29.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" From b0e103a8e277264d25a657e6feb4ffe54ea4cddc Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 29 Apr 2021 16:58:26 +0200 Subject: [PATCH 05/14] Prepare data imported from csv --- .../src/ui/useImportResourceFromCsv.tsx | 61 +++++++++++++++---- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx b/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx index 337327cf23b..2f90efa1c54 100644 --- a/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx +++ b/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { parse } from 'papaparse'; -import { Record, useDataProvider } from 'ra-core'; +import { getValuesFromRecords, Record, useDataProvider } from 'ra-core'; +import set from 'lodash/set'; import { useResourcesConfiguration, @@ -24,23 +25,23 @@ export const useImportResourceFromCsv = ( setParsing(true); parse(file, { header: true, - complete: async ({ data }) => { + skipEmptyLines: true, + complete: async ({ data, meta }) => { const resourceAlreadyExists = !!resources[resource]; - + const records = sanitizeRecords( + data.filter(record => !!record.id), + meta + ); + console.log({ records }); await Promise.all( - data.map(record => { - if (record.id) { - return dataProvider - .create(resource, { - data: record, - }) - .catch(error => console.error(error)); - } - return Promise.resolve(); + records.map(record => { + return dataProvider.create(resource, { + data: record, + }); }) ); setParsing(false); - const fields = getFieldDefinitionsFromRecords(data); + const fields = getFieldDefinitionsFromRecords(records); addResource({ name: resource, fields }); onImportCompleted({ resource, resourceAlreadyExists }); }, @@ -58,3 +59,37 @@ type ImportCompletedHandler = ({ resourceAlreadyExists: boolean; resource: string; }) => void; + +const sanitizeRecords = ( + records: Record[], + { fields }: { fields: string[] } +): Record[] => { + const values = getValuesFromRecords(records); + return fields.reduce( + (newRecords, field) => sanitizeRecord(newRecords, values, field), + [...records] + ); +}; + +const sanitizeRecord = (records, values, field) => { + if (field.split('.').length > 1) { + return records.map(record => { + let { [field]: pathField, ...newRecord } = record; + return set(newRecord, field, record[field]); + }); + } + + const fieldValues = values[field]; + + if ( + fieldValues.some(value => + ['false', 'true'].includes(value.toString().toLowerCase()) + ) + ) { + return records.map(record => + set(record, field, Boolean(record[field])) + ); + } + + return records; +}; From 0c0b492fc2502aa298c064c4966f6bed267e0f46 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 29 Apr 2021 16:58:37 +0200 Subject: [PATCH 06/14] Add support for object edition --- .../builders/getInputFromFieldDefinition.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/ra-no-code/src/builders/getInputFromFieldDefinition.tsx b/packages/ra-no-code/src/builders/getInputFromFieldDefinition.tsx index e23fedcd049..30eddd7b3da 100644 --- a/packages/ra-no-code/src/builders/getInputFromFieldDefinition.tsx +++ b/packages/ra-no-code/src/builders/getInputFromFieldDefinition.tsx @@ -9,20 +9,21 @@ import { } from 'ra-ui-materialui'; export const getInputFromFieldDefinition = ( - definition: InferredElementDescription + definition: InferredElementDescription, + keyPrefix?: string ) => { switch (definition.type) { case 'date': return ( ); case 'email': return ( @@ -30,37 +31,46 @@ export const getInputFromFieldDefinition = ( case 'boolean': return ( ); case 'number': return ( ); case 'image': return ( ); case 'url': return ( ); + case 'object': + if (Array.isArray(definition.children)) { + return definition.children.map((child, index) => + getInputFromFieldDefinition(child, index.toString()) + ); + } + return <>{getInputFromFieldDefinition(definition.children)}; default: return ( ); } }; + +const getKey = (prefix, source) => (prefix ? `${prefix}_${source}` : source); From 6043944ee7e2988af898d05692094cbb10d3460f Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 30 Apr 2021 12:33:06 +0200 Subject: [PATCH 07/14] Update tests --- .../src/inference/inferTypesFromValues.spec.ts | 14 ++++++++++++-- packages/ra-no-code/src/Admin.spec.tsx | 3 +-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/ra-core/src/inference/inferTypesFromValues.spec.ts b/packages/ra-core/src/inference/inferTypesFromValues.spec.ts index f3b1ea6344f..35680b3ff19 100644 --- a/packages/ra-core/src/inference/inferTypesFromValues.spec.ts +++ b/packages/ra-core/src/inference/inferTypesFromValues.spec.ts @@ -159,8 +159,18 @@ describe('inferTypeFromValues', () => { { bar: 3, baz: 4 }, ]) ).toEqual({ - type: 'number', - props: { source: 'foo.bar' }, + type: 'object', + props: { source: 'foo' }, + children: [ + { + type: 'number', + props: { source: 'bar' }, + }, + { + type: 'number', + props: { source: 'baz' }, + }, + ], }); }); }); diff --git a/packages/ra-no-code/src/Admin.spec.tsx b/packages/ra-no-code/src/Admin.spec.tsx index 2d2bc2311a2..5004fd8bb8a 100644 --- a/packages/ra-no-code/src/Admin.spec.tsx +++ b/packages/ra-no-code/src/Admin.spec.tsx @@ -82,8 +82,7 @@ describe('Admin', () => { getByText('Reference', { selector: 'th *' }); getByText('Date', { selector: 'th *' }); getByText('Customer', { selector: 'th *' }); - getByText('Basket.product', { selector: 'th *' }); - getByText('Basket.quantity', { selector: 'th *' }); + getByText('Basket', { selector: 'th *' }); getByText('Total ex taxes', { selector: 'th *' }); getByText('Delivery fees', { selector: 'th *' }); getByText('Tax rate', { selector: 'th *' }); From a8b160fd3107fec3ae4a38210ae3a5af6e7cef57 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 30 Apr 2021 12:33:13 +0200 Subject: [PATCH 08/14] Cleanup --- packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx b/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx index 2f90efa1c54..68203376279 100644 --- a/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx +++ b/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx @@ -32,7 +32,6 @@ export const useImportResourceFromCsv = ( data.filter(record => !!record.id), meta ); - console.log({ records }); await Promise.all( records.map(record => { return dataProvider.create(resource, { From bc3a8a4602ceb9322293c6407ad4fdcecfe38b49 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 5 May 2021 10:42:21 +0200 Subject: [PATCH 09/14] Apply review --- packages/ra-core/package.json | 2 +- packages/ra-core/src/inference/assertions.ts | 4 +-- .../src/inference/inferTypeFromValues.ts | 17 +++------ packages/ra-no-code/package.json | 2 +- .../FieldConfiguration/FieldTypeInput.tsx | 12 +++++++ .../FieldConfiguration/FieldViewsInput.tsx | 31 ++++++++++++++++ .../FieldConfiguration/index.ts | 2 ++ .../FieldConfigurationFormSection.tsx | 35 +++---------------- .../ResourceConfiguration.tsx | 4 +-- yarn.lock | 7 +--- 10 files changed, 60 insertions(+), 56 deletions(-) create mode 100644 packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/FieldTypeInput.tsx create mode 100644 packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/FieldViewsInput.tsx create mode 100644 packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/index.ts diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 424f4fdad7e..0bbac672e67 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -66,7 +66,7 @@ }, "dependencies": { "classnames": "~2.2.5", - "date-fns": "^2.21.1", + "date-fns": "^1.29.0", "eventemitter3": "^3.0.0", "inflection": "~1.12.0", "lodash": "~4.17.5", diff --git a/packages/ra-core/src/inference/assertions.ts b/packages/ra-core/src/inference/assertions.ts index 2bc884a260b..bbabb93263b 100644 --- a/packages/ra-core/src/inference/assertions.ts +++ b/packages/ra-core/src/inference/assertions.ts @@ -1,5 +1,5 @@ -import isValid from 'date-fns/isValid'; -import parseDate from 'date-fns/parseISO'; +import isValid from 'date-fns/is_valid'; +import parseDate from 'date-fns/parse'; export const isNumeric = (value: any) => !isNaN(parseFloat(value)) && isFinite(value); diff --git a/packages/ra-core/src/inference/inferTypeFromValues.ts b/packages/ra-core/src/inference/inferTypeFromValues.ts index 56cbdfff33a..b7a7c3d18ef 100644 --- a/packages/ra-core/src/inference/inferTypeFromValues.ts +++ b/packages/ra-core/src/inference/inferTypeFromValues.ts @@ -170,19 +170,10 @@ export const inferTypeFromValues = ( return { type: 'number', props: { source: name } }; } if (valuesAreObject(values)) { - // we need to go deeper - // Arbitrarily, choose the first object - // FIXME bad visual representation - return { - type: 'object', - props: { source: name }, - children: Object.keys(values[0]).map(leafName => - inferTypeFromValues( - leafName, - values.map(value => value[leafName]) - ) - ), - }; + /// Arbitrarily, choose the first prop of the first object + const propName = Object.keys(values[0]).shift(); + const leafValues = values.map(v => v[propName]); + return inferTypeFromValues(`${name}.${propName}`, leafValues); } return { type: 'string', props: { source: name } }; }; diff --git a/packages/ra-no-code/package.json b/packages/ra-no-code/package.json index 230cb0a7fe6..86d867250cb 100644 --- a/packages/ra-no-code/package.json +++ b/packages/ra-no-code/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "classnames": "~2.2.5", - "date-fns": "^2.21.1", + "date-fns": "^1.29.0", "inflection": "~1.12.0", "lodash": "~4.17.5", "react-admin": "^3.14.5", diff --git a/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/FieldTypeInput.tsx b/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/FieldTypeInput.tsx new file mode 100644 index 00000000000..b09d5718005 --- /dev/null +++ b/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/FieldTypeInput.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { InferenceTypes } from 'ra-core'; +import { FieldProps, SelectInput } from 'ra-ui-materialui'; + +export const FieldTypeInput = (props: FieldProps) => ( + +); + +const INFERENCE_TYPES = InferenceTypes.map(type => ({ + id: type, + name: type, +})); diff --git a/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/FieldViewsInput.tsx b/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/FieldViewsInput.tsx new file mode 100644 index 00000000000..66fee6625b0 --- /dev/null +++ b/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/FieldViewsInput.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { CheckboxGroupInput, FieldProps } from 'ra-ui-materialui'; + +export const FieldViewsInput = (props: FieldProps) => ( + +); + +const VIEWS = [ + { + id: 'list', + name: 'List', + }, + { + id: 'edit', + name: 'Edit', + }, + { + id: 'create', + name: 'Create', + }, + { + id: 'show', + name: 'Show', + }, +]; + +const VIEWS_INITIAL_VALUE = ['list', 'edit', 'create', 'show']; diff --git a/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/index.ts b/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/index.ts new file mode 100644 index 00000000000..834dabd86f7 --- /dev/null +++ b/packages/ra-no-code/src/ResourceConfiguration/FieldConfiguration/index.ts @@ -0,0 +1,2 @@ +export * from './FieldTypeInput'; +export * from './FieldViewsInput'; diff --git a/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx index 090e5e177ed..ae3f81424d5 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx +++ b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx @@ -6,6 +6,8 @@ import { } from 'ra-core'; import { CheckboxGroupInput, SelectInput, TextInput } from 'ra-ui-materialui'; import { CardContent } from '@material-ui/core'; +import { FieldTypeInput } from './FieldConfiguration/FieldTypeInput'; +import { FieldViewsInput } from './FieldConfiguration/FieldViewsInput'; export const FieldConfigurationFormSection = props => { const { index, field, resource } = props; @@ -30,45 +32,16 @@ export const FieldConfigurationFormSection = props => { fullWidth initialValue={translate(...labelArgs)} /> - - ); }; - -const INFERENCE_TYPES = InferenceTypes.map(type => ({ - id: type, - name: type, -})); - -const VIEWS = [ - { - id: 'list', - name: 'List', - }, - { - id: 'edit', - name: 'Edit', - }, - { - id: 'create', - name: 'Create', - }, - { - id: 'show', - name: 'Show', - }, -]; - -const VIEWS_INITIAL_VALUE = ['list', 'edit', 'create', 'show']; diff --git a/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx b/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx index 04d187e7b9b..52d19cbccfe 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx +++ b/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx @@ -73,7 +73,7 @@ export const ResourceConfigurationPage = ({ avatar={ { - // Here will go an icon selector + // TODO: Add an icon selector ( resourceConfiguration.label || resourceConfiguration.name @@ -82,7 +82,7 @@ export const ResourceConfigurationPage = ({ } action={ - // Will display a menu to delete the resource maybe ? + // TODO: Add a menu with resource related actions (delete, etc.) diff --git a/yarn.lock b/yarn.lock index d937813357c..3b24a0f8521 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6592,7 +6592,7 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -date-fns@^1.27.2: +date-fns@^1.27.2, date-fns@^1.29.0: version "1.30.1" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== @@ -6602,11 +6602,6 @@ date-fns@^2.0.1: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.9.0.tgz#d0b175a5c37ed5f17b97e2272bbc1fa5aec677d2" integrity sha512-khbFLu/MlzLjEzy9Gh8oY1hNt/Dvxw3J6Rbc28cVoYWQaC1S3YI4xwkF9ZWcjDLscbZlY9hISMr66RFzZagLsA== -date-fns@^2.21.1: - version "2.21.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0" - integrity sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA== - date-fns@~1.29.0: version "1.29.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" From a08ce5b088514d1ff5953543d2e8df81c88e04d7 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 5 May 2021 10:53:06 +0200 Subject: [PATCH 10/14] Fix unit tests --- .../src/inference/inferTypesFromValues.spec.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/ra-core/src/inference/inferTypesFromValues.spec.ts b/packages/ra-core/src/inference/inferTypesFromValues.spec.ts index 35680b3ff19..f3b1ea6344f 100644 --- a/packages/ra-core/src/inference/inferTypesFromValues.spec.ts +++ b/packages/ra-core/src/inference/inferTypesFromValues.spec.ts @@ -159,18 +159,8 @@ describe('inferTypeFromValues', () => { { bar: 3, baz: 4 }, ]) ).toEqual({ - type: 'object', - props: { source: 'foo' }, - children: [ - { - type: 'number', - props: { source: 'bar' }, - }, - { - type: 'number', - props: { source: 'baz' }, - }, - ], + type: 'number', + props: { source: 'foo.bar' }, }); }); }); From 9d73ae7902f75774ce7357284e109b982f50fe3c Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 5 May 2021 11:37:17 +0200 Subject: [PATCH 11/14] Fix no-code Admin tests --- packages/ra-no-code/src/Admin.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-no-code/src/Admin.spec.tsx b/packages/ra-no-code/src/Admin.spec.tsx index 5004fd8bb8a..b1d61deae84 100644 --- a/packages/ra-no-code/src/Admin.spec.tsx +++ b/packages/ra-no-code/src/Admin.spec.tsx @@ -82,7 +82,7 @@ describe('Admin', () => { getByText('Reference', { selector: 'th *' }); getByText('Date', { selector: 'th *' }); getByText('Customer', { selector: 'th *' }); - getByText('Basket', { selector: 'th *' }); + getByText('Basket.product', { selector: 'th *' }); getByText('Total ex taxes', { selector: 'th *' }); getByText('Delivery fees', { selector: 'th *' }); getByText('Tax rate', { selector: 'th *' }); From 1625f871ef2d21518671141af78660e7091c747c Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 5 May 2021 14:51:11 +0200 Subject: [PATCH 12/14] Add better error handling --- .../src/ui/ImportResourceDialog.tsx | 33 ++++---- .../src/ui/useImportResourceFromCsv.tsx | 84 ++++++++++++------- 2 files changed, 70 insertions(+), 47 deletions(-) diff --git a/packages/ra-no-code/src/ui/ImportResourceDialog.tsx b/packages/ra-no-code/src/ui/ImportResourceDialog.tsx index d8157e45aed..1707ef52ec3 100644 --- a/packages/ra-no-code/src/ui/ImportResourceDialog.tsx +++ b/packages/ra-no-code/src/ui/ImportResourceDialog.tsx @@ -12,7 +12,7 @@ import { } from '@material-ui/core'; import { useDropzone } from 'react-dropzone'; -import { useRefresh } from 'ra-core'; +import { useNotify, useRefresh } from 'ra-core'; import { useHistory } from 'react-router-dom'; import { useImportResourceFromCsv } from './useImportResourceFromCsv'; @@ -21,6 +21,7 @@ export const ImportResourceDialog = (props: ImportResourceDialogProps) => { const [resource, setResource] = useState(''); const history = useHistory(); const refresh = useRefresh(); + const notify = useNotify(); const handleClose = () => { if (props.onClose) { @@ -28,20 +29,7 @@ export const ImportResourceDialog = (props: ImportResourceDialogProps) => { } }; - const handleImportCompleted = ({ resourceAlreadyExists }) => { - handleClose(); - history.push(`/${resource}`); - - if (resourceAlreadyExists) { - // If we imported more records for an existing resource, - // we must refresh the list - refresh(); - } - }; - - const [parsing, importResource] = useImportResourceFromCsv( - handleImportCompleted - ); + const [parsing, importResource] = useImportResourceFromCsv(); const onDrop = (acceptedFiles: File[]) => { if (acceptedFiles.length > 0) { @@ -57,7 +45,20 @@ export const ImportResourceDialog = (props: ImportResourceDialogProps) => { event.preventDefault(); if (resource && file) { - importResource(resource, file); + importResource(resource, file) + .then(({ resource, resourceAlreadyExists }) => { + handleClose(); + history.push(`/${resource}`); + + if (resourceAlreadyExists) { + // If we imported more records for an existing resource, + // we must refresh the list + refresh(); + } + }) + .catch(() => { + notify('An error occured while handling this CSV file'); + }); } }; diff --git a/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx b/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx index 68203376279..b8717a3764a 100644 --- a/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx +++ b/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx @@ -1,6 +1,11 @@ import { useState } from 'react'; import { parse } from 'papaparse'; -import { getValuesFromRecords, Record, useDataProvider } from 'ra-core'; +import { + DataProvider, + getValuesFromRecords, + Record, + useDataProvider, +} from 'ra-core'; import set from 'lodash/set'; import { @@ -14,50 +19,67 @@ import { * @param onImportCompleted A function called once the import is completed. Receive an object containing the resource imported and the resourceAlreadyExists boolean. * @returns {[boolean, ImportResource]} */ -export const useImportResourceFromCsv = ( - onImportCompleted: ImportCompletedHandler -): [boolean, ImportResource] => { +export const useImportResourceFromCsv = (): [boolean, ImportResource] => { const [parsing, setParsing] = useState(false); const dataProvider = useDataProvider(); const [resources, { addResource }] = useResourcesConfiguration(); - const importResource = (resource: string, file: File) => { + const importResource = async (resource: string, file: File) => { setParsing(true); + const resourceAlreadyExists = !!resources[resource]; + const { data, meta } = await parseCSV(file); + const records = sanitizeRecords( + data.filter(record => !!record.id), + meta + ); + await Promise.all( + records.map(record => { + return dataProvider + .create(resource, { + data: record, + }) + .catch(error => { + // Ignore errors while adding a single record + console.error( + `Error while importing record ${JSON.stringify( + record, + null, + 4 + )}` + ); + }); + }) + ); + setParsing(false); + const fields = getFieldDefinitionsFromRecords(records); + addResource({ name: resource, fields }); + return { resource, resourceAlreadyExists }; + }; + + return [parsing, importResource]; +}; + +const parseCSV = (file: File): Promise<{ data: Record[]; meta: any }> => + new Promise((resolve, reject) => { parse(file, { header: true, skipEmptyLines: true, complete: async ({ data, meta }) => { - const resourceAlreadyExists = !!resources[resource]; - const records = sanitizeRecords( - data.filter(record => !!record.id), - meta - ); - await Promise.all( - records.map(record => { - return dataProvider.create(resource, { - data: record, - }); - }) - ); - setParsing(false); - const fields = getFieldDefinitionsFromRecords(records); - addResource({ name: resource, fields }); - onImportCompleted({ resource, resourceAlreadyExists }); + resolve({ data, meta }); + }, + error: error => { + reject(error); }, }); - }; - - return [parsing, importResource]; -}; + }); -type ImportResource = (resource: string, file: File) => void; -type ImportCompletedHandler = ({ - resourceAlreadyExists, - resource, -}: { +type ImportResource = ( + resource: string, + file: File +) => Promise<{ resourceAlreadyExists: boolean; resource: string; -}) => void; +}>; const sanitizeRecords = ( records: Record[], From 6c243ec044bba7476f6d17d7e291f0fd98e35fd4 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 5 May 2021 14:56:39 +0200 Subject: [PATCH 13/14] Clearer source handling for resource fields --- .../FieldConfigurationFormSection.tsx | 18 +++++++----------- .../ResourceConfiguration.tsx | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx index ae3f81424d5..d23e25299f6 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx +++ b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx @@ -1,16 +1,12 @@ import * as React from 'react'; -import { - getFieldLabelTranslationArgs, - InferenceTypes, - useTranslate, -} from 'ra-core'; -import { CheckboxGroupInput, SelectInput, TextInput } from 'ra-ui-materialui'; +import { getFieldLabelTranslationArgs, useTranslate } from 'ra-core'; +import { TextInput } from 'ra-ui-materialui'; import { CardContent } from '@material-ui/core'; import { FieldTypeInput } from './FieldConfiguration/FieldTypeInput'; import { FieldViewsInput } from './FieldConfiguration/FieldViewsInput'; export const FieldConfigurationFormSection = props => { - const { index, field, resource } = props; + const { sourcePrefix, field, resource } = props; const translate = useTranslate(); const labelArgs = getFieldLabelTranslationArgs({ source: field.props.source, @@ -21,24 +17,24 @@ export const FieldConfigurationFormSection = props => { return ( diff --git a/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx b/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx index 52d19cbccfe..9a18d2317f7 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx +++ b/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx @@ -136,7 +136,7 @@ export const ResourceConfigurationPage = ({ Date: Thu, 6 May 2021 15:06:54 +0200 Subject: [PATCH 14/14] Update useImportResourceFromCsv.tsx --- packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx b/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx index b8717a3764a..f94733be4cf 100644 --- a/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx +++ b/packages/ra-no-code/src/ui/useImportResourceFromCsv.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; import { parse } from 'papaparse'; import { - DataProvider, getValuesFromRecords, Record, useDataProvider,