mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-05-25 00:36:08 -04:00
Merge pull request #3538 from inaturalist/mob-713-project-search-typing-in-k-causes-a-crash
Mob 713 project search typing in k causes a crash
This commit is contained in:
@@ -1,61 +1,46 @@
|
||||
import type { ApiProject } from "api/types";
|
||||
import type { i18n as i18next, TFunction } from "i18next";
|
||||
import { formatProjectsApiDatetimeLong } from "sharedHelpers/dateAndTime";
|
||||
|
||||
const getFieldValue = item => item?.[0]?.value;
|
||||
interface ProjectRulePreference {
|
||||
field?: string;
|
||||
value?: string | null;
|
||||
}
|
||||
|
||||
type ProjectWithDateRules = Omit<ApiProject, "project_type"> & {
|
||||
project_type?: ApiProject["project_type"];
|
||||
rule_preferences?: ProjectRulePreference[] | null;
|
||||
};
|
||||
|
||||
interface FormattedProjectDate {
|
||||
projectDate: string | null;
|
||||
shouldDisplayDateRange: boolean;
|
||||
}
|
||||
|
||||
const getFieldValue = ( item?: ProjectRulePreference[] | null ) => item?.[0]?.value ?? null;
|
||||
|
||||
// https://github.com/inaturalist/inaturalist/blob/0994c85e2b87661042289ff080d3fc29ed8e70b3/app/webpack/projects/show/components/requirements.jsx#L100C3-L114C4
|
||||
const formatProjectDate = ( project, t, i18n ) => {
|
||||
const monthValues = {
|
||||
1: {
|
||||
label: t( "January" ),
|
||||
value: 1,
|
||||
},
|
||||
2: {
|
||||
label: t( "February" ),
|
||||
value: 2,
|
||||
},
|
||||
3: {
|
||||
label: t( "March" ),
|
||||
value: 3,
|
||||
},
|
||||
4: {
|
||||
label: t( "April" ),
|
||||
value: 4,
|
||||
},
|
||||
5: {
|
||||
label: t( "May" ),
|
||||
value: 5,
|
||||
},
|
||||
6: {
|
||||
label: t( "June" ),
|
||||
value: 6,
|
||||
},
|
||||
7: {
|
||||
label: t( "July" ),
|
||||
value: 7,
|
||||
},
|
||||
8: {
|
||||
label: t( "August" ),
|
||||
value: 8,
|
||||
},
|
||||
9: {
|
||||
label: t( "September" ),
|
||||
value: 9,
|
||||
},
|
||||
10: {
|
||||
label: t( "October" ),
|
||||
value: 10,
|
||||
},
|
||||
11: {
|
||||
label: t( "November" ),
|
||||
value: 11,
|
||||
},
|
||||
12: {
|
||||
label: t( "December" ),
|
||||
value: 12,
|
||||
},
|
||||
const formatProjectDate = (
|
||||
project: ProjectWithDateRules | null | undefined,
|
||||
t: TFunction,
|
||||
i18n: i18next,
|
||||
): FormattedProjectDate => {
|
||||
const monthValues: Record<string, string> = {
|
||||
1: t( "January" ),
|
||||
2: t( "February" ),
|
||||
3: t( "March" ),
|
||||
4: t( "April" ),
|
||||
5: t( "May" ),
|
||||
6: t( "June" ),
|
||||
7: t( "July" ),
|
||||
8: t( "August" ),
|
||||
9: t( "September" ),
|
||||
10: t( "October" ),
|
||||
11: t( "November" ),
|
||||
12: t( "December" ),
|
||||
};
|
||||
|
||||
let projectDate = null;
|
||||
let projectDate: string | null = null;
|
||||
|
||||
const projectStartDate = getFieldValue( project?.rule_preferences
|
||||
?.filter( pref => pref.field === "d1" ) );
|
||||
@@ -65,29 +50,45 @@ const formatProjectDate = ( project, t, i18n ) => {
|
||||
?.filter( pref => pref.field === "observed_on" ) );
|
||||
const months = getFieldValue( project?.rule_preferences
|
||||
?.filter( pref => pref.field === "month" ) );
|
||||
const formattedStartDate = projectStartDate
|
||||
? formatProjectsApiDatetimeLong( projectStartDate, i18n, { missing: null } )
|
||||
: null;
|
||||
const formattedEndDate = projectEndDate
|
||||
? formatProjectsApiDatetimeLong( projectEndDate, i18n, { missing: null } )
|
||||
: null;
|
||||
const formattedObservedOnDate = observedOnDate
|
||||
? formatProjectsApiDatetimeLong( observedOnDate, i18n, { missing: null } )
|
||||
: null;
|
||||
|
||||
if ( projectStartDate && !projectEndDate ) {
|
||||
projectDate = t( "project-start-time-datetime", {
|
||||
datetime: formatProjectsApiDatetimeLong( projectStartDate, i18n ),
|
||||
} );
|
||||
if ( formattedStartDate && !formattedEndDate ) {
|
||||
projectDate = formattedStartDate
|
||||
? t( "project-start-time-datetime", {
|
||||
datetime: formattedStartDate,
|
||||
} )
|
||||
: null;
|
||||
}
|
||||
if ( projectStartDate && projectEndDate ) {
|
||||
if ( formattedStartDate && formattedEndDate ) {
|
||||
projectDate = t( "date-to-date", {
|
||||
d1: formatProjectsApiDatetimeLong( projectStartDate, i18n ),
|
||||
d2: formatProjectsApiDatetimeLong( projectEndDate, i18n ),
|
||||
d1: formattedStartDate,
|
||||
d2: formattedEndDate,
|
||||
} );
|
||||
}
|
||||
if ( observedOnDate ) {
|
||||
projectDate = formatProjectsApiDatetimeLong( observedOnDate, i18n );
|
||||
if ( formattedObservedOnDate ) {
|
||||
projectDate = formattedObservedOnDate;
|
||||
}
|
||||
if ( months ) {
|
||||
const monthList = months.split( "," );
|
||||
projectDate = monthList.map( numberOfMonth => monthValues[numberOfMonth].label ).join( ", " );
|
||||
const monthLabels = monthList
|
||||
.map( numberOfMonth => monthValues[numberOfMonth] )
|
||||
.filter( ( label ): label is string => Boolean( label ) );
|
||||
projectDate = monthLabels.length > 0
|
||||
? monthLabels.join( ", " )
|
||||
: null;
|
||||
}
|
||||
return {
|
||||
projectDate,
|
||||
shouldDisplayDateRange: projectStartDate && projectEndDate
|
||||
&& project?.project_type !== "traditional",
|
||||
shouldDisplayDateRange: !!( formattedStartDate && formattedEndDate )
|
||||
&& project?.project_type !== "", // "" means "traditional"
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
fromUnixTime,
|
||||
getUnixTime,
|
||||
getYear,
|
||||
isValid as isValidDate,
|
||||
parse,
|
||||
parseISO,
|
||||
} from "date-fns";
|
||||
@@ -80,6 +81,9 @@ import {
|
||||
} from "date-fns/locale";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import type { i18n as i18next } from "i18next";
|
||||
import { log } from "sharedHelpers/logger";
|
||||
|
||||
const logger = log.extend( "dateAndTime" );
|
||||
|
||||
// Convert iNat locale to date-fns locale. Note that coverage is *not*
|
||||
// complete, so some locales will see dates formatted in a nearby locale,
|
||||
@@ -332,9 +336,23 @@ function formatDateString(
|
||||
timeZone = Intl.DateTimeFormat( ).resolvedOptions( ).timeZone;
|
||||
}
|
||||
|
||||
const parsedDate = parseISO( isoDateString );
|
||||
if ( !isValidDate( parsedDate ) ) {
|
||||
logger.warnWithExtra(
|
||||
"invalid date string in formatDateString",
|
||||
{
|
||||
reason: "invalid_parse_iso",
|
||||
isoDateString,
|
||||
},
|
||||
);
|
||||
return options.missing === undefined
|
||||
? i18n.t( "Missing-Date" )
|
||||
: options.missing;
|
||||
}
|
||||
|
||||
try {
|
||||
return formatInTimeZone(
|
||||
parseISO( isoDateString ),
|
||||
parsedDate,
|
||||
timeZone,
|
||||
fmt,
|
||||
{ locale: dateFnsLocale( i18n.language ) },
|
||||
@@ -346,7 +364,7 @@ function formatDateString(
|
||||
// Remove timezone (zzz) from format string
|
||||
fmt = fmt.replace( / zzz/g, "" );
|
||||
return format(
|
||||
parseISO( isoDateString ),
|
||||
parsedDate,
|
||||
fmt,
|
||||
{ locale: dateFnsLocale( i18n.language ) },
|
||||
);
|
||||
@@ -419,6 +437,12 @@ function formatProjectsApiDatetimeLong(
|
||||
i18n: i18next,
|
||||
options: FormatDateStringOptions = {},
|
||||
) {
|
||||
if ( !dateString || dateString === "" ) {
|
||||
return options.missing === undefined
|
||||
? i18n.t( "Missing-Date" )
|
||||
: options.missing;
|
||||
}
|
||||
|
||||
const hasTime = String( dateString ).includes( "T" );
|
||||
if ( hasTime ) {
|
||||
return formatDateString( dateString, i18n.t( "datetime-format-long" ), i18n, options );
|
||||
@@ -440,6 +464,11 @@ function formatProjectsApiDatetimeLong(
|
||||
const hasComma = String( dateString ).includes( "," );
|
||||
if ( hasComma ) {
|
||||
const parsedDate = parse( dateString, "MMMM d, yyyy", new Date( ) );
|
||||
if ( !isValidDate( parsedDate ) ) {
|
||||
return options.missing === undefined
|
||||
? i18n.t( "Missing-Date" )
|
||||
: options.missing;
|
||||
}
|
||||
|
||||
return formatDateString(
|
||||
formatISO( parsedDate ),
|
||||
|
||||
@@ -116,7 +116,7 @@ describe( "ProjectDetails", ( ) => {
|
||||
renderComponent( <ProjectDetails
|
||||
project={{
|
||||
...mockProjectWithDateRange,
|
||||
project_type: "traditional",
|
||||
project_type: "", // "" means "traditional"
|
||||
}}
|
||||
/> );
|
||||
const projectTypeText = await screen.findByText( /Traditional Project/ );
|
||||
|
||||
@@ -105,7 +105,7 @@ describe( "Projects", ( ) => {
|
||||
useAuthenticatedInfiniteQuery.mockImplementation( ( ) => infiniteScrollResults(
|
||||
[{
|
||||
...mockProjectWithDateRange,
|
||||
project_type: "traditional",
|
||||
project_type: "", // "" means "traditional"
|
||||
}],
|
||||
) );
|
||||
renderComponent( <ProjectsContainer /> );
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
formatApiDatetime,
|
||||
formatDifferenceForHumans,
|
||||
formatISONoSeconds,
|
||||
formatProjectsApiDatetimeLong,
|
||||
getNowISO,
|
||||
} from "sharedHelpers/dateAndTime";
|
||||
|
||||
@@ -99,6 +100,24 @@ describe( "formatApiDatetime", ( ) => {
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "formatProjectsApiDatetimeLong", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( { lng: "en" } );
|
||||
} );
|
||||
|
||||
it( "returns null for malformed timezone project dates when missing is null", ( ) => {
|
||||
expect(
|
||||
formatProjectsApiDatetimeLong( "2023-06-080 0 +04:00", i18next, { missing: null } ),
|
||||
).toBeNull( );
|
||||
} );
|
||||
|
||||
it( "returns Missing Date for malformed timezone project dates by default", ( ) => {
|
||||
expect(
|
||||
formatProjectsApiDatetimeLong( "2023-06-080 0 +04:00", i18next ),
|
||||
).toEqual( "Missing Date" );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "getNowISO", ( ) => {
|
||||
it( "should return a valid ISO8601 string", ( ) => {
|
||||
const dateString = getNowISO( );
|
||||
|
||||
Reference in New Issue
Block a user