Skip to content

Commit 60d8a42

Browse files
Merge pull request #117 from Tietokilta/fix/editor-perf
Rewrite editor using react-final-form
2 parents 4089166 + ee909aa commit 60d8a42

31 files changed

+1609
-1154
lines changed

package.json

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@
2525
"url": "https://github.com/Tietokilta/ilmomasiina/issues"
2626
},
2727
"dependencies": {
28-
"@typescript-eslint/eslint-plugin": "^5.48.2",
29-
"@typescript-eslint/parser": "^5.48.2",
30-
"eslint": "^8.32.0",
28+
"@typescript-eslint/eslint-plugin": "^6.7.3",
29+
"@typescript-eslint/parser": "^6.7.3",
30+
"eslint": "^8.50.0",
3131
"eslint-config-airbnb": "^19.0.4",
32-
"eslint-config-airbnb-typescript": "^17.0.0",
33-
"eslint-plugin-import": "^2.27.5",
34-
"eslint-plugin-jest": "^26.9.0",
32+
"eslint-config-airbnb-typescript": "^17.1.0",
33+
"eslint-plugin-import": "^2.28.1",
34+
"eslint-plugin-jest": "^27.4.0",
3535
"eslint-plugin-jsx-a11y": "^6.7.1",
3636
"eslint-plugin-promise": "^6.1.1",
37-
"eslint-plugin-react": "^7.32.1",
37+
"eslint-plugin-react": "^7.33.2",
3838
"eslint-plugin-react-hooks": "^4.6.0",
39-
"eslint-plugin-simple-import-sort": "^7.0.0",
39+
"eslint-plugin-simple-import-sort": "^10.0.0",
4040
"pnpm": "^7.25.0",
41-
"typescript": "~4.9"
41+
"typescript": "~5.2.2"
4242
},
4343
"browserslist": {
4444
"production": [
@@ -64,6 +64,9 @@
6464
"popper.js",
6565
"eslint"
6666
]
67+
},
68+
"overrides": {
69+
"@types/react": "^17.0.47"
6770
}
6871
}
6972
}

