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

LF-4384: End to end animal details #3534

Merged
merged 30 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d40e520
LF-4384 Update WithStepperProgressBar not to use FixedHeaderContainer…
SayakaOno Nov 19, 2024
aab542c
LF-4384 Add animals url to FULL_WIDTH_ROUTES in App
SayakaOno Nov 19, 2024
5c390de
LF-4384 Refactor WithStepperProgressBar
SayakaOno Nov 19, 2024
45ba590
LF-4384 Update SingleAnimalView to use FixedHeaderContainer
SayakaOno Nov 19, 2024
2632814
LF-4384 Implement animal removal
SayakaOno Nov 19, 2024
b7b76c5
LF-4384 Create custom RouteComponentProps type
SayakaOno Nov 19, 2024
4248a6b
LF-4384 Replace RouteComponentProps with CustomRouteComponentProps
SayakaOno Nov 19, 2024
2f1ac45
LF-4384 Replace history.push with history.back in SingleAnimalView
SayakaOno Nov 19, 2024
d8e013a
LF-4384 Redirect to unknown record screen when no animal or batch for…
SayakaOno Nov 19, 2024
ace818d
LF-4384 Update MeatballsMenu and DropdownButton to accept disabled prop
SayakaOno Nov 19, 2024
4ac69a8
LF-4384 Update AnimalSingleViewHeader to disable menu button while ed…
SayakaOno Nov 19, 2024
6054d79
LF-4384 Add missing desiredKeys to animalBatchController's editAnimal…
SayakaOno Nov 19, 2024
3f72c80
LF-4384 Update parseUniqueDefaultId to handle exception
SayakaOno Nov 19, 2024
b3edb84
LF-4384 Add styles to batch count in AnimalSingleViewHeader
SayakaOno Nov 19, 2024
f29364a
LF-4384 Adjust batch count width in AnimalSingleViewHeader
SayakaOno Nov 19, 2024
a5452e1
LF-4384 Update getRecordIfExists middleware helper not to include rem…
SayakaOno Nov 19, 2024
ad65572
LF-4384 Update useAnimalOrBatchRemoval to return result
SayakaOno Nov 19, 2024
8c43213
LF-4384 Update onConfirmRemoval in SingleAnimalView to call history.b…
SayakaOno Nov 19, 2024
b9d72e3
LF-4384 Hide meatballs menu for removed animals
SayakaOno Nov 19, 2024
196e475
LF-4384 Hide single animal view menu for non-admin users
SayakaOno Nov 21, 2024
035f8e7
LF-4384 Fix crashing
SayakaOno Nov 21, 2024
e599d65
LF-4384 Fix uses not being rendered after logging in
SayakaOno Nov 25, 2024
9241b04
LF-4384 Update prop name hideMenu to showMenu for showing meatballs menu
SayakaOno Nov 25, 2024
4d9fcc6
LF-4384 Refactor and create StepperProgressBarWrapper
SayakaOno Nov 25, 2024
a46d530
LF-4384 Make setSelectedInventoryIds prop optional in useAnimalOrBatc…
SayakaOno Nov 25, 2024
9021d49
LF-4384 Remove unnecessary code in SingleAnimalView
SayakaOno Nov 25, 2024
b295005
Merge branch 'integration' into LF-4384/End-to-End_Animal_Details
SayakaOno Nov 27, 2024
d7ed709
LF-4384 Update onSave callback in WithStepperProgressBar
SayakaOno Nov 27, 2024
ebde1a2
LF-4384 Remove setActiveStepIndex(0) in onSave in ContextForm
SayakaOno Nov 27, 2024
2325f80
LF-4384 Update AnimalBreedSelect to replace undefined with null as value
SayakaOno Nov 27, 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
5 changes: 5 additions & 0 deletions packages/api/src/controllers/animalBatchController.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ const animalBatchController = {
'origin_id',
'group_ids',
'animal_batch_use_relationships',
'birth_date',
'dam',
'sire',
'brought_in_date',
'weaning_date',
];

// select only allowed properties to edit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ const getRecordIfExists = async (animalOrBatch, animalOrBatchKey, farm_id) => {
return await AnimalOrBatchModel[animalOrBatchKey]
.query()
.findById(animalOrBatch.id)
.where({ farm_id })
.where({ farm_id, animal_removal_reason_id: null })
.whereNotDeleted()
.withGraphFetched(relations);
};
Expand Down
4 changes: 2 additions & 2 deletions packages/webapp/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import { NotistackSnackbar } from './containers/Snackbar/NotistackSnackbar';
import { OfflineDetector } from './containers/hooks/useOfflineDetector/OfflineDetector';
import styles from './styles.module.scss';
import Routes from './routes';
import { ANIMALS_INVENTORY_URL } from './util/siteMapConstants';
import { ANIMALS_INVENTORY_URL, ANIMALS_URL } from './util/siteMapConstants';

