Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#81 (1) JsonForms skeleton #148

Merged
merged 21 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions client/packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
"@capacitor/core": "^3.5.1",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@jsonforms/core": "^3.0.0-beta.3",
"@jsonforms/material-renderers": "^3.0.0-beta.3",
"@jsonforms/react": "^3.0.0-beta.3",
"@material-ui/icons": "^4.11.3",
"@mui/icons-material": "^5.8.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the icons, I'll try and remove in next PR

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please! the icons packages is huge and we have deliberately created only the icons we need in order to avoid it

"@mui/lab": "^5.0.0-alpha.59",
"@mui/material": "^5.2.3",
"@openmsupply-client/config": "^0.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface ButtonProps {
label: string;
width?: string;
height?: string;
color?: 'primary' | 'secondary' | undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll notice this isn't used in this PR -- I'm using it for the Arrays to create the "Add item" button -- in next PR

sx?: SxProps;
}

Expand All @@ -18,6 +19,7 @@ export const IconButton: React.FC<ButtonProps> = ({
label,
width,
height,
color,
sx,
}) => (
<Tooltip title={disabled ? '' : label}>
Expand All @@ -27,6 +29,7 @@ export const IconButton: React.FC<ButtonProps> = ({
onClick={onClick}
aria-label={label}
size="small"
color={color}
>
{icon}
</MuiIconButton>
Expand Down
35 changes: 35 additions & 0 deletions client/packages/common/src/ui/forms/JsonForms/components/Date.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { rankWith, ControlProps, isDateControl } from '@jsonforms/core';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { FormLabel, Box } from '@mui/material';
import { BaseDatePickerInput } from '@openmsupply-client/common';

export const dateTester = rankWith(5, isDateControl);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as a new user to this - what is a tester?
Can you add a comment to explain?
Could they be grouped in a testers file? Would that make it clearer? or is it helpful to have them next to the component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tester -- function that returns a number based on whether or not a predicate is true (In this case it will return 5 if isDateControl, otherwise it returns -1). Then for each form element, the tester that returns the highest number gets rendered by the component matched to that tester (in "renderers" prop), i.e.

{ tester: stringTester, renderer: TextField },
{ tester: selectTester, renderer: Selector },
{ tester: groupTester, renderer: Group },
{ tester: labelTester, renderer: Label },
{ tester: dateTester, renderer: Date },
{ tester: arrayTester, renderer: Array }

I thought since we're generally going to have testers coupled to components in a one-to-one manner, it made sense to keep each tester with its associated component.

I did make a more generic tester for matching custom fields (in uiSchema) to components, but it wasn't being used in here. I might include it in a later PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, right. thanks - wasn't clear to me from the name alone!


const UIComponent = (props: ControlProps) => {
const { data, handleChange, label, path } = props;

return (
<Box
display="flex"
alignItems="center"
gap={2}
justifyContent="space-around"
style={{ minWidth: 300 }}
marginTop={1}
>
<Box flex={1} style={{ textAlign: 'end' }} flexBasis="40%">
<FormLabel sx={{ fontWeight: 'bold' }}>{label}:</FormLabel>
</Box>
<Box flex={1} flexBasis="60%">
<BaseDatePickerInput
value={data}
onChange={e => handleChange(path, e)}
inputFormat="dd/MM/yyyy"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hard-coded format : can it be intl based? can it be configurable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually just copied straight from common/src/ui/components/inputs/DatePickers/DatePickerInput/DatePickerInput.tsx 😉

So yeah, we should localise them both at some point.

/>
</Box>
</Box>
);
};

export const Date = withJsonFormsControlProps(UIComponent);
38 changes: 38 additions & 0 deletions client/packages/common/src/ui/forms/JsonForms/components/Group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { rankWith, uiTypeIs, LayoutProps, GroupLayout } from '@jsonforms/core';
import { withJsonFormsLayoutProps } from '@jsonforms/react';
import { MaterialLayoutRenderer } from '@jsonforms/material-renderers';
import { Box, Typography } from '@mui/material';

export const groupTester = rankWith(4, uiTypeIs('Group'));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, these things aren't making sense. what is 4 here?


const UIComponent = (props: LayoutProps) => {
const { uischema, schema, visible, renderers, path } = props;

const layoutProps = {
elements: (uischema as GroupLayout).elements,
schema: schema,
path: path,
direction: 'column' as 'column' | 'row',
visible: visible,
uischema: uischema,
renderers: renderers,
};
return (
<Box
sx={{
maxWidth: 500,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the maxWidth is fairly specific, just wondering if that will cause problems later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved these values into a "styleConstants" file so we can update them easily if we need to.

Also -- if we need a wider "Group" in future, we can always make "width" a new property in uiSchema which the component can use, or have an alternative component "WideGroup". (for one that spans 2 columns, perhaps?)

paddingLeft: 2,
paddingRight: 2,
marginBottom: 2,
}}
>
<Typography width="40%" fontSize="1.2em" textAlign="right">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we may have to use ems throughout 🤔

Could the font size here come from the theme?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, have re-styled the "subtitle" variant in theme with this fontSize -- didn't look like it was being used elsewhere.

Would like to organise the typography a bit better overall at some point: msupply-foundation/openmsupply-client#1149

<strong>{(uischema as GroupLayout).label}</strong>
</Typography>
<MaterialLayoutRenderer {...layoutProps} />
</Box>
);
};

export const Group = withJsonFormsLayoutProps(UIComponent);
53 changes: 53 additions & 0 deletions client/packages/common/src/ui/forms/JsonForms/components/Label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import {
LayoutProps,
rankWith,
UISchemaElement,
uiTypeIs,
} from '@jsonforms/core';
import { withJsonFormsLayoutProps } from '@jsonforms/react';
import { SxProps, Typography } from '@mui/material';
import { RegexUtils } from '@common/utils';

export const labelTester = rankWith(3, uiTypeIs('Label'));

type LabelVariant = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';

type LayoutPropsExtended = LayoutProps & {
uischema: UISchemaElement & {
sx?: SxProps;
text?: string;
variant?: LabelVariant;
};
};

const variants: { [key in LabelVariant]: SxProps } = {
h1: { fontSize: '1.8em', fontWeight: 'bold', textAlign: 'center' },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wider discussion: these should really come from the theme

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'd like to standardise h1, h2, etc, but I don't want to go overwriting theme values for those just yet as it'll probably have significant flow-on effects. To do though: msupply-foundation/openmsupply-client#1149

h2: { fontSize: '1.4em', fontWeight: 'bold', textAlign: 'center' },
h3: {
fontWeight: 'bold',
textAlign: 'right',
width: '40%',
height: '1.5em', // This shouldn't be necessary 🤷‍♂️
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't be necessary -- but it is. That's that weird quirk I showed you the other day where changing the % width affected the height for no apparent reason.

marginTop: '1em',
},
h4: {},
h5: {},
h6: {},
p: {},
};

const UIComponent = (props: LayoutPropsExtended) => {
const {
uischema: { variant = 'p', sx, text },
data,
} = props;
const variantStyles = variants[variant];
return (
<Typography sx={{ ...variantStyles, ...sx } as SxProps}>
{RegexUtils.stringSubstitution(text ?? '', data)}
</Typography>
CarlosNZ marked this conversation as resolved.
Show resolved Hide resolved
);
};

export const Label = withJsonFormsLayoutProps(UIComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import { rankWith, isEnumControl, ControlProps } from '@jsonforms/core';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { FormLabel, Box } from '@mui/material';
import { Select } from '@openmsupply-client/common';

export const selectTester = rankWith(4, isEnumControl);

const UIComponent = (props: ControlProps) => {
const { data, handleChange, label, schema, path } = props;

const options = schema.enum
? schema.enum.map((option: string) => ({
label: option,
value: option,
}))
: [];

return (
<Box
display="flex"
alignItems="center"
gap={2}
justifyContent="space-around"
style={{ minWidth: 300 }}
marginTop={0.5}
>
<Box flex={1} style={{ textAlign: 'end' }} flexBasis="40%">
<FormLabel sx={{ fontWeight: 'bold' }}>{label}:</FormLabel>
</Box>
<Box flex={1} flexBasis="60%">
<Select
sx={{ minWidth: 100 }}
options={options}
value={data}
onChange={e => handleChange(path, e.target.value)}
/>
</Box>
</Box>
);
};

export const Selector = withJsonFormsControlProps(UIComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './Group';
export * from './Label';
export * from './Text';
export * from './Select';
export * from './Date';
// export * from './Array';
23 changes: 23 additions & 0 deletions client/packages/common/src/ui/forms/JsonForms/components/text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { ControlProps, rankWith, schemaTypeIs } from '@jsonforms/core';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { DetailInputWithLabelRow } from '@openmsupply-client/common';

export const stringTester = rankWith(3, schemaTypeIs('string'));

const UIComponent = (props: ControlProps) => {
const { data, handleChange, label, path } = props;
return (
<DetailInputWithLabelRow
label={label}
inputProps={{
value: data,
sx: { margin: 0.5, width: '100%' },
disabled: false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this the default value for disabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes -- I think I put that in there as a reminder that I needed to update it with a dynamic value based on whether it's editable or now. I'll remove for now.

onChange: e => handleChange(path, e.target.value),
}}
/>
);
};

export const TextField = withJsonFormsControlProps(UIComponent);
1 change: 1 addition & 0 deletions client/packages/common/src/ui/forms/JsonForms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useJsonForms';
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"Patient": {
"id": "123",
"gender": "Male",
"firstName": "Carl",
"lastName": "Smith",
"healthCenter": {
"id": "999",
"name": "My Health Center"
},
"addresses": [
{
"key": "primary",
"address1": "111 Address Street",
"address2": "Auckland",
"country": "NZ"
},
{
"key": "secondary",
"address1": "222 Address",
"address2": "address2",
"country": "NZ"
}
],
"contactDetails": [
{
"key": "primary",
"address1": "111 Contact Street",
"address2": "Auckland",
"country": "NZ"
},
{
"key": "secondary",
"address1": "222 Contact",
"mobile": "+023456",
"email": "email",
"website": "website"
}
],
"socioEconomics": {
"education": "education",
"occupation": "occupation",
"literate": "literate"
},
"testingArray": [{ "field1": "ONE", "field2": "TWO" }],
"family": {
"maritalStatus": "single",
"nextOfKin": {
"id": "nextOfKinId",
"firstName": "namekin",
"lastName": "lastNameKin",
"addresses": [],
"contactDetails": [
{
"key": "primary",
"address1": "NOK contact",
"address2": "Auckland",
"country": "New Zealand"
}
],
"socioEconomics": {}
},
"caregiver": {
"id": "cargiverId",
"firstName": "caregiver",
"lastName": "caregiverLastName",
"addresses": [],
"contactDetails": [],
"socioEconomics": {}
},
"mother": {
"id": "motherId",
"firstName": "Jenny",
"lastName": "Shaw",
"addresses": [],
"contactDetails": [],
"socioEconomics": {}
}
}
}
}
Loading