diff --git a/src/features/schema/fields/location/LocationBased.jsx b/src/features/schema/fields/location/LocationBased.jsx
index ce7bbe03fb..770aaac811 100644
--- a/src/features/schema/fields/location/LocationBased.jsx
+++ b/src/features/schema/fields/location/LocationBased.jsx
@@ -1,35 +1,272 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
-
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
-import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
-import InputSlider from './InputSlider';
import { set } from '../../../config/configSlice';
import { callHandler } from '../../../handlers/actions';
+// Helper function to extract all URL parameters
+const getUrlParams = () => {
+ // Parse the query string from the URL
+ const queryString = window.location.search;
+ const params = new URLSearchParams(queryString);
+
+ // Create an object with all parameters
+ const urlParams = {};
+ for (const [key, value] of params.entries()) {
+ urlParams[key] = value;
+ }
+
+ return urlParams;
+};
+
+// UI component library for the location picker dropdown
+// Base container for the entire dropdown component
+const Command = ({ children, className, onClick }) => (
+
+ {children}
+
+);
+
+// Text input component for the search box
+const CommandInput = ({ placeholder, value, onValueChange, onClick }) => (
+ onValueChange(e.target.value)}
+ onClick={onClick}
+ />
+);
+
+// Container for the results dropdown list
+const CommandList = ({ children }) => (
+
+ {children}
+
+);
+
+// Individual selectable item in the dropdown list
+const CommandItem = ({ children, onSelect }) => (
+
+ {children}
+
+);
+
+// LocationPicker component that handles location search via Geoapify API
+const LocationPicker = ({ onLocationSelect, initialLocation }) => {
+ // State management for the search and results
+ const [query, setQuery] = useState(initialLocation?.locality || '');
+ const [results, setResults] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [isOpen, setIsOpen] = useState(false);
+
+ // Debounce utility to prevent excessive API calls during typing
+ const debounce = (func, wait) => {
+ let timeout;
+ return (...args) => {
+ clearTimeout(timeout);
+ timeout = setTimeout(() => func(...args), wait);
+ };
+ };
+
+ // Function to search locations via Geoapify API
+ const searchLocations = async (searchQuery) => {
+ if (!searchQuery) {
+ setResults([]);
+ setIsOpen(false); // Close dropdown if query is empty
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const apiKey = '49863bd631b84a169b347fafbf128ce6';
+ const encodedQuery = encodeURIComponent(searchQuery);
+ const response = await fetch(
+ `https://api.geoapify.com/v1/geocode/search?text=${encodedQuery}&apiKey=${apiKey}`
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch locations');
+ }
+
+ const data = await response.json();
+
+ // Transform the API response to our internal format
+ const locations = data.features.map(feature => ({
+ formattedName: feature.properties.formatted,
+ locality: feature.properties.formatted,
+ lat: feature.properties.lat,
+ lng: feature.properties.lon,
+ timezone: feature.properties.timezone?.name || 'America/New_York'
+ }));
+
+ setResults(locations);
+ } catch (err) {
+ setError('Failed to load locations');
+ console.error('Error fetching locations:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Create a debounced search function to delay API calls
+ const debouncedSearch = useCallback(
+ debounce(searchLocations, 300),
+ []
+ );
+
+ // Handler for search input changes
+ const handleSearch = (value) => {
+ setQuery(value);
+ setIsOpen(true); // Open dropdown when searching
+ debouncedSearch(value);
+ };
+
+ // Effect to handle clicks outside the component to close the dropdown
+ useEffect(() => {
+ const handleClickOutside = () => {
+ setIsOpen(false);
+ };
+
+ // Add event listener when component mounts
+ document.addEventListener('click', handleClickOutside);
+
+ // Clean up the event listener on unmount
+ return () => {
+ document.removeEventListener('click', handleClickOutside);
+ };
+ }, []);
+
+ // Prevent dropdown closure when clicking inside the component
+ const handleCommandClick = (e) => {
+ e.stopPropagation();
+ };
+
+ return (
+
+ setIsOpen(true)} // Open dropdown when clicking input
+ />
+ {isOpen && loading && (
+
+ Loading...
+
+ )}
+ {isOpen && error && (
+
+ {error}
+
+ )}
+ {isOpen && results.length > 0 && (
+
+ {results.map((location, index) => (
+ {
+ onLocationSelect(location);
+ setIsOpen(false); // Close dropdown after selection
+ setResults([]); // Clear results
+ }}
+ >
+ 📍
+ {location.formattedName}
+
+ ))}
+
+ )}
+
+ );
+};
+
+// Main component that extends the LocationForm with handler integration
export default function LocationBased({ field }) {
- const [value, setValue] = useState({
- // Default to Brooklyn, because that's where tidbyt folks
- // are and we can only dispatch a location object which
- // has all fields set.
- 'lat': 40.678,
- 'lng': -73.944,
- 'locality': 'Brooklyn, New York',
- 'timezone': 'America/New_York',
- // But overwrite with app-specific defaults set in config.
- 'display': '',
- 'value': '',
- ...field.default
- });
+ // Get URL parameters to check for location data
+ const urlParams = getUrlParams();
+
+ // Try to find location data in the URL parameters
+ let urlLocation = null;
+
+ // First check if field.id exists as a parameter
+ if (urlParams[field.id]) {
+ try {
+ const parsed = JSON.parse(decodeURIComponent(urlParams[field.id]));
+ if (parsed && typeof parsed === 'object' && 'locality' in parsed) {
+ urlLocation = parsed;
+ }
+ } catch (err) {
+ console.error(`Error parsing ${field.id} from URL:`, err);
+ }
+ }
+
+ // If location not found by field.id, search all URL parameters for valid location data
+ if (!urlLocation) {
+ for (const [key, value] of Object.entries(urlParams)) {
+ try {
+ const parsed = JSON.parse(decodeURIComponent(value));
+ if (parsed && typeof parsed === 'object' && 'locality' in parsed) {
+ urlLocation = parsed;
+ break; // Found a valid location, stop searching
+ }
+ } catch (err) {
+ // Not a valid JSON, continue to next parameter
+ continue;
+ }
+ }
+ }
+
+ // Function to calculate default values with fallbacks and overrides
+ const getDefaultValues = () => {
+ const defaultValues = {
+ // Default to Brooklyn as a fallback
+ 'lat': 40.678,
+ 'lng': -73.944,
+ 'locality': 'Brooklyn, New York',
+ 'timezone': 'America/New_York',
+ 'display': '', // For dropdown display value
+ 'value': '', // For dropdown selected value
+ };
+ // Override with field.default if available
+ if (field.default) {
+ Object.assign(defaultValues, field.default);
+ }
+
+ // Override with URL location if available
+ if (urlLocation) {
+ // Merge the URL location with defaults for any missing fields
+ return {
+ ...defaultValues,
+ ...urlLocation
+ };
+ }
+
+ return defaultValues;
+ };
+
+ // Component state management
+ const [value, setValue] = useState(getDefaultValues());
+ const [initialized, setInitialized] = useState(false);
+
+ // Redux hooks
const config = useSelector(state => state.config);
const dispatch = useDispatch();
- const handlerResults = useSelector(state => state.handlers)
+ const handlerResults = useSelector(state => state.handlers);
+ // Helper function to format location for handlers
const getLocationAsJson = (v) => {
return JSON.stringify({
lat: v.lat,
@@ -37,73 +274,81 @@ export default function LocationBased({ field }) {
locality: v.locality,
timezone: v.timezone
});
- }
+ };
+ // Effect to initialize the component
useEffect(() => {
- if (field.id in config) {
+ // Set initialized to true
+ setInitialized(true);
+ }, []);
+
+ // Effect to load configuration from Redux or initialize with defaults
+ useEffect(() => {
+ if (initialized && field.id in config) {
+ // Load saved configuration from Redux
let v = JSON.parse(config[field.id].value);
setValue(v);
+
+ // Call the associated handler with the location data
callHandler(field.id, field.handler, getLocationAsJson(v));
- } else if (field.default) {
- value = field.default;
+ } else if (initialized) {
+ // Use default values (including URL params if available)
+ const defaultValues = getDefaultValues();
+
+ // Save to Redux
dispatch(set({
id: field.id,
- value: JSON.stringify(field.default),
+ value: JSON.stringify(defaultValues),
}));
- }
- }, [config]);
+ setValue(defaultValues);
- useEffect(() => {
- if (!(field.id in config)) {
- callHandler(field.id, field.handler, getLocationAsJson(value));
+ // Call handler with initial location
+ callHandler(field.id, field.handler, getLocationAsJson(defaultValues));
}
- }, []);
+ }, [config, dispatch, field.id, field.default, field.handler, initialized]);
- const setLocationPart = (partName, partValue) => {
- let newValue = { ...value };
- newValue[partName] = partValue;
+ // Handler for location selection from the picker
+ const handleLocationSelect = (location) => {
+ // Create new state with selected location
+ const newValue = {
+ ...value,
+ lat: location.lat,
+ lng: location.lng,
+ locality: location.locality || location.formattedName,
+ timezone: location.timezone
+ };
+
+ // Update state and Redux store
setValue(newValue);
dispatch(set({
id: field.id,
value: JSON.stringify(newValue),
}));
- callHandler(field.id, field.handler, getLocationAsJson(newValue));
- }
-
- const truncateLatLng = (value) => {
- return String(Number(value).toFixed(3));
- }
- const onChangeLatitude = (event) => {
- setLocationPart('lat', truncateLatLng(event.target.value));
- }
-
- const onChangeLongitude = (event) => {
- setLocationPart('lng', truncateLatLng(event.target.value));
- }
-
- const onChangeLocality = (event) => {
- setLocationPart('locality', event.target.value);
- }
-
- const onChangeTimezone = (event) => {
- setLocationPart('timezone', event.target.value);
- }
+ // Call handler with new location to update location-dependent options
+ callHandler(field.id, field.handler, getLocationAsJson(newValue));
+ };
+ // Handler for dropdown option selection
const onChangeOption = (event) => {
let newValue = { ...value };
newValue.value = event.target.value;
+
+ // Get display text from selected option
const selectedOption = options.find(option => option.value === event.target.value);
if (selectedOption) {
newValue.display = selectedOption.display;
}
+
+ // Update state and Redux store
setValue(newValue);
dispatch(set({
id: field.id,
value: JSON.stringify(newValue),
}));
- }
+ };
+ // Get options from handler results for the dropdown
let options = [];
if (field.id in handlerResults.values) {
options = handlerResults.values[field.id];
@@ -111,50 +356,32 @@ export default function LocationBased({ field }) {
return (
- Latitude
-
-
- Longitude
-
-
- Locality
- Location
+
- Timezone
-
- Options for chosen location
+
+ {/* Display selected location details */}
+ {value && (
+
+ Selected: {value.locality}
+ Coordinates: {value.lat.toFixed(3)}, {value.lng.toFixed(3)}
+ Timezone: {value.timezone}
+
+ )}
+
+ {/* Options dropdown for the chosen location */}
+ Options for chosen location
);
diff --git a/src/features/schema/fields/location/LocationForm.jsx b/src/features/schema/fields/location/LocationForm.jsx
index f3da710c87..92ccd85074 100644
--- a/src/features/schema/fields/location/LocationForm.jsx
+++ b/src/features/schema/fields/location/LocationForm.jsx
@@ -1,110 +1,300 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
-
-import MenuItem from '@mui/material/MenuItem';
-import FormControl from '@mui/material/FormControl';
-import Select from '@mui/material/Select';
-import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
+import FormControl from '@mui/material/FormControl';
-import InputSlider from './InputSlider';
import { set } from '../../../config/configSlice';
+// Command components for the location picker - these create the UI for the dropdown interface
+// Container component that wraps the entire command interface
+const Command = ({ children, className }) => (
+
+ {children}
+
+);
+
+// Input field component for search queries
+const CommandInput = ({ placeholder, value, onValueChange }) => (
+ onValueChange(e.target.value)}
+ />
+);
+
+// Container for the dropdown list of location results
+const CommandList = ({ children }) => (
+
+ {children}
+
+);
+
+// Individual item in the dropdown list that user can select
+const CommandItem = ({ children, onSelect }) => (
+
+ {children}
+
+);
+
+// Helper function to extract URL parameters
+const getUrlParams = () => {
+ // Parse the query string from the URL
+ const queryString = window.location.search;
+ const params = new URLSearchParams(queryString);
+
+ // Create an object with all parameters
+ const urlParams = {};
+ for (const [key, value] of params.entries()) {
+ urlParams[key] = value;
+ }
+
+ return urlParams;
+};
+
+// Main location picker component that integrates with Geoapify API
+const LocationPicker = ({ onLocationSelect, initialLocation }) => {
+ // State for search input, results, loading status, and dropdown visibility
+ const [query, setQuery] = useState(initialLocation?.locality || '');
+ const [results, setResults] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [isOpen, setIsOpen] = useState(false);
+
+ // Debounce function to prevent excessive API calls during typing
+ const debounce = (func, wait) => {
+ let timeout;
+ return (...args) => {
+ clearTimeout(timeout);
+ timeout = setTimeout(() => func(...args), wait);
+ };
+ };
+
+ // Function to fetch location results from Geoapify API
+ const searchLocations = async (searchQuery) => {
+ if (!searchQuery) {
+ setResults([]);
+ setIsOpen(false); // Close dropdown if query is empty
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const apiKey = '49863bd631b84a169b347fafbf128ce6';
+ const encodedQuery = encodeURIComponent(searchQuery);
+ const response = await fetch(
+ `https://api.geoapify.com/v1/geocode/search?text=${encodedQuery}&apiKey=${apiKey}`
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch locations');
+ }
+
+ const data = await response.json();
+
+ // Transform API response to match our format
+ const locations = data.features.map(feature => ({
+ formattedName: feature.properties.formatted,
+ locality: feature.properties.formatted,
+ lat: feature.properties.lat,
+ lng: feature.properties.lon,
+ timezone: feature.properties.timezone?.name || 'America/New_York'
+ }));
+
+ setResults(locations);
+ } catch (err) {
+ setError('Failed to load locations');
+ console.error('Error fetching locations:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Create a debounced version of the search function (300ms delay)
+ const debouncedSearch = useCallback(
+ debounce(searchLocations, 300),
+ []
+ );
+
+ // Handler for search input changes
+ const handleSearch = (value) => {
+ setQuery(value);
+ setIsOpen(true); // Open dropdown when searching
+ debouncedSearch(value);
+ };
+
+ // Close dropdown when clicking outside the component
+ useEffect(() => {
+ const handleClickOutside = () => {
+ setIsOpen(false);
+ };
+
+ // Add event listener when component mounts
+ document.addEventListener('click', handleClickOutside);
+
+ // Return cleanup function
+ return () => {
+ document.removeEventListener('click', handleClickOutside);
+ };
+ }, []);
+
+ // Prevent dropdown from closing when clicking inside the component
+ const handleCommandClick = (e) => {
+ e.stopPropagation();
+ };
+
+ return (
+
+ setIsOpen(true)} // Open dropdown when clicking input
+ />
+ {isOpen && loading && (
+
+ Loading...
+
+ )}
+ {isOpen && error && (
+
+ {error}
+
+ )}
+ {isOpen && results.length > 0 && (
+
+ {results.map((location, index) => (
+ {
+ onLocationSelect(location);
+ setIsOpen(false); // Close dropdown after selection
+ setResults([]); // Clear results
+ }}
+ >
+ 📍
+ {location.formattedName}
+
+ ))}
+
+ )}
+
+ );
+};
+
+// Main exported component that wraps the LocationPicker with Redux integration
export default function LocationForm({ field }) {
- const [value, setValue] = useState({
- // Default to Brooklyn, because that's where tidbyt folks
- // are and we can only dispatch a location object which
- // has all fields set.
- 'lat': 40.678,
- 'lng': -73.944,
- 'locality': 'Brooklyn, New York',
- 'timezone': 'America/New_York',
- // But overwrite with app-specific defaults set in config.
- ...field.default
- });
+ // Get URL parameters to check for location data
+ const urlParams = getUrlParams();
- const config = useSelector(state => state.config);
+ // Check if location parameter exists in URL (it's a JSON string)
+ const urlLocationParam = urlParams.location;
+ let urlLocation = null;
+
+ // Try to parse the URL location parameter if it exists
+ if (urlLocationParam) {
+ try {
+ urlLocation = JSON.parse(decodeURIComponent(urlLocationParam));
+ } catch (err) {
+ console.error('Error parsing location from URL:', err);
+ }
+ }
+
+ // Determine default values based on defaults, field props, and URL params
+ const getDefaultValues = () => {
+ const defaultValues = {
+ // Default to Brooklyn as fallback
+ 'lat': 40.678,
+ 'lng': -73.944,
+ 'locality': 'Brooklyn, New York',
+ 'timezone': 'America/New_York',
+ };
+ // Override with field.default if available
+ if (field.default) {
+ Object.assign(defaultValues, field.default);
+ }
+
+ // Override with URL location if available
+ if (urlLocation) {
+ return urlLocation; // Replace the entire location object
+ }
+
+ return defaultValues;
+ };
+
+ // State for location value and component initialization tracking
+ const [value, setValue] = useState(getDefaultValues());
+ const [initialized, setInitialized] = useState(false);
+
+ // Redux hooks for state management
+ const config = useSelector(state => state.config);
const dispatch = useDispatch();
+ // Set component as initialized on first render
useEffect(() => {
- if (field.id in config) {
+ // Set initialized to true immediately - we don't need to search
+ // since we're getting the full location object from the URL
+ setInitialized(true);
+ }, []);
+
+ // Load location from Redux store or initialize with defaults
+ useEffect(() => {
+ if (initialized && field.id in config) {
+ // If location exists in Redux, use that value
setValue(JSON.parse(config[field.id].value));
- } else if (field.default) {
+ } else if (initialized) {
+ // Otherwise use default values and save to Redux
+ const defaultValues = getDefaultValues();
dispatch(set({
id: field.id,
- value: field.default,
+ value: JSON.stringify(defaultValues),
}));
+ setValue(defaultValues);
}
- }, [config])
+ }, [config, dispatch, field.id, field.default, initialized]);
+
+ // Handler for when user selects a location from the dropdown
+ const handleLocationSelect = (location) => {
+ const newValue = {
+ lat: location.lat,
+ lng: location.lng,
+ locality: location.locality || location.formattedName,
+ timezone: location.timezone
+ };
- const setPart = (partName, partValue) => {
- let newValue = { ...value };
- newValue[partName] = partValue;
+ // Update local state and Redux store
setValue(newValue);
dispatch(set({
id: field.id,
value: JSON.stringify(newValue),
}));
- }
-
- const truncateLatLng = (value) => {
- return String(Number(value).toFixed(3));
- }
-
- const onChangeLatitude = (event) => {
- setPart('lat', truncateLatLng(event.target.value));
- }
-
- const onChangeLongitude = (event) => {
- setPart('lng', truncateLatLng(event.target.value));
- }
-
- const onChangeLocality = (event) => {
- setPart('locality', event.target.value);
- }
-
- const onChangeTimezone = (event) => {
- setPart('timezone', event.target.value);
- }
+ };
return (
- Latitude
-
-
- Longitude
-
-
- Locality
- Location
+
+ {/* Location search component */}
+
- Timezone
-
+
+ {/* Display the currently selected location details */}
+ {value && (
+
+ Selected: {value.locality}
+ Coordinates: {value.lat.toFixed(3)}, {value.lng.toFixed(3)}
+ Timezone: {value.timezone}
+
+ )}
);
}
\ No newline at end of file