clean up plant search
This commit is contained in:
@@ -151,7 +151,7 @@ static IEnumerable<PlantInfoImportRecord> ReadGbifDwcaRecords(
|
|||||||
var rank = layout.Taxon.Value(fields, "taxonRank");
|
var rank = layout.Taxon.Value(fields, "taxonRank");
|
||||||
var status = layout.Taxon.Value(fields, "taxonomicStatus");
|
var status = layout.Taxon.Value(fields, "taxonomicStatus");
|
||||||
if (!EqualsIgnoreCase(kingdom, "Plantae") ||
|
if (!EqualsIgnoreCase(kingdom, "Plantae") ||
|
||||||
!EqualsIgnoreCase(rank, "species") ||
|
!IsSpeciesOrBelowRank(rank) ||
|
||||||
!EqualsIgnoreCase(status, "accepted"))
|
!EqualsIgnoreCase(status, "accepted"))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -296,6 +296,20 @@ static bool IsEnglishOrUnknown(string? language) =>
|
|||||||
EqualsIgnoreCase(language, "eng") ||
|
EqualsIgnoreCase(language, "eng") ||
|
||||||
EqualsIgnoreCase(language, "english");
|
EqualsIgnoreCase(language, "english");
|
||||||
|
|
||||||
|
static bool IsSpeciesOrBelowRank(string? rank)
|
||||||
|
{
|
||||||
|
var normalized = rank?.Trim().Replace(" ", "_").Replace("-", "_").ToLowerInvariant();
|
||||||
|
return normalized is
|
||||||
|
"species" or
|
||||||
|
"subspecies" or
|
||||||
|
"variety" or
|
||||||
|
"subvariety" or
|
||||||
|
"form" or
|
||||||
|
"forma" or
|
||||||
|
"subform" or
|
||||||
|
"subforma";
|
||||||
|
}
|
||||||
|
|
||||||
static async Task CreateSchemaAsync(SqliteConnection connection)
|
static async Task CreateSchemaAsync(SqliteConnection connection)
|
||||||
{
|
{
|
||||||
await ExecuteAsync(connection, """
|
await ExecuteAsync(connection, """
|
||||||
|
|||||||
@@ -30,13 +30,6 @@ type TaxaViewProps = {
|
|||||||
onSearchQueryChange: (value: string) => void;
|
onSearchQueryChange: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const plantInfoSearchExamples = [
|
|
||||||
'ficus',
|
|
||||||
'monstera',
|
|
||||||
'alocasia',
|
|
||||||
'croton',
|
|
||||||
];
|
|
||||||
|
|
||||||
function getPlantInfoSubtitle(result: PlantInfoSearchResult) {
|
function getPlantInfoSubtitle(result: PlantInfoSearchResult) {
|
||||||
if (result.commonName) {
|
if (result.commonName) {
|
||||||
return result.commonName;
|
return result.commonName;
|
||||||
@@ -116,19 +109,6 @@ export function TaxaView({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="query-chip-row" aria-label="Example plant info searches">
|
|
||||||
{plantInfoSearchExamples.map((query) => (
|
|
||||||
<button
|
|
||||||
className="query-chip"
|
|
||||||
type="button"
|
|
||||||
key={query}
|
|
||||||
onClick={() => onSearch(query)}
|
|
||||||
>
|
|
||||||
{query}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{plantInfoResults.length > 0 ? (
|
{plantInfoResults.length > 0 ? (
|
||||||
<div className="plant-list">
|
<div className="plant-list">
|
||||||
{plantInfoResults.map((result) => (
|
{plantInfoResults.map((result) => (
|
||||||
@@ -137,6 +117,10 @@ export function TaxaView({
|
|||||||
<h3>{result.canonicalName ?? result.scientificName}</h3>
|
<h3>{result.canonicalName ?? result.scientificName}</h3>
|
||||||
<p>{getPlantInfoSubtitle(result)}</p>
|
<p>{getPlantInfoSubtitle(result)}</p>
|
||||||
<dl className="plant-info-meta">
|
<dl className="plant-info-meta">
|
||||||
|
<div>
|
||||||
|
<dt>Common Names</dt>
|
||||||
|
<dd>{result.commonNames.length > 0 ? result.commonNames.join(', ') : 'None'}</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Family</dt>
|
<dt>Family</dt>
|
||||||
<dd>{result.family ?? 'Unknown'}</dd>
|
<dd>{result.family ?? 'Unknown'}</dd>
|
||||||
@@ -155,7 +139,19 @@ export function TaxaView({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Source</dt>
|
<dt>Source</dt>
|
||||||
<dd>{`${result.source}:${result.externalId}`}</dd>
|
<dd>
|
||||||
|
{result.source === 'gbif' ? (
|
||||||
|
<a
|
||||||
|
href={`https://www.gbif.org/species/${result.externalId}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{`${result.source}:${result.externalId}`}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
`${result.source}:${result.externalId}`
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export type PlantInfoSearchResult = {
|
|||||||
family: string | null;
|
family: string | null;
|
||||||
genus: string | null;
|
genus: string | null;
|
||||||
species: string | null;
|
species: string | null;
|
||||||
|
commonNames: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlantLocation = {
|
export type PlantLocation = {
|
||||||
|
|||||||
@@ -757,31 +757,6 @@ dd {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-chip-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
margin: 8px 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.query-chip {
|
|
||||||
min-height: 30px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 1px solid #cfc4b4;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--field);
|
|
||||||
color: #33463b;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.query-chip:hover {
|
|
||||||
border-color: var(--link);
|
|
||||||
background: var(--sage);
|
|
||||||
color: var(--link);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plant-info-result {
|
.plant-info-result {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
assignPlantFlag,
|
assignPlantFlag,
|
||||||
completeCareTasksBulk,
|
completeCareTasksBulk,
|
||||||
@@ -56,6 +56,9 @@ import {
|
|||||||
import type { useAppEditors } from './use-app-editors';
|
import type { useAppEditors } from './use-app-editors';
|
||||||
import type { useDashboardData } from './use-dashboard-data';
|
import type { useDashboardData } from './use-dashboard-data';
|
||||||
|
|
||||||
|
const plantInfoSearchDebounceMs = 150;
|
||||||
|
const plantInfoSearchMinLength = 2;
|
||||||
|
|
||||||
type Editors = ReturnType<typeof useAppEditors>;
|
type Editors = ReturnType<typeof useAppEditors>;
|
||||||
type DashboardData = ReturnType<typeof useDashboardData>;
|
type DashboardData = ReturnType<typeof useDashboardData>;
|
||||||
|
|
||||||
@@ -90,6 +93,39 @@ export function useAppActions({
|
|||||||
const [plantInfoQuery, setPlantInfoQuery] = useState('');
|
const [plantInfoQuery, setPlantInfoQuery] = useState('');
|
||||||
const [plantInfoResults, setPlantInfoResults] = useState<PlantInfoSearchResult[]>([]);
|
const [plantInfoResults, setPlantInfoResults] = useState<PlantInfoSearchResult[]>([]);
|
||||||
const [hasSearchedPlantInfo, setHasSearchedPlantInfo] = useState(false);
|
const [hasSearchedPlantInfo, setHasSearchedPlantInfo] = useState(false);
|
||||||
|
const latestPlantInfoSearchId = useRef(0);
|
||||||
|
const lastCompletedPlantInfoQuery = useRef('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const query = plantInfoQuery.trim();
|
||||||
|
if (query.length === 0) {
|
||||||
|
latestPlantInfoSearchId.current += 1;
|
||||||
|
lastCompletedPlantInfoQuery.current = '';
|
||||||
|
setPlantInfoResults([]);
|
||||||
|
setHasSearchedPlantInfo(false);
|
||||||
|
setIsSearchingPlantInfo(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.length < plantInfoSearchMinLength) {
|
||||||
|
latestPlantInfoSearchId.current += 1;
|
||||||
|
lastCompletedPlantInfoQuery.current = '';
|
||||||
|
setPlantInfoResults([]);
|
||||||
|
setHasSearchedPlantInfo(false);
|
||||||
|
setIsSearchingPlantInfo(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query !== lastCompletedPlantInfoQuery.current) {
|
||||||
|
setIsSearchingPlantInfo(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = window.setTimeout(() => {
|
||||||
|
void searchTaxonInfo(query, { updateQuery: false });
|
||||||
|
}, plantInfoSearchDebounceMs);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timeout);
|
||||||
|
}, [plantInfoQuery]);
|
||||||
|
|
||||||
async function savePlant() {
|
async function savePlant() {
|
||||||
if (!editors.form.nickname.trim()) {
|
if (!editors.form.nickname.trim()) {
|
||||||
@@ -211,24 +247,44 @@ export function useAppActions({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchTaxonInfo(queryOverride?: string) {
|
async function searchTaxonInfo(
|
||||||
|
queryOverride?: string,
|
||||||
|
options: { updateQuery?: boolean } = {},
|
||||||
|
) {
|
||||||
const query = queryOverride ?? plantInfoQuery;
|
const query = queryOverride ?? plantInfoQuery;
|
||||||
setPlantInfoQuery(query);
|
if (options.updateQuery ?? true) {
|
||||||
|
setPlantInfoQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
if (query.trim().length < 2) {
|
const trimmedQuery = query.trim();
|
||||||
|
if (trimmedQuery.length < plantInfoSearchMinLength) {
|
||||||
setError('Search needs at least 2 characters.');
|
setError('Search needs at least 2 characters.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trimmedQuery === lastCompletedPlantInfoQuery.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchId = latestPlantInfoSearchId.current + 1;
|
||||||
|
latestPlantInfoSearchId.current = searchId;
|
||||||
setIsSearchingPlantInfo(true);
|
setIsSearchingPlantInfo(true);
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
setHasSearchedPlantInfo(true);
|
setHasSearchedPlantInfo(true);
|
||||||
setPlantInfoResults(await searchPlantInfo(query));
|
const results = await searchPlantInfo(trimmedQuery);
|
||||||
|
if (latestPlantInfoSearchId.current === searchId) {
|
||||||
|
lastCompletedPlantInfoQuery.current = trimmedQuery;
|
||||||
|
setPlantInfoResults(results);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError('Could not search offline plant info.');
|
if (latestPlantInfoSearchId.current === searchId) {
|
||||||
|
setError('Could not search offline plant info.');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSearchingPlantInfo(false);
|
if (latestPlantInfoSearchId.current === searchId) {
|
||||||
|
setIsSearchingPlantInfo(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ namespace plant_manager.Services
|
|||||||
r.Family,
|
r.Family,
|
||||||
r.Genus,
|
r.Genus,
|
||||||
r.Species,
|
r.Species,
|
||||||
|
r.AliasesText,
|
||||||
CASE
|
CASE
|
||||||
WHEN lower(coalesce(r.CommonName, '')) = $normalized THEN 0
|
WHEN lower(coalesce(r.CommonName, '')) = $normalized THEN 0
|
||||||
WHEN lower(coalesce(r.CanonicalName, '')) = $normalized THEN 1
|
WHEN lower(coalesce(r.CanonicalName, '')) = $normalized THEN 1
|
||||||
@@ -88,7 +89,8 @@ namespace plant_manager.Services
|
|||||||
ReadNullableString(reader, 6),
|
ReadNullableString(reader, 6),
|
||||||
ReadNullableString(reader, 7),
|
ReadNullableString(reader, 7),
|
||||||
ReadNullableString(reader, 8),
|
ReadNullableString(reader, 8),
|
||||||
ReadNullableString(reader, 9)));
|
ReadNullableString(reader, 9),
|
||||||
|
ToCommonNames(ReadNullableString(reader, 10))));
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
@@ -120,6 +122,20 @@ namespace plant_manager.Services
|
|||||||
|
|
||||||
private static string? ReadNullableString(IDataRecord reader, int ordinal) =>
|
private static string? ReadNullableString(IDataRecord reader, int ordinal) =>
|
||||||
reader.IsDBNull(ordinal) ? null : reader.GetString(ordinal);
|
reader.IsDBNull(ordinal) ? null : reader.GetString(ordinal);
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> ToCommonNames(string? aliasesText)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(aliasesText))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliasesText
|
||||||
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Order(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PlantInfoSearchResultDto(
|
public record PlantInfoSearchResultDto(
|
||||||
@@ -132,5 +148,6 @@ namespace plant_manager.Services
|
|||||||
string? Status,
|
string? Status,
|
||||||
string? Family,
|
string? Family,
|
||||||
string? Genus,
|
string? Genus,
|
||||||
string? Species);
|
string? Species,
|
||||||
|
IReadOnlyList<string> CommonNames);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user