mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
changes
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"@edr/ui-common": "workspace:*",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@mantine/core": "^9.3.0",
|
||||
"@mantine/dates": "^9.3.0",
|
||||
"@mantine/hooks": "^9.3.0",
|
||||
"@tabler/icons-react": "^3.44.0",
|
||||
"@tanstack/react-query": "^5.100.11",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/dates/styles.css";
|
||||
import "@edr/ui-common/styles.css";
|
||||
import "../index.css";
|
||||
import "@edr/ui-common/theme.css";
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Combobox,
|
||||
Group,
|
||||
InputBase,
|
||||
Loader,
|
||||
Modal,
|
||||
Text,
|
||||
useCombobox,
|
||||
} from "@mantine/core";
|
||||
import { Check, MapPin, Search } from "lucide-react";
|
||||
import {
|
||||
@@ -265,7 +263,7 @@ export interface LocationPickerProps {
|
||||
|
||||
/**
|
||||
* Address + map location picker backed by Google Maps:
|
||||
* - type to search (Geocoding API forward geocoding, debounced),
|
||||
* - type to search (Places Autocomplete, debounced),
|
||||
* - or click anywhere on the map to drop a pin (reverse geocoding).
|
||||
* Reports the resolved address and coordinates up via `onChange`.
|
||||
*/
|
||||
@@ -386,7 +384,6 @@ function LocationPickerInline({
|
||||
mapHeight = 260,
|
||||
withinPortal = true,
|
||||
}: LocationPickerProps & { mapHeight?: number; withinPortal?: boolean }) {
|
||||
const combobox = useCombobox();
|
||||
const geocoder = useGeocoder();
|
||||
const places = usePlacesSearch();
|
||||
const placesLib = useMapsLibrary("places");
|
||||
@@ -394,7 +391,6 @@ function LocationPickerInline({
|
||||
const [results, setResults] = useState<PlacePrediction[]>([]);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [resolving, setResolving] = useState(false);
|
||||
const searchStaleRef = useRef<{ stale: boolean } | null>(null);
|
||||
const reverseStaleRef = useRef<{ stale: boolean } | null>(null);
|
||||
// One Autocomplete session groups every keystroke of a search with the final
|
||||
// Details fetch into a single billable unit. Reset after each pick.
|
||||
@@ -409,9 +405,8 @@ function LocationPickerInline({
|
||||
|
||||
// Debounced forward search — fires only after the user stops typing
|
||||
// (SEARCH_DEBOUNCE_MS of silence), so we make one request per pause rather
|
||||
// than one per keystroke. The dropdown is kept open the whole time so the
|
||||
// user sees the "Searching…" state and then the live results for what they
|
||||
// typed.
|
||||
// than one per keystroke. While it runs, the input shows a spinner; the
|
||||
// dropdown itself only appears once there are predictions to show.
|
||||
useEffect(() => {
|
||||
const q = query.trim();
|
||||
if (q.length < MIN_QUERY_LEN) {
|
||||
@@ -420,12 +415,10 @@ function LocationPickerInline({
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
combobox.openDropdown();
|
||||
if (!places) return; // re-runs once the places library loads
|
||||
// Autocomplete has no abort support, so a token marks superseded requests
|
||||
// and their responses are dropped instead of overwriting newer results.
|
||||
const token = { stale: false };
|
||||
searchStaleRef.current = token;
|
||||
const handle = setTimeout(async () => {
|
||||
const found = await searchPlaces(
|
||||
places.autocomplete,
|
||||
@@ -435,13 +428,12 @@ function LocationPickerInline({
|
||||
if (token.stale) return;
|
||||
setResults(found);
|
||||
setSearching(false);
|
||||
combobox.openDropdown();
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
return () => {
|
||||
clearTimeout(handle);
|
||||
token.stale = true;
|
||||
};
|
||||
}, [query, places, combobox]);
|
||||
}, [query, places]);
|
||||
|
||||
// Drop any in-flight reverse lookup when the picker unmounts.
|
||||
useEffect(
|
||||
@@ -453,7 +445,10 @@ function LocationPickerInline({
|
||||
|
||||
const selectResult = useCallback(
|
||||
async (prediction: PlacePrediction) => {
|
||||
combobox.closeDropdown();
|
||||
// Clear the query/results immediately so the pending debounce can't fire
|
||||
// a search for the picked address and pop the dropdown back open.
|
||||
setQuery("");
|
||||
setResults([]);
|
||||
// Predictions carry no coordinates — resolve them now via Place Details.
|
||||
if (!places) return;
|
||||
setResolving(true);
|
||||
@@ -474,10 +469,8 @@ function LocationPickerInline({
|
||||
lat: resolved.lat,
|
||||
lng: resolved.lng,
|
||||
});
|
||||
setQuery("");
|
||||
setResults([]);
|
||||
},
|
||||
[onChange, combobox, places, placesLib],
|
||||
[onChange, places, placesLib],
|
||||
);
|
||||
|
||||
const handlePin = useCallback(
|
||||
@@ -515,60 +508,44 @@ function LocationPickerInline({
|
||||
? { lat: value.lat as number, lng: value.lng as number }
|
||||
: DEFAULT_CENTER;
|
||||
|
||||
// Mantine Autocomplete requires unique option values; predictions are keyed
|
||||
// by their display text, so de-duplicate the rare identical descriptions.
|
||||
const optionsByName = useMemo(() => {
|
||||
const byName = new Map<string, PlacePrediction>();
|
||||
for (const r of results) {
|
||||
if (!byName.has(r.displayName)) byName.set(r.displayName, r);
|
||||
}
|
||||
return byName;
|
||||
}, [results]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Combobox
|
||||
store={combobox}
|
||||
withinPortal={withinPortal}
|
||||
shadow="md"
|
||||
radius="md"
|
||||
>
|
||||
<Combobox.Target>
|
||||
<InputBase
|
||||
label={label || undefined}
|
||||
placeholder={placeholder}
|
||||
value={inputValue}
|
||||
error={error}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
leftSection={<Search size={16} />}
|
||||
rightSection={searching || resolving ? <Loader size={14} /> : null}
|
||||
onChange={(e) => {
|
||||
setQuery(e.currentTarget.value);
|
||||
combobox.openDropdown();
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (query.trim().length >= MIN_QUERY_LEN) combobox.openDropdown();
|
||||
}}
|
||||
/>
|
||||
</Combobox.Target>
|
||||
|
||||
<Combobox.Dropdown>
|
||||
<Combobox.Options mah={240} style={{ overflowY: "auto" }}>
|
||||
{searching ? (
|
||||
<Combobox.Empty>Searching “{query.trim()}”…</Combobox.Empty>
|
||||
) : results.length === 0 ? (
|
||||
<Combobox.Empty>
|
||||
{query.trim().length < MIN_QUERY_LEN
|
||||
? `Type at least ${MIN_QUERY_LEN} characters`
|
||||
: "No matching places"}
|
||||
</Combobox.Empty>
|
||||
) : (
|
||||
results.map((r, i) => (
|
||||
<Combobox.Option
|
||||
key={`${r.placeId}-${i}`}
|
||||
value={String(i)}
|
||||
onClick={() => void selectResult(r)}
|
||||
>
|
||||
<Text fz={13} lineClamp={2}>
|
||||
{r.displayName}
|
||||
</Text>
|
||||
</Combobox.Option>
|
||||
))
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Combobox.Dropdown>
|
||||
</Combobox>
|
||||
<Autocomplete
|
||||
label={label || undefined}
|
||||
placeholder={placeholder}
|
||||
value={inputValue}
|
||||
error={error}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
leftSection={<Search size={16} />}
|
||||
rightSection={searching || resolving ? <Loader size={14} /> : null}
|
||||
data={[...optionsByName.keys()]}
|
||||
// Predictions are already ranked by the Places API for the typed
|
||||
// query; Mantine's default substring filter would hide most of them.
|
||||
filter={({ options }) => options}
|
||||
maxDropdownHeight={240}
|
||||
comboboxProps={{ withinPortal, shadow: "md", radius: "md" }}
|
||||
onChange={setQuery}
|
||||
onOptionSubmit={(name) => {
|
||||
const prediction = optionsByName.get(name);
|
||||
if (prediction) void selectResult(prediction);
|
||||
}}
|
||||
renderOption={({ option }) => (
|
||||
<Text fz={13} lineClamp={2}>
|
||||
{option.value}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Box
|
||||
mt={10}
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -217,6 +217,9 @@ importers:
|
||||
'@mantine/core':
|
||||
specifier: ^9.3.0
|
||||
version: 9.3.0(@mantine/hooks@9.3.0(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@mantine/dates':
|
||||
specifier: ^9.3.0
|
||||
version: 9.3.2(@mantine/core@9.3.0(@mantine/hooks@9.3.0(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.3.0(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@mantine/hooks':
|
||||
specifier: ^9.3.0
|
||||
version: 9.3.0(react@19.2.6)
|
||||
|
||||
Reference in New Issue
Block a user