function App() {
const [isCompactSideMenu, setIsCompactSideMenu] = useState(false);
const FULL_WIDTH_ROUTES = ['/map', ANIMALS_INVENTORY_URL];
const FULL_WIDTH_ROUTES = ['/map', ANIMALS_INVENTORY_URL, ANIMALS_URL];
const isFullWidth = FULL_WIDTH_ROUTES.some((path) => matchPath(history.location.pathname, path));

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type ContainerWithButtonsProps = {
children: ReactNode;
contentClassName?: string;
isCompactView?: boolean;
showMenu: boolean;
isEditing?: boolean;
options: { label: ReactNode; onClick: () => void }[];
onBack: () => void;
Expand All @@ -93,6 +94,7 @@ const ContainerWithButtons = ({
children,
contentClassName,
isCompactView,
showMenu = true,
isEditing,
options,
onBack,
Expand All @@ -106,16 +108,20 @@ const ContainerWithButtons = ({
<div className={clsx(styles.content, contentClassName)}>{children}</div>
<div className={styles.statusAndButton}>
{!isCompactView && isEditing ? <div>{t('common:EDITING')}</div> : null}
<MeatballsMenu
options={options}
classes={{ button: isEditing ? styles.editingStatusButton : '' }}
/>
{showMenu && (
<MeatballsMenu
disabled={!!isEditing}
options={options}
classes={{ button: isEditing ? styles.editingStatusButton : '' }}
/>
)}
</div>
</div>
);
};

export type AnimalSingleViewHeaderProps = {
showMenu: boolean;
isEditing?: boolean;
onEdit: () => void;
onRemove: () => void;
Expand All @@ -128,6 +134,7 @@ export type AnimalSingleViewHeaderProps = {
};

const AnimalSingleViewHeader = ({
showMenu = true,
isEditing,
onEdit,
onRemove,
Expand Down Expand Up @@ -163,7 +170,7 @@ const AnimalSingleViewHeader = ({
{ label: <MenuItem iconName="TRASH" text={t('common:REMOVE')} />, onClick: onRemove },
];

const commonProp = { t, isEditing, isCompactView, options: menuOptions, onBack };
const commonProp = { t, showMenu, isEditing, isCompactView, options: menuOptions, onBack };

const renderCompactHeader = () => (
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@
padding-left: 16px;
}

.editingStatusButton {
.editingStatusButton,
.editingStatusButton:disabled {
background: var(--Btn-primary-hover);
box-shadow: 1px 1px 0px 0px var(--Colors-Primary-Primary-teal-300);

Expand Down Expand Up @@ -152,6 +153,8 @@
bottom: 8px;

height: 24px;
min-width: 20px;
max-width: 44px;
padding: 4px;
border-radius: 2px;
background: var(--Colors-Accent---singles-Purple-light);
Expand All @@ -160,6 +163,10 @@
color: var(--Colors-Accent---singles-Purple-full);
font-size: 12px;
font-weight: 600;
text-align: center;

/* Counts with more than five digits will be visually cut off */
overflow: hidden;
Duncan-Brain marked this conversation as resolved.
Show resolved Hide resolved
}

.desktopBasicInfo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import { ReactNode, useEffect, useRef, useState } from 'react';
import { UseFormHandleSubmit, FieldValues, FormState, UseFormReset } from 'react-hook-form';
import { ReactNode, useEffect, useState } from 'react';
import {
UseFormHandleSubmit,
FieldValues,
FormState,
UseFormReset,
UseFormGetValues,
} from 'react-hook-form';
import { History } from 'history';
import StepperProgressBar from '../../StepperProgressBar';
import StepperProgressBar, { StepperProgressBarProps } from '../../StepperProgressBar';
import FloatingContainer from '../../FloatingContainer';
import FormNavigationButtons from '../FormNavigationButtons';
import FixedHeaderContainer from '../../Animals/FixedHeaderContainer';
Expand Down Expand Up @@ -45,6 +51,7 @@ interface WithStepperProgressBarProps {
onCancel: () => void;
onGoForward: () => void;
reset: UseFormReset<FieldValues>;
getValues: UseFormGetValues<FieldValues>;
formState: FormState<FieldValues>;
handleSubmit: UseFormHandleSubmit<FieldValues>;
setFormResultData: (data: any) => void;
Expand All @@ -70,6 +77,7 @@ export const WithStepperProgressBar = ({
onGoForward,
handleSubmit,
reset,
getValues,
formState: { isValid, isDirty },
setFormResultData,
isEditing,
Expand Down Expand Up @@ -109,6 +117,7 @@ export const WithStepperProgressBar = ({
if (isFinalStep) {
setIsSaving(true);
await handleSubmit((data: FieldValues) => onSave(data, onGoForward, setFormResultData))();
reset(getValues());
Copy link
Collaborator

@Duncan-Brain Duncan-Brain Nov 26, 2024

Choose a reason for hiding this comment

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

At first I was worried that this would clear the edited values when unsuccessful (api is down). But instead It kept them? Do you think this is expected?

Screen.Recording.2024-11-26.at.1.07.32.PM.mov

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

reset() will reset the form to the default values. However, reset(getValues()) updates the default values to the current form values before resetting, so this behaviour is expected. You can find a brief explanation in the RULES section on this page.

Thank you Duncan for thoroughly testing this, I don't think reset, setIsSaving and setIsEditing shouldn't be called unless the API call is successful. Let me see if I can fix this!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting use of reset! and sounds good!

setIsSaving(false);
setIsEditing?.(false);
return;
Expand All @@ -134,17 +143,12 @@ export const WithStepperProgressBar = ({
};

return (
<FixedHeaderContainer
header={
isSingleStep ? null : (
<StepperProgressBar
{...stepperProgressBarConfig}
title={stepperProgressBarTitle}
steps={steps.map(({ title }) => title)}
activeStep={activeStepIndex}
/>
)
}
<StepperProgressBarWrapper
isSingleStep={isSingleStep}
{...stepperProgressBarConfig}
title={stepperProgressBarTitle}
steps={steps.map(({ title }) => title)}
activeStep={activeStepIndex}
>
<div className={styles.contentWrapper}>{children}</div>
{shouldShowFormNavigationButtons && (
Expand All @@ -166,6 +170,27 @@ export const WithStepperProgressBar = ({
handleCancel={handleCancel}
/>
)}
</StepperProgressBarWrapper>
);
};

type StepperProgressBarWrapperProps = StepperProgressBarProps & {
children: ReactNode;
isSingleStep: boolean;
};

const StepperProgressBarWrapper = ({
children,
isSingleStep,
...stepperProgressBarProps
}: StepperProgressBarWrapperProps) => {
if (isSingleStep) {
return <>{children}</>;
}

return (
<FixedHeaderContainer header={<StepperProgressBar {...stepperProgressBarProps} />}>
{children}
</FixedHeaderContainer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default function DropdownButton({
type,
classes: propClasses = {},
menuPositionOffset,
disabled = false,
}) {
const classes = useStyles();
const [isOpen, setOpen] = useState(defaultOpen);
Expand Down Expand Up @@ -68,6 +69,7 @@ export default function DropdownButton({
aria-controls={isOpen ? 'composition-menu' : undefined}
aria-expanded={isOpen ? 'true' : undefined}
aria-haspopup="true"
disabled={disabled}
>
{children}
{!noIcon && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import styles from './styles.module.scss';
export type MeatballsMenuProps = {
classes?: { button?: string };
options: { label: ReactNode; onClick: () => void }[];
disabled: boolean;
};

const MeatballsMenu = ({ options, classes }: MeatballsMenuProps) => {
const MeatballsMenu = ({ options, classes, disabled = false }: MeatballsMenuProps) => {
return (
<DropdownButton
type={'v2'}
Expand All @@ -39,6 +40,7 @@ const MeatballsMenu = ({ options, classes }: MeatballsMenuProps) => {
/>
))}
menuPositionOffset={[0, 1]}
disabled={disabled}
>
<BsThreeDots className={styles.menuIcon} />
</DropdownButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import { Dispatch, SetStateAction, useState, useEffect } from 'react';
import { Dispatch, SetStateAction, useState } from 'react';
import {
useDeleteAnimalBatchesMutation,
useDeleteAnimalsMutation,
Expand All @@ -31,7 +31,7 @@ import { useTranslation } from 'react-i18next';

const useAnimalOrBatchRemoval = (
selectedInventoryIds: string[],
setSelectedInventoryIds: Dispatch<SetStateAction<string[]>>,
setSelectedInventoryIds?: Dispatch<SetStateAction<string[]>>,
) => {
const dispatch = useDispatch();
const { t } = useTranslation(['message']);
Expand All @@ -52,6 +52,7 @@ const useAnimalOrBatchRemoval = (
const animalBatchRemovalArray = [];
const selectedAnimalIds: string[] = [];
const selectedBatchIds: string[] = [];
let result;

for (const id of selectedInventoryIds) {
const { kind, id: entity_id } = parseInventoryId(id);
Expand All @@ -74,40 +75,44 @@ const useAnimalOrBatchRemoval = (
}
}

try {
if (animalRemovalArray.length) {
await mutations['removeAnimals'].trigger(animalRemovalArray).unwrap();
setSelectedInventoryIds((selectedInventoryIds) =>
if (animalRemovalArray.length) {
result = await mutations['removeAnimals'].trigger(animalRemovalArray);

if (result.error) {
console.log(result.error);
dispatch(enqueueErrorSnackbar(t('ANIMALS.FAILED_REMOVE_ANIMALS', { ns: 'message' })));
} else {
setSelectedInventoryIds?.((selectedInventoryIds) =>
selectedInventoryIds.filter((i) => !selectedAnimalIds.includes(i)),
);
dispatch(enqueueSuccessSnackbar(t('ANIMALS.SUCCESS_REMOVE_ANIMALS', { ns: 'message' })));
}
} catch (e) {
console.log(e);
dispatch(enqueueErrorSnackbar(t('ANIMALS.FAILED_REMOVE_ANIMALS', { ns: 'message' })));
}

try {
if (animalBatchRemovalArray.length) {
await mutations['removeBatches'].trigger(animalBatchRemovalArray).unwrap();
setSelectedInventoryIds((selectedInventoryIds) =>
if (animalBatchRemovalArray.length) {
result = await mutations['removeBatches'].trigger(animalBatchRemovalArray);

if (result.error) {
console.log(result.error);
dispatch(enqueueErrorSnackbar(t('ANIMALS.FAILED_REMOVE_BATCHES', { ns: 'message' })));
} else {
setSelectedInventoryIds?.((selectedInventoryIds) =>
selectedInventoryIds.filter((i) => !selectedBatchIds.includes(i)),
);
dispatch(enqueueSuccessSnackbar(t('ANIMALS.SUCCESS_REMOVE_BATCHES', { ns: 'message' })));
}
} catch (e) {
console.log(e);
dispatch(enqueueErrorSnackbar(t('ANIMALS.FAILED_REMOVE_BATCHES', { ns: 'message' })));
}

setRemovalModalOpen(false);
return result;
};

const handleAnimalOrBatchDeletion = async () => {
const animalIds: number[] = [];
const selectedAnimalIds: string[] = [];
const animalBatchIds: number[] = [];
const selectedBatchIds: string[] = [];
let result;

for (const id of selectedInventoryIds) {
const { kind, id: entity_id } = parseInventoryId(id);
Expand All @@ -120,40 +125,42 @@ const useAnimalOrBatchRemoval = (
}
}

try {
if (animalIds.length) {
await mutations['deleteAnimals'].trigger(animalIds).unwrap();
setSelectedInventoryIds((selectedInventoryIds) =>
if (animalIds.length) {
result = await mutations['deleteAnimals'].trigger(animalIds);

if (result.error) {
console.log(result.error);
dispatch(enqueueErrorSnackbar(t('ANIMALS.FAILED_REMOVE_ANIMALS', { ns: 'message' })));
} else {
setSelectedInventoryIds?.((selectedInventoryIds) =>
selectedInventoryIds.filter((i) => !selectedAnimalIds.includes(i)),
);
dispatch(enqueueSuccessSnackbar(t('ANIMALS.SUCCESS_REMOVE_ANIMALS', { ns: 'message' })));
}
} catch (e) {
console.log(e);
dispatch(enqueueErrorSnackbar(t('ANIMALS.FAILED_REMOVE_ANIMALS', { ns: 'message' })));
}

try {
if (animalBatchIds.length) {
await mutations['deleteBatches'].trigger(animalBatchIds).unwrap();
setSelectedInventoryIds((selectedInventoryIds) =>
if (animalBatchIds.length) {
result = await mutations['deleteBatches'].trigger(animalBatchIds);
if (result.error) {
console.log(result.error);
dispatch(enqueueErrorSnackbar(t('ANIMALS.FAILED_REMOVE_BATCHES', { ns: 'message' })));
} else {
setSelectedInventoryIds?.((selectedInventoryIds) =>
selectedInventoryIds.filter((i) => !selectedBatchIds.includes(i)),
);
dispatch(enqueueSuccessSnackbar(t('ANIMALS.SUCCESS_REMOVE_BATCHES', { ns: 'message' })));
}
} catch (e) {
console.log(e);
dispatch(enqueueErrorSnackbar(t('ANIMALS.FAILED_REMOVE_BATCHES', { ns: 'message' })));
}

setRemovalModalOpen(false);
return result;
};

const onConfirmRemoveAnimals = (formData: FormFields) => {
const onConfirmRemoveAnimals = async (formData: FormFields) => {
if (Number(formData.reason) === CREATED_IN_ERROR_ID) {
handleAnimalOrBatchDeletion();
return handleAnimalOrBatchDeletion();
Duncan-Brain marked this conversation as resolved.
Show resolved Hide resolved
} else {
handleAnimalOrBatchRemoval(formData);
return handleAnimalOrBatchRemoval(formData);
}
};

Expand Down
Loading
Loading