diff --git a/packages/webapp/src/components/Animals/Inventory/index.tsx b/packages/webapp/src/components/Animals/Inventory/index.tsx index 86f3c183a5..aca7d72821 100644 --- a/packages/webapp/src/components/Animals/Inventory/index.tsx +++ b/packages/webapp/src/components/Animals/Inventory/index.tsx @@ -29,6 +29,7 @@ import styles from './styles.module.scss'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { ADD_ANIMALS_URL } from '../../../util/siteMapConstants'; +import { animalDescendingComparator } from '../../../util/sort'; export type SearchProps = { searchString: string | null | undefined; @@ -151,6 +152,7 @@ const PureAnimalInventory = ({ headerClass={styles.headerClass} onRowClick={onRowClick} extraRowSpacing={extraRowSpacing} + comparator={animalDescendingComparator} /> ) : ( data .slice() - .sort(getComparator(order, orderBy)) + .sort(getComparator(order, orderBy, comparator)) .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage), [order, orderBy, page, rowsPerPage, data], ); diff --git a/packages/webapp/src/components/Table/types.ts b/packages/webapp/src/components/Table/types.ts index 1e7010eb72..e2a5a3d5c6 100644 --- a/packages/webapp/src/components/Table/types.ts +++ b/packages/webapp/src/components/Table/types.ts @@ -17,6 +17,7 @@ import type { ReactElement, ReactNode, ChangeEvent } from 'react'; import type { ColumnInstance } from 'react-table'; import { ReactComponentLike } from 'prop-types'; import { ClassValue } from 'clsx'; +import { DescendingComparator } from '../../util/sort'; export enum TableKind { V1 = 'v1', @@ -86,4 +87,5 @@ export type TableV2Props = { spacerRowHeight?: number; headerClass?: ClassValue; extraRowSpacing?: boolean; + comparator?: DescendingComparator; }; diff --git a/packages/webapp/src/containers/Animals/Inventory/useAnimalInventory.tsx b/packages/webapp/src/containers/Animals/Inventory/useAnimalInventory.tsx index 9de773305c..68f21d2013 100644 --- a/packages/webapp/src/containers/Animals/Inventory/useAnimalInventory.tsx +++ b/packages/webapp/src/containers/Animals/Inventory/useAnimalInventory.tsx @@ -32,7 +32,6 @@ import { DefaultAnimalBreed, DefaultAnimalType, } from '../../../store/api/types'; -import { getComparator, orderEnum } from '../../../util/sort'; import { AnimalOrBatchKeys } from '../types'; import { generateInventoryId } from '../../../util/animal'; import { AnimalTypeIconKey, isAnimalTypeIconKey } from '../../../components/Icons/icons'; @@ -40,11 +39,15 @@ import { createSingleAnimalViewURL } from '../../../util/siteMapConstants'; import { useSelector } from 'react-redux'; import { locationsSelector } from '../../locationSlice'; import { Location } from '../../../types'; +import { getComparator, orderEnum, animalDescendingComparator } from '../../../util/sort'; export type AnimalInventoryItem = { id: string; iconName: AnimalTypeIconKey; identification: string; + identifier?: string | null; + internal_identifier: number; + name: string | null; type: string; breed: string; path: string; @@ -88,7 +91,7 @@ const getAnimalBreedLabel = (key: string) => { return t(`BREED.${key}`, { ns: 'animal' }); }; -const chooseIdentification = (animalOrBatch: Animal | AnimalBatch) => { +export const chooseIdentification = (animalOrBatch: Animal | AnimalBatch) => { if ('identifier' in animalOrBatch && animalOrBatch.identifier) { if (animalOrBatch.name && animalOrBatch.identifier) { return `${animalOrBatch.name} | ${animalOrBatch.identifier}`; @@ -153,11 +156,14 @@ const formatAnimalsData = ( id: generateInventoryId(AnimalOrBatchKeys.ANIMAL, animal), iconName: getDefaultAnimalIconName(defaultAnimalTypes, animal.default_type_id), identification: chooseIdentification(animal), + identifier: animal.identifier, + internal_identifier: animal.internal_identifier, type: chooseAnimalTypeLabel(animal, defaultAnimalTypes, customAnimalTypes), breed: chooseAnimalBreedLabel(animal, defaultAnimalBreeds, customAnimalBreeds), path: createSingleAnimalViewURL(animal.internal_identifier), count: 1, batch: false, + name: animal.name, location: animal.location_id ? locationsMap[animal.location_id] : '', // preserve some untransformed data for filtering sex_id: animal.sex_id, @@ -190,10 +196,12 @@ const formatAnimalBatchesData = ( id: generateInventoryId(AnimalOrBatchKeys.BATCH, batch), iconName: 'BATCH', identification: chooseIdentification(batch), + internal_identifier: batch.internal_identifier, type: chooseAnimalTypeLabel(batch, defaultAnimalTypes, customAnimalTypes), breed: chooseAnimalBreedLabel(batch, defaultAnimalBreeds, customAnimalBreeds), path: createSingleAnimalViewURL(batch.internal_identifier), count: batch.count, + name: batch.name, batch: true, location: batch.location_id ? locationsMap[batch.location_id] : '', // preserve some untransformed data for filtering @@ -246,7 +254,9 @@ export const buildInventory = ({ ), ]; - const sortedInventory = inventory.sort(getComparator(orderEnum.ASC, 'identification')); + const sortedInventory = inventory.sort( + getComparator(orderEnum.ASC, 'identification', animalDescendingComparator), + ); return sortedInventory; }; diff --git a/packages/webapp/src/tests/sort.test.js b/packages/webapp/src/tests/sort.test.js index aa03d22ae4..32cfdc6cbb 100644 --- a/packages/webapp/src/tests/sort.test.js +++ b/packages/webapp/src/tests/sort.test.js @@ -14,7 +14,9 @@ */ import { expect, describe, test } from 'vitest'; -import { descendingComparator, getComparator } from '../util/sort'; +import _ from 'lodash-es'; +import { descendingComparator, getComparator, animalDescendingComparator } from '../util/sort'; +import { chooseIdentification } from '../containers/Animals/Inventory/useAnimalInventory'; describe('Sort test', () => { describe('descendingComparator test', () => { @@ -109,5 +111,31 @@ describe('Sort test', () => { expect(sortable.sort(getComparator('asc', 'value'))).toEqual(createSortable(asc)); expect(sortable.sort(getComparator('desc', 'value'))).toEqual(createSortable(asc.reverse())); }); + + test('Sort animals using animalDescendingComparator', () => { + const expectedSortedAnimalsAsc = [ + { name: 'Farm', identifier: 'identifier 2', internal_identifier: 2 }, + { name: 'Java', identifier: null, internal_identifier: 4 }, + { name: 'Lite', identifier: 'identifier 1', internal_identifier: 9 }, + { name: 'Script', identifier: null, internal_identifier: 5 }, + { name: null, identifier: 'identifier 5', internal_identifier: 7 }, + { name: null, identifier: 'identifier 6', internal_identifier: 6 }, + { name: null, identifier: null, internal_identifier: 1 }, + { name: null, internal_identifier: 3 }, + { name: null, internal_identifier: 8 }, + { name: null, identifier: null, internal_identifier: 10 }, + ].map((data) => ({ ...data, identification: chooseIdentification(data) })); + + const shuffledAnimals1 = _.shuffle(expectedSortedAnimalsAsc); + const shuffledAnimals2 = _.shuffle(expectedSortedAnimalsAsc); + const sortedAnimalsAsc = shuffledAnimals1.sort( + getComparator('asc', 'identification', animalDescendingComparator), + ); + const sortedAnimalsDesc = shuffledAnimals2.sort( + getComparator('desc', 'identification', animalDescendingComparator), + ); + expect(sortedAnimalsAsc).toEqual(expectedSortedAnimalsAsc); + expect(sortedAnimalsDesc).toEqual(expectedSortedAnimalsAsc.reverse()); + }); }); }); diff --git a/packages/webapp/src/util/sort.ts b/packages/webapp/src/util/sort.ts index c0f1109200..72b39c8728 100644 --- a/packages/webapp/src/util/sort.ts +++ b/packages/webapp/src/util/sort.ts @@ -21,9 +21,15 @@ export enum orderEnum { } type Comparable = { - [key in T]: any; + [key in T]?: any; }; +export type DescendingComparator = ( + a: Comparable, + b: Comparable, + orderBy: T, +) => number; + /** * Comparator function for descending sorting of an array of objects based on a specific property. * @@ -32,11 +38,7 @@ type Comparable = { * @param {string} orderBy - The property by which to compare the objects. * @returns {number} - A negative number if a should come before b, a positive number if b should come before a, or 0 if they are equal. */ -export function descendingComparator( - a: Comparable, - b: Comparable, - orderBy: T, -): number { +export const descendingComparator: DescendingComparator = (a, b, orderBy) => { if (!hasValue(b[orderBy]) && hasValue(a[orderBy])) { return 1; } @@ -50,20 +52,58 @@ export function descendingComparator( return 1; } return 0; -} +}; /** * Returns a comparator function for sorting an array of objects in either ascending or descending order based on a specific property. * * @param {'asc' | 'desc'} order - The sorting order, either 'asc' for ascending or 'desc' for descending. * @param {string} orderBy - The property by which to compare the objects. + * @param {function} comparator - The decending comparator to use. * @returns {function} - A comparator function for use with the `Array.prototype.sort()` method. */ export function getComparator( order: 'asc' | 'desc', orderBy: T, + comparator: DescendingComparator = descendingComparator, ): (a: Comparable, b: Comparable) => number { return order === 'desc' - ? (a: Comparable, b: Comparable) => descendingComparator(a, b, orderBy) - : (a: Comparable, b: Comparable) => -descendingComparator(a, b, orderBy); + ? (a: Comparable, b: Comparable) => comparator(a, b, orderBy) + : (a: Comparable, b: Comparable) => -comparator(a, b, orderBy); } + +// Ideally, the type of "a" and "b" should be AnimalInventoryItem. +export const animalDescendingComparator: DescendingComparator = ( + a, + b, + orderBy, +) => { + if (orderBy === 'identification') { + if (a.name && !b.name) { + return 1; + } + if (b.name && !a.name) { + return -1; + } + + if (!a.name && !b.name) { + if (a.identifier && !b.identifier) { + return 1; + } + if (b.identifier && !a.identifier) { + return -1; + } + } + + let key = 'internal_identifier'; + if (a.name && b.name) { + key = 'name'; + } else if (a.identifier && b.identifier) { + key = 'identifier'; + } + + return descendingComparator(a, b, key); + } + + return descendingComparator(a, b, orderBy); +};