From b5837f96a479dfe9d9cd200ff3705a101fe672e3 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:38:18 -0800 Subject: [PATCH] chore: refactor track list in observation with maintainable architecture (#883) * chore: find associated track with test * chore: refactor TrackAccordian * chore: integrate new tracsk accordian * chore: pr review * chore: change to mts file --- package-lock.json | 146 +++++++++++++++++- package.json | 1 + .../screens/Observation/TrackAccordian.tsx | 60 ++++--- .../Observation/findAssociatedTrack.test.mts | 43 ++++++ .../Observation/findAssociatedTrack.ts | 13 ++ src/frontend/screens/Observation/index.tsx | 97 +++++------- 6 files changed, 275 insertions(+), 85 deletions(-) create mode 100644 src/frontend/screens/Observation/findAssociatedTrack.test.mts create mode 100644 src/frontend/screens/Observation/findAssociatedTrack.ts diff --git a/package-lock.json b/package-lock.json index 83594cebe..4141db3e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@formatjs/intl-pluralrules": "^5.2.4", "@formatjs/intl-relativetimeformat": "^11.2.4", "@gorhom/bottom-sheet": "^5.0.5", + "@mapeo/mock-data": "2.1.2", "@osm_borders/maritime_10000m": "^1.1.0", "@react-native-community/hooks": "^2.8.0", "@react-native-community/netinfo": "11.1.0", @@ -4490,6 +4491,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@faker-js/faker": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.3.0.tgz", + "integrity": "sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@fastify/accept-negotiator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", @@ -5784,6 +5801,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -5837,6 +5878,25 @@ "z32": "^1.0.0" } }, + "node_modules/@mapeo/mock-data": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@mapeo/mock-data/-/mock-data-2.1.2.tgz", + "integrity": "sha512-eh5uVvuckgsEiWMYYWkixx0N2tzJnr+NsbVdYiA0i7EwmwkAKA5k5B8S8uXZVXLeOH3TfYxYezoZLtAoBvbIvg==", + "license": "MIT", + "dependencies": { + "@faker-js/faker": "^9.2.0", + "dereference-json-schema": "^0.2.1", + "json-schema-faker": "^0.5.8", + "type-fest": "^4.29.1" + }, + "bin": { + "generate-mapeo-data": "bin/generate-mapeo-data.js", + "list-mapeo-schemas": "bin/list-mapeo-schemas.js" + }, + "peerDependencies": { + "@comapeo/schema": "^1.1.1" + } + }, "node_modules/@mapeo/sqlite-indexer": { "version": "1.0.0-alpha.9", "resolved": "https://registry.npmjs.org/@mapeo/sqlite-indexer/-/sqlite-indexer-1.0.0-alpha.9.tgz", @@ -11110,6 +11170,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", @@ -12700,6 +12766,12 @@ "node": ">=18" } }, + "node_modules/dereference-json-schema": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/dereference-json-schema/-/dereference-json-schema-0.2.1.tgz", + "integrity": "sha512-uzJsrg225owJyRQ8FNTPHIuBOdSzIZlHhss9u6W8mp7jJldHqGuLv9cULagP/E26QVJDnjtG8U7Dw139mM1ydA==", + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -15300,6 +15372,12 @@ "node": ">= 6" } }, + "node_modules/format-util": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", + "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==", + "license": "MIT" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -19095,6 +19173,15 @@ } } }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -19159,6 +19246,31 @@ "is-buffer": "~1.1.1" } }, + "node_modules/json-schema-faker": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.8.tgz", + "integrity": "sha512-sqzPEbEDlpiH8U1tfmJHScXHy52onvMxITPsHyhe/jhS83g8TX6ruvRqt/ot1bXUPRsh7Ps1sWqJiBxIXmW5Xw==", + "license": "MIT", + "dependencies": { + "json-schema-ref-parser": "^6.1.0", + "jsonpath-plus": "^10.1.0" + }, + "bin": { + "jsf": "bin/gen.cjs" + } + }, + "node_modules/json-schema-ref-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz", + "integrity": "sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==", + "deprecated": "Please switch to @apidevtools/json-schema-ref-parser", + "license": "MIT", + "dependencies": { + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.12.1", + "ono": "^4.0.11" + } + }, "node_modules/json-schema-ref-resolver": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", @@ -19240,6 +19352,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/jsonpath-plus": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.2.0.tgz", + "integrity": "sha512-T9V+8iNYKFL2n2rF+w02LBOT2JjDnTjioaNFrxRy0Bv1y/hNsqR/EBK7Ojy2ythRHwmz2cRIls+9JitQGZC/sw==", + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "dev": true, @@ -21793,6 +21923,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ono": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", + "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==", + "license": "MIT", + "dependencies": { + "format-util": "^1.0.3" + } + }, "node_modules/open": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", @@ -26404,9 +26543,10 @@ } }, "node_modules/type-fest": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", - "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.0.tgz", + "integrity": "sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, diff --git a/package.json b/package.json index 7688c01e4..ac0f7f93d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@formatjs/intl-pluralrules": "^5.2.4", "@formatjs/intl-relativetimeformat": "^11.2.4", "@gorhom/bottom-sheet": "^5.0.5", + "@mapeo/mock-data": "2.1.2", "@osm_borders/maritime_10000m": "^1.1.0", "@react-native-community/hooks": "^2.8.0", "@react-native-community/netinfo": "11.1.0", diff --git a/src/frontend/screens/Observation/TrackAccordian.tsx b/src/frontend/screens/Observation/TrackAccordian.tsx index fcd26dd42..ee11faf68 100644 --- a/src/frontend/screens/Observation/TrackAccordian.tsx +++ b/src/frontend/screens/Observation/TrackAccordian.tsx @@ -1,11 +1,14 @@ import React from 'react'; import ChainIcon from '../../images/Chain.svg'; -import {Track} from '@comapeo/schema'; import {useNavigationFromRoot} from '../../hooks/useNavigationWithTypes.ts'; import {defineMessages, useIntl} from 'react-intl'; import {Accordian} from '../../sharedComponents/Accordian.tsx'; import {HeaderText} from '../../sharedComponents/Text/HeaderText.tsx'; import {TrackListItem} from '../ObservationsList/TrackListItem.tsx'; +import {useTracks} from '../../hooks/server/track.ts'; +import {View} from 'react-native'; +import {LIGHT_GREY} from '../../lib/styles.ts'; +import {findAssociatedTrack} from './findAssociatedTrack.ts'; const m = defineMessages({ track: { @@ -14,29 +17,44 @@ const m = defineMessages({ }, }); -export function TrackAccordian({track}: {track: Track}) { +export function TrackAccordian({observationId}: {observationId: string}) { const navigation = useNavigationFromRoot(); + const {data: allTracks} = useTracks(); + const track = + allTracks === undefined + ? undefined + : findAssociatedTrack({tracks: allTracks, observationId}); const {formatMessage} = useIntl(); + if (!track) return null; + return ( - - {1} - - {formatMessage(m.track)} - - } - innerAccordianDetails={ - { - navigation.push('Track', {trackId: track.docId}); - }} - testID={`trackListItem:${track.docId}`} - /> - } - /> + + + {1} + + {formatMessage(m.track)} + + } + innerAccordianDetails={ + { + navigation.push('Track', {trackId: track.docId}); + }} + testID={`trackListItem:${track.docId}`} + /> + } + /> + ); } diff --git a/src/frontend/screens/Observation/findAssociatedTrack.test.mts b/src/frontend/screens/Observation/findAssociatedTrack.test.mts new file mode 100644 index 000000000..bbb0cb1a4 --- /dev/null +++ b/src/frontend/screens/Observation/findAssociatedTrack.test.mts @@ -0,0 +1,43 @@ +import {findAssociatedTrack} from './findAssociatedTrack'; +import mock from '@mapeo/mock-data'; +import {assert} from '../../lib/assert'; + +describe('Tests findAssociatedTrack', () => { + it('returns the correct track with the associated observationId', () => { + const [observation] = mock.generate('observation'); + assert(observation); + const tracks = mock.generate('track', {count: 10}); + const targetTrack = tracks[3]; + assert(targetTrack); + targetTrack.observationRefs.push({ + docId: observation.docId, + versionId: observation.versionId, + }); + const track = findAssociatedTrack({ + tracks, + observationId: observation.docId, + }); + expect(track).toStrictEqual(targetTrack); + }); + + it('returns undefined when there is no matching observation docId', () => { + const [observation] = mock.generate('observation'); + assert(observation); + const tracks = mock.generate('track', {count: 10}); + const track = findAssociatedTrack({ + tracks, + observationId: observation.docId, + }); + expect(track).toBeUndefined(); + }); + + it('returns undefined when tracks array is empty', () => { + const [observation] = mock.generate('observation'); + assert(observation); + const track = findAssociatedTrack({ + tracks: [], + observationId: observation.docId, + }); + expect(track).toBeUndefined(); + }); +}); diff --git a/src/frontend/screens/Observation/findAssociatedTrack.ts b/src/frontend/screens/Observation/findAssociatedTrack.ts new file mode 100644 index 000000000..259d3b834 --- /dev/null +++ b/src/frontend/screens/Observation/findAssociatedTrack.ts @@ -0,0 +1,13 @@ +import {type Track} from '@comapeo/schema'; + +export function findAssociatedTrack({ + tracks, + observationId, +}: { + tracks: Track[]; + observationId: string; +}) { + return tracks.find(trackData => + trackData.observationRefs.some(ref => ref.docId === observationId), + ); +} diff --git a/src/frontend/screens/Observation/index.tsx b/src/frontend/screens/Observation/index.tsx index a055779e8..bf7b8cb71 100644 --- a/src/frontend/screens/Observation/index.tsx +++ b/src/frontend/screens/Observation/index.tsx @@ -6,7 +6,7 @@ import {WHITE, DARK_GREY, LIGHT_GREY, BLUE_GREY} from '../../lib/styles'; import {UIActivityIndicator} from 'react-native-indicators'; import {FormattedObservationDate} from '../../sharedComponents/FormattedData'; -import {Field, Track} from '@comapeo/schema'; +import {Field} from '@comapeo/schema'; import {PresetHeader} from './PresetHeader'; import {useObservationWithPreset} from '../../hooks/useObservationWithPreset'; import {useFieldsQuery} from '../../hooks/server/fields'; @@ -23,8 +23,6 @@ import {ButtonFields} from './Buttons.tsx'; import {AudioAttachment} from '../../sharedTypes/audio.ts'; import {isSavedPhoto, isAudioAttachment} from '../../lib/attachmentTypeChecks'; import {TrackAccordian} from './TrackAccordian.tsx'; -import {useTracks} from '../../hooks/server/track.ts'; -import {Loading} from '../../sharedComponents/Loading.tsx'; import {Divider} from '../../sharedComponents/Divider.tsx'; import {BodyText} from '../../sharedComponents/Text/BodyText.tsx'; import {HeaderText} from '../../sharedComponents/Text/HeaderText.tsx'; @@ -59,20 +57,6 @@ export const ObservationScreen: NativeNavigationComponent<'Observation'> = ({ const {observation, preset} = useObservationWithPreset(observationId); const {data: fieldData} = useFieldsQuery(); - const [track, setTrack] = React.useState( - 'loading', - ); - const tracksQuery = useTracks(); - - React.useEffect(() => { - if (track !== 'loading') return; - if (tracksQuery.data) { - const associatedTrack = tracksQuery.data.find(trackData => - trackData.observationRefs.some(ref => ref.docId === observationId), - ); - setTrack(associatedTrack); - } - }, [track, tracksQuery.data, observationId]); const defaultAcc: Field[] = []; const fields = @@ -116,53 +100,44 @@ export const ObservationScreen: NativeNavigationComponent<'Observation'> = ({ /> - {track === 'loading' ? ( - - ) : ( + + + + }> + + + {typeof observation.tags.notes === 'string' ? ( + + {observation.tags.notes} + + ) : null} + {attachments.length > 0 && ( + + )} + + {fields.length > 0 && ( <> - - - {track && ( - - - - - - )} - {typeof observation.tags.notes === 'string' ? ( - - {observation.tags.notes} - - ) : null} - {attachments.length > 0 && ( - - )} - - {fields.length > 0 && ( - <> - - - - )} - - {isDeviceInfoPending || isDeviceIdPending ? ( - - ) : ( - - )} + + )} + + {isDeviceInfoPending || isDeviceIdPending ? ( + + ) : ( + + )} );