packages/ilmomasiina-components/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,21 @@
2929
"@types/lodash": "^4.14.182",
3030
"@types/react": "^17 || ^18",
3131
"bootstrap": "^4.6.1",
32-
"formik": "^2.2.9",
32+
"final-form": "^4.20.10",
33+
"formik": "^2.4.5",
34+
"i18next": "^22.4.11",
3335
"lodash": "^4.17.21",
3436
"moment": "^2.29.3",
3537
"moment-timezone": "^0.5.34",
3638
"react": "^17 || ^18",
3739
"react-bootstrap": "^1.6.5",
3840
"react-countdown-now": "^2.1.2",
3941
"react-dom": "^17 || ^18",
42+
"react-final-form": "^6.5.9",
43+
"react-i18next": "^12.2.0",
4044
"react-markdown": "^8.0.3",
4145
"react-toastify": "^8.2.0",
42-
"remark-gfm": "^3.0.1",
43-
"i18next": "^22.4.11",
44-
"react-i18next": "^12.2.0"
46+
"remark-gfm": "^3.0.1"
4547
},
4648
"devDependencies": {
4749
"rimraf": "^3.0.2",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React, { ReactNode } from 'react';
2+
3+
import { Col, Form, Row } from 'react-bootstrap';
4+
5+
export type BaseFieldRowProps = {
6+
/** The name of the field in the Formik data. */
7+
name: string;
8+
/** The label placed in the left column. */
9+
label?: string;
10+
/** The help string placed below the field. */
11+
help?: ReactNode;
12+
/** Whether the field is required. */
13+
required?: boolean;
14+
/** Error message rendered below the field. */
15+
error?: ReactNode;
16+
/** Extra feedback rendered below the field. Bring your own `Form.Control.Feedback`. */
17+
extraFeedback?: ReactNode;
18+
/** `true` to adjust the vertical alignment of the left column label for checkboxes/radios. */
19+
checkAlign?: boolean;
20+
children: ReactNode;
21+
};
22+
23+
export default function BaseFieldRow({
24+
name,
25+
label = '',
26+
help,
27+
required = false,
28+
extraFeedback,
29+
checkAlign,
30+
children,
31+
error,
32+
}: BaseFieldRowProps) {
33+
return (
34+
<Form.Group as={Row} controlId={name}>
35+
<Form.Label column sm="3" data-required={required} className={checkAlign ? 'pt-0' : ''}>{label}</Form.Label>
36+
<Col sm="9">
37+
{children}
38+
{error && (
39+
<Form.Control.Feedback type="invalid">
40+
{error}
41+
</Form.Control.Feedback>
42+
)}
43+
{extraFeedback}
44+
{help && <Form.Text muted>{help}</Form.Text>}
45+
</Col>
46+
</Form.Group>
47+
);
48+
}
Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
11
import React, { ComponentType, ReactNode } from 'react';
22

3-
import { Field, useFormikContext } from 'formik';
4-
import { Col, Form, Row } from 'react-bootstrap';
3+
import { Field, useField } from 'formik';
4+
import { Form } from 'react-bootstrap';
55

6-
type Props = {
7-
/** The name of the field in the Formik data. */
8-
name: string;
9-
/** The label placed in the left column. */
10-
label?: string;
11-
/** The help string placed below the field. */
12-
help?: ReactNode;
13-
/** Whether the field is required. */
14-
required?: boolean;
6+
import BaseFieldRow, { BaseFieldRowProps } from '../BaseFieldRow';
7+
8+
type Props = Omit<BaseFieldRowProps, 'error' | 'children'> & {
159
/** Overrides the real error message if the field has errors. */
1610
alternateError?: string;
17-
/** Extra feedback rendered below the field. Bring your own `Form.Control.Feedback`. */
18-
extraFeedback?: ReactNode;
19-
/** `true` to adjust the vertical alignment of the left column label for checkboxes/radios. */
20-
checkAlign?: boolean;
2111
/** Passed as `label` to the field component. Intended for checkboxes. */
2212
checkLabel?: ReactNode;
2313
/** The component or element to use as the field. Passed to Formik's `Field`. */
@@ -26,6 +16,7 @@ type Props = {
2616
children?: ReactNode;
2717
};
2818

19+
/** FieldRow for use with formik */
2920
export default function FieldRow<P = unknown>({
3021
name,
3122
label = '',
@@ -39,7 +30,7 @@ export default function FieldRow<P = unknown>({
3930
children,
4031
...props
4132
}: Props & P) {
42-
const { errors } = useFormikContext<any>();
33+
const [, { error }] = useField<any>(name);
4334

4435
let field: ReactNode;
4536
if (children) {
@@ -48,24 +39,21 @@ export default function FieldRow<P = unknown>({
4839
// Checkboxes have two labels: in the left column and next to the checkbox. Form.Check handles the latter for us
4940
// and calls it "label", but we still want to call the other one "label" for all other types of field. Therefore
5041
// we pass "checkLabel" to the field here.
51-
let fieldProps = props;
52-
if (checkLabel !== undefined) {
53-
fieldProps = { ...props, label: checkLabel };
54-
}
55-
field = <Field as={as} name={name} required={required} {...fieldProps} />;
42+
const overrideProps = checkLabel !== undefined ? { label: checkLabel } : {};
43+
field = <Field as={as} name={name} required={required} {...props} {...overrideProps} />;
5644
}
5745

5846
return (
59-
<Form.Group as={Row} controlId={name}>
60-
<Form.Label column sm="3" data-required={required} className={checkAlign ? 'pt-0' : ''}>{label}</Form.Label>
61-
<Col sm="9">
62-
{field}
63-
<Form.Control.Feedback type="invalid">
64-
{errors[name] && (alternateError || errors[name])}
65-
</Form.Control.Feedback>
66-
{extraFeedback}
67-
{help && <Form.Text muted>{help}</Form.Text>}
68-
</Col>
69-
</Form.Group>
47+
<BaseFieldRow
48+
name={name}
49+
label={label}
50+
help={help}
51+
required={required}
52+
error={error && (alternateError || error)}
53+
extraFeedback={extraFeedback}
54+
checkAlign={checkAlign}
55+
>
56+
{field}
57+
</BaseFieldRow>
7058
);
7159
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { ComponentType, ReactNode } from 'react';
2+
3+
import { Form } from 'react-bootstrap';
4+
import { useField, UseFieldConfig } from 'react-final-form';
5+
6+
import BaseFieldRow, { BaseFieldRowProps } from '../BaseFieldRow';
7+
8+
type Props = Omit<BaseFieldRowProps, 'error' | 'children'> & Pick<UseFieldConfig<any>, 'type'> & {
9+
/** Overrides the real error message if the field has errors. */
10+
alternateError?: string;
11+
/** Passed as `label` to the field component. Intended for checkboxes. */
12+
checkLabel?: ReactNode;
13+
/** The component or element to use as the field. Passed to Formik's `Field`. */
14+
as?: ComponentType<any> | string;
15+
/** useField() config. */
16+
config?: UseFieldConfig<any>;
17+
/** If given, this is used as the field. */
18+
children?: ReactNode;
19+
};
20+
21+
/** FieldRow for use with react-final-form */
22+
export default function FinalFieldRow<P = unknown>({
23+
name,
24+
label = '',
25+
help,
26+
required = false,
27+
alternateError,
28+
extraFeedback,
29+
checkAlign,
30+
checkLabel,
31+
as: Component = Form.Control,
32+
children,
33+
type,
34+
config,
35+
...props
36+
}: Props & P) {
37+
const { input, meta: { error, invalid } } = useField(name, { type, ...config });
38+
39+
let field: ReactNode;
40+
if (children) {
41+
field = children;
42+
} else {
43+
// Checkboxes have two labels: in the left column and next to the checkbox. Form.Check handles the latter for us
44+
// and calls it "label", but we still want to call the other one "label" for all other types of field. Therefore
45+
// we pass "checkLabel" to the field here.
46+
const overrideProps = checkLabel !== undefined ? { label: checkLabel } : {};
47+
field = <Component required={required} {...props} {...input} {...overrideProps} />;
48+
}
49+
50+
return (
51+
<BaseFieldRow
52+
name={name}
53+
label={label}
54+
help={help}
55+
required={required}
56+
error={invalid && (alternateError || error)}
57+
extraFeedback={extraFeedback}
58+
checkAlign={checkAlign}
59+
>
60+
{field}
61+
</BaseFieldRow>
62+
);
63+
}

packages/ilmomasiina-components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export { default as SingleEvent } from './routes/SingleEvent';
1717
export { default as EditSignup } from './routes/EditSignup';
1818

1919
export { default as FieldRow } from './components/FieldRow';
20+
export { default as FinalFieldRow } from './components/FinalFieldRow';

packages/ilmomasiina-frontend/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,19 @@
2323
"@types/node": "^16.11.41",
2424
"@types/node-sass": "^4.11.2",
2525
"@types/react": "^17.0.47",
26-
"@types/react-csv": "^1.1.2",
2726
"@types/react-datepicker": "^4.4.2",
2827
"@types/react-dom": "^17.0.17",
2928
"@types/react-redux": "^7.1.24",
3029
"@types/react-router-dom": "^5.3.3",
31-
"@welldone-software/why-did-you-render": "^6.2.3",
3230
"bootstrap": "^4.6.1",
3331
"connected-react-router": "^6.9.2",
32+
"csv-stringify": "^6.4.2",
3433
"date-fns": "^2.28.0",
3534
"esbuild": "^0.14.46",
3635
"esbuild-sass-plugin": "^2.2.6",
37-
"formik": "^2.2.9",
36+
"final-form": "^4.20.10",
37+
"final-form-arrays": "^3.1.0",
38+
"formik": "^2.4.5",
3839
"history": "^4.10.1",
3940
"i18next": "^22.4.11",
4041
"i18next-browser-languagedetector": "^7.0.1",
@@ -43,9 +44,10 @@
4344
"moment-timezone": "^0.5.34",
4445
"react": "^17.0.2",
4546
"react-bootstrap": "^1.6.5",
46-
"react-csv": "^2.2.2",
4747
"react-datepicker": "^4.8.0",
4848
"react-dom": "^17.0.2",
49+
"react-final-form": "^6.5.9",
50+
"react-final-form-arrays": "^3.1.4",
4951
"react-i18next": "^12.2.0",
5052
"react-redux": "^7.2.8",
5153
"react-router": "^5.3.3",

packages/ilmomasiina-frontend/src/index.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@ import { configure } from '@tietokilta/ilmomasiina-components';
1010
import AppContainer from './containers/AppContainer';
1111
import { apiUrl } from './paths';
1212

13-
if (!PROD) {
14-
// eslint-disable-next-line global-require
15-
const whyDidYouRender = require('@welldone-software/why-did-you-render');
16-
whyDidYouRender(React);
17-
}
18-
1913
if (PROD && SENTRY_DSN) {
2014
Sentry.init({ dsn: SENTRY_DSN });
2115
}

packages/ilmomasiina-frontend/src/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@
181181
"editor.emails.verificationEmail": "Confirmation email",
182182
"editor.signups.noSignups": "There are currently no signups for this event. When people sign up, they will appear here.",
183183
"editor.signups.download": "Download list of participants",
184+
"editor.signups.download.filename": "{{event}} participants.csv",
184185
"editor.signups.column.firstName": "First name",
185186
"editor.signups.column.lastName": "Last name",
186187
"editor.signups.column.email": "Email",
@@ -189,6 +190,7 @@
189190
"editor.signups.column.delete": "Delete",
190191
"editor.signups.action.delete": "Delete",
191192
"editor.signups.action.delete.confirm": "Are you sure? This cannot be undone.",
193+
"editor.signups.unconfirmed": "Unconfirmed",
192194
"editor.editConflict.title": "Conflicting edits",
193195
"editor.editConflict.info1": "Another user or tab has edited this event at <1>{{time}}</1>.",
194196
"editor.editConflict.info1.withDeleted": "Another user or tab has edited this event at <1>{{time}}</1> and deleted the following quotas or questions:",

packages/ilmomasiina-frontend/src/locales/fi.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@
183183
"editor.emails.verificationEmail": "Vahvistusviesti sähköpostiin",
184184
"editor.signups.noSignups": "Tapahtumaan ei vielä ole yhtään ilmoittautumista. Kun tapahtumaan tulee ilmoittautumisia, näet ne tästä.",
185185
"editor.signups.download": "Lataa osallistujalista",
186+
"editor.signups.download.filename": "{{event}} osallistujalista.csv",
186187
"editor.signups.column.firstName": "Etunimi",
187188
"editor.signups.column.lastName": "Sukunimi",
188189
"editor.signups.column.email": "Sähköposti",
@@ -191,6 +192,7 @@
191192
"editor.signups.column.delete": "Poista",
192193
"editor.signups.action.delete": "Poista",
193194
"editor.signups.action.delete.confirm": "Oletko varma? Poistamista ei voi perua.",
195+
"editor.signups.unconfirmed": "Vahvistamatta",
194196
"editor.editConflict.title": "Päällekkäinen muokkaus",
195197
"editor.editConflict.info1": "Toinen käyttäjä tai välilehti on muokannut tätä tapahtumaa <1>{{time}}</1>.",
196198
"editor.editConflict.info1.withDeleted": "Toinen käyttäjä tai välilehti on muokannut tätä tapahtumaa <1>{{time}}</1> ja poistanut seuraavat kiintiöt tai kysymykset:",

0 commit comments

Comments
 (0)