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

Recordedit performance improvements #2389

Merged
merged 21 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
919e0ec
since sensors are delayed, they may see an empty row. Before this was…
jrchudy Dec 15, 2023
c22eef8
show clone form spinner. track number of forms shown using formsRef a…
jrchudy Dec 19, 2023
6b7e655
add comment describing when/how the useEffect triggers
jrchudy Dec 19, 2023
211349f
Merge branch 'master' into clone-spinner
jrchudy Dec 19, 2023
5c3a4db
add comment for addFormsEffect state variable
jrchudy Dec 19, 2023
1fea004
moved useEffect logic into other useEffect. updated comment
jrchudy Jan 8, 2024
f826f88
initialize to empty string if not defined
jrchudy Jan 8, 2024
fb662a1
rewrote addForm function. no reason to capture the 'previous' value o…
jrchudy Jan 8, 2024
3601182
Merge branch 'master' into clone-spinner
jrchudy Jan 9, 2024
2bbddc0
on clone, only set each field value instead of reseting the whole form
jrchudy Jan 9, 2024
e50523c
Merge branch 'clone-spinner' into recordedit-performance-improvement
jrchudy Jan 9, 2024
66842e5
datetime field refactored to not use watch. Changed clearErrors calls…
jrchudy Jan 12, 2024
555320e
memoize form-row so if props don't change, don't rerender component. …
jrchudy Jan 12, 2024
88fa6f0
merge conflicts
jrchudy Jan 12, 2024
8d5508d
memoize input switch
jrchudy Jan 13, 2024
bee9378
remove activeMultiForm and toggleActiveMultiForm from the recordedit …
jrchudy Jan 13, 2024
31850e6
undo change to add translateY
jrchudy Jan 13, 2024
461236b
fix bugs for timestamp and FK fields
jrchudy Jan 16, 2024
d09f4ad
another bug uncovered by the changes I made to improve performance. T…
jrchudy Jan 16, 2024
19abfb0
modify copyOrClear to take an optional funciton argument to use for s…
jrchudy Jan 17, 2024
c0c0017
Merge branch 'master' into recordedit-performance-improvement
jrchudy Jan 17, 2024
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
141 changes: 71 additions & 70 deletions src/components/input-switch/date-time-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import DateField from '@isrd-isi-edu/chaise/src/components/input-switch/date-fie

// hooks
import { useEffect } from 'react';
import { useController, useFormContext } from 'react-hook-form';
import { useController, useFormContext, useWatch } from 'react-hook-form';

// utils
import { ERROR_MESSAGES, formatDatetime, VALIDATE_VALUE_BY_TYPE } from '@isrd-isi-edu/chaise/src/utils/input-utils';
Expand Down Expand Up @@ -35,94 +35,95 @@ type DateTimeFieldProps = InputFieldProps & {

const DateTimeField = (props: DateTimeFieldProps): JSX.Element => {

const { setValue, control, clearErrors, watch, setError } = useFormContext();
// NOTE: Including these context properties causes this component to redraw every time a change to the form occurs
// Can this functionality be done in a different way with react-hook-form to prevent so much rerendering when the component hasn't changed?
const { setValue, control, clearErrors, setError, getFieldState } = useFormContext();
const dateVal = useWatch({ name: `${props.name}-date` });
const timeVal = useWatch({ name: `${props.name}-time` });

const DATE_TIME_FORMAT = props.hasTimezone ? dataFormats.datetime.return : dataFormats.timestamp;

useEffect(() => {

/**
* this will make sure we're updating the underlying value after
* each update to the date and time fields.
*
* NOTE: just claling setError will not mark the form as invalid and the form.
* NOTE: just calling setError will not mark the form as invalid and the form.
* when users submit the form, the validators on the input itself will trigger
* that's why I'm setting the values to something invalid so it can then invalidate
* them and disallow submit.
*/
const sub = watch((data, options) => {
const name = props.name

if (options.name && (options.name === `${name}-date` || options.name === `${name}-time`)) {
const dateVal = data[`${name}-date`];
let timeVal = data[`${name}-time`];

// if both are missing, the input is empty
if (!dateVal && !timeVal) {
clearErrors(name);
setValue(name, '');
return;
}

// if date is missing, this is invalid
if (!dateVal) {
setError(name, { type: 'custom', message: ERROR_MESSAGES.INVALID_DATE });
setValue(name, 'invalid-value');
return;
}
// otherwise validate the date value
else {
const err = VALIDATE_VALUE_BY_TYPE['date'](dateVal);
if (typeof err === 'string') {
setError(name, { type: 'custom', message: err });
setValue(name, 'invalid-value');
return;
}
}

// if only time is missing, just use 00:00:00 for it
if (!timeVal) {
timeVal = '00:00:00';
}
// otherwise validate the time value
else {
const err = VALIDATE_VALUE_BY_TYPE['time'](timeVal);
if (typeof err === 'string') {
setError(name, { type: 'custom', message: err });
setValue(name, 'invalid-value');
return;
}
}

/**
* concatenate date and time together
* since time can have multiple formats, we cannot simply concatenate the strings
* and have to rely on moment to do this for us.
*/
const date = windowRef.moment(dateVal, dataFormats.date);
const time = windowRef.moment(timeVal, dataFormats.time);
const dateTime = date.set({
hour: time.get('hour'),
minute: time.get('minute'),
second: time.get('second')
});

// adds the timezone info if needed
const valueToSet = dateTime.format(DATE_TIME_FORMAT);

clearErrors(name);
setValue(name, valueToSet);
const datetimeFieldState = getFieldState(props.name);

// if both are missing, the input is empty
if (!dateVal && !timeVal && !props.requiredInput) {
if (datetimeFieldState.error) clearErrors(props.name);
setValue(props.name, '');
return;
}

// if date is missing, this is invalid
if (!dateVal) {
setError(props.name, { type: 'custom', message: ERROR_MESSAGES.INVALID_DATE });
setValue(props.name, 'invalid-value');
return;
}
// otherwise validate the date value
else {
const err = VALIDATE_VALUE_BY_TYPE['date'](dateVal);
if (typeof err === 'string') {
setError(props.name, { type: 'custom', message: err });
setValue(props.name, 'invalid-value');
return;
}
}

// if only time is missing, use 00:00:00 for it
let timeValTemp = '';
if (!timeVal && !props.requiredInput) {
timeValTemp = '00:00:00';
}
// otherwise validate the time value
else {
if (!timeVal) {
setError(props.name, { type: 'custom', message: 'Please enter a valid time' });
setValue(props.name, 'invalid-value');
return;
}
const err = VALIDATE_VALUE_BY_TYPE['time'](timeVal);
if (typeof err === 'string') {
setError(props.name, { type: 'custom', message: err });
setValue(props.name, 'invalid-value');
return;
}
}

/**
* concatenate date and time together
* since time can have multiple formats, we cannot simply concatenate the strings
* and have to rely on moment to do this for us.
*/
const date = windowRef.moment(dateVal, dataFormats.date);
const time = windowRef.moment(timeValTemp ? timeValTemp : timeVal, dataFormats.time);
const dateTime = date.set({
hour: time.get('hour'),
minute: time.get('minute'),
second: time.get('second')
});

return () => sub.unsubscribe();
}, [watch]);
// adds the timezone info if needed
const valueToSet = dateTime.format(DATE_TIME_FORMAT);

if (datetimeFieldState.error) clearErrors(props.name);
setValue(props.name, valueToSet);

}, [dateVal, timeVal])

const formInputDate = useController({
name: `${props.name}-date`,
control,
rules: {
required: false
required: props.requiredInput
},
});
const dateField = formInputDate?.field;
Expand All @@ -134,7 +135,7 @@ const DateTimeField = (props: DateTimeFieldProps): JSX.Element => {
name: `${props.name}-time`,
control,
rules: {
required: false
required: props.requiredInput
},
});
const timeField = formInputTime?.field;
Expand Down
4 changes: 3 additions & 1 deletion src/components/input-switch/input-switch.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import '@isrd-isi-edu/chaise/src/assets/scss/_input-switch.scss';

import { memo } from 'react';

// components
import ArrayField from '@isrd-isi-edu/chaise/src/components/input-switch/array-field';
import BooleanField from '@isrd-isi-edu/chaise/src/components/input-switch/boolean-field';
Expand Down Expand Up @@ -403,4 +405,4 @@ const InputSwitch = ({
})();
};

export default InputSwitch;
export default memo(InputSwitch);
4 changes: 2 additions & 2 deletions src/components/range-inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ const RangeInputs = ({
const minName = `${name}-min`;
const maxName = `${name}-max`;

const timestampOptions = { outputMomentFormat: dataFormats.datetime.return }
if (type === 'timestamptz') timestampOptions.outputMomentFormat = dataFormats.timestamp;
const timestampOptions = { outputMomentFormat: dataFormats.timestamp }
if (type === 'timestamptz') timestampOptions.outputMomentFormat = dataFormats.datetime.return;

let defVals = {};
if (type.indexOf('timestamp') !== -1) {
Expand Down
14 changes: 13 additions & 1 deletion src/components/recordedit/form-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@ import { RecordeditDisplayMode } from '@isrd-isi-edu/chaise/src/models/recordedi
import ResizeSensor from 'css-element-queries/src/ResizeSensor';
import { addTopHorizontalScroll } from '@isrd-isi-edu/chaise/src/utils/ui-utils';

const FormContainer = (): JSX.Element => {
type FormContainerProps = {
/* the index of column that is showing the select all input */
activeMultiForm: number;
/* change the active select all */
toggleActiveMultiForm: (colIndex: number) => void;
}

const FormContainer = ({
activeMultiForm,
toggleActiveMultiForm
}: FormContainerProps): JSX.Element => {

const {
columnModels, config, forms, onSubmitValid, onSubmitInvalid, removeForm
Expand Down Expand Up @@ -119,11 +129,13 @@ const FormContainer = (): JSX.Element => {
{/* inputs for each column */}
{columnModels.map(({ }, idx) => (
<FormRow
isActiveForm={activeMultiForm === idx}
removeClicked={removeClicked}
setRemoveClicked={setRemoveClicked}
removeFormIndex={removeFormIndex}
key={`form-row-${idx}`}
columnModelIndex={idx}
toggleActiveMultiForm={toggleActiveMultiForm}
/>
))}
</form>
Expand Down
26 changes: 17 additions & 9 deletions src/components/recordedit/form-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import InputSwitch from '@isrd-isi-edu/chaise/src/components/input-switch/input-
import MultiFormInputRow from '@isrd-isi-edu/chaise/src/components/recordedit/multi-form-input-row';

// hooks
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
import useRecordedit from '@isrd-isi-edu/chaise/src/hooks/recordedit';

// models
Expand All @@ -16,6 +16,11 @@ import { isObjectAndKeyDefined } from '@isrd-isi-edu/chaise/src/utils/type-utils
import { makeSafeIdAttr } from '@isrd-isi-edu/chaise/src/utils/string-utils';

type FormRowProps = {
/**
* If the current form row has the multi form row open
* NOTE: this is a prop to prevent every form row from rerendering when the activeMultiForm changes in the provider
*/
isActiveForm: boolean;
/**
* The column index.
*/
Expand All @@ -35,20 +40,23 @@ type FormRowProps = {
* (change back the removeClicked that is passed to this component)
*/
setRemoveClicked?: any;
/* change the active select all */
toggleActiveMultiForm: (colIndex: number) => void;
};
const FormRow = ({
isActiveForm,
columnModelIndex,
removeFormIndex,
removeClicked,
setRemoveClicked,
toggleActiveMultiForm
}: FormRowProps): JSX.Element => {
const {
forms,
appMode,
reference,
columnModels,
tuples,
activeMultiForm,
canUpdateValues,
columnPermissionErrors,
foreignKeyData,
Expand Down Expand Up @@ -182,7 +190,7 @@ const FormRow = ({
}

// only call when we're actually showing the multi-form-row
if (!showMultiFormRow) {
if (!isActiveForm) {
return;
}

Expand All @@ -204,7 +212,6 @@ const FormRow = ({

// -------------------------- render logic ---------------------- //

const showMultiFormRow = activeMultiForm === columnModelIndex;
const columnModel = columnModels[columnModelIndex];

/**
Expand All @@ -221,7 +228,7 @@ const FormRow = ({
return false;
}

if (columnModel.isDisabled || showMultiFormRow) {
if (columnModel.isDisabled || isActiveForm) {
return true;
}

Expand All @@ -241,7 +248,7 @@ const FormRow = ({
const getEntityValueClass = (formNumber: number) => {
const classes = [];
const cannotBeUpdated = (appMode === appModes.EDIT && canUpdateValues && !canUpdateValues[`${formNumber}-${cm.column.name}`])
if (!cannotBeUpdated && showMultiFormRow) {
if (!cannotBeUpdated && isActiveForm) {
classes.push('clickable-form-overlay');
if (activeForms.includes(formNumber)) {
classes.push('entity-active');
Expand Down Expand Up @@ -312,7 +319,7 @@ const FormRow = ({
};

return (
<div className={`form-inputs-row ${showMultiFormRow ? 'highlighted-row' : ''}`} ref={container}>
<div className={`form-inputs-row ${isActiveForm ? 'highlighted-row' : ''}`} ref={container}>
<div className='inputs-row' ref={formsRef}>
{forms.map((formNumber: number, formIndex: number) => (
<div
Expand All @@ -324,15 +331,16 @@ const FormRow = ({
</div>
))}
</div>
{showMultiFormRow &&
{isActiveForm &&
<MultiFormInputRow
activeForms={activeForms}
setActiveForm={setActiveForm}
columnModelIndex={columnModelIndex}
toggleActiveMultiForm={toggleActiveMultiForm}
/>
}
</div>
);
};

export default FormRow;
export default memo(FormRow);
16 changes: 13 additions & 3 deletions src/components/recordedit/key-column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,21 @@ import { LogService } from '@isrd-isi-edu/chaise/src/services/log';
import { makeSafeIdAttr } from '@isrd-isi-edu/chaise/src/utils/string-utils';
import { isObjectAndKeyDefined } from '@isrd-isi-edu/chaise/src/utils/type-utils';

const KeyColumn = (): JSX.Element => {
type KeyColumnProps = {
/* the index of column that is showing the select all input */
activeMultiForm: number;
/* function to change the active select all */
toggleActiveMultiForm: (colIndex: number) => void;
}

const KeyColumn = ({
activeMultiForm,
toggleActiveMultiForm
}: KeyColumnProps): JSX.Element => {

const {
appMode, activeMultiForm, columnModels, columnPermissionErrors,
config, forms, logRecordeditClientAction, toggleActiveMultiForm
appMode, columnModels, columnPermissionErrors,
config, forms, logRecordeditClientAction
} = useRecordedit();

const onToggleClick = (cmIndex: number) => {
Expand Down
Loading