pl-fe: Remove location search reducer
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
df3b5dfc35
commit
6c61f749fa
10 changed files with 43 additions and 113 deletions
|
@ -679,7 +679,7 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
let completion = '', startPosition = position;
|
let completion = '', startPosition = position;
|
||||||
|
|
||||||
if (typeof suggestion === 'object' && suggestion.id) {
|
if (typeof suggestion === 'object' && 'id' in suggestion) {
|
||||||
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
||||||
startPosition = position - 1;
|
startPosition = position - 1;
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,6 @@ import { STATUS_FETCH_SOURCE_FAIL, STATUS_FETCH_SOURCE_REQUEST, STATUS_FETCH_SOU
|
||||||
import type { Account, CreateEventParams, Location, MediaAttachment, PaginatedResponse, Status } from 'pl-api';
|
import type { Account, CreateEventParams, Location, MediaAttachment, PaginatedResponse, Status } from 'pl-api';
|
||||||
import type { AppDispatch, RootState } from 'pl-fe/store';
|
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||||
|
|
||||||
const LOCATION_SEARCH_REQUEST = 'LOCATION_SEARCH_REQUEST' as const;
|
|
||||||
const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS' as const;
|
|
||||||
const LOCATION_SEARCH_FAIL = 'LOCATION_SEARCH_FAIL' as const;
|
|
||||||
|
|
||||||
const EVENT_SUBMIT_REQUEST = 'EVENT_SUBMIT_REQUEST' as const;
|
const EVENT_SUBMIT_REQUEST = 'EVENT_SUBMIT_REQUEST' as const;
|
||||||
const EVENT_SUBMIT_SUCCESS = 'EVENT_SUBMIT_SUCCESS' as const;
|
const EVENT_SUBMIT_SUCCESS = 'EVENT_SUBMIT_SUCCESS' as const;
|
||||||
const EVENT_SUBMIT_FAIL = 'EVENT_SUBMIT_FAIL' as const;
|
const EVENT_SUBMIT_FAIL = 'EVENT_SUBMIT_FAIL' as const;
|
||||||
|
@ -73,18 +69,6 @@ const messages = defineMessages({
|
||||||
rejected: { id: 'compose_event.participation_requests.reject_success', defaultMessage: 'User rejected' },
|
rejected: { id: 'compose_event.participation_requests.reject_success', defaultMessage: 'User rejected' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const locationSearch = (query: string, signal?: AbortSignal) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: LOCATION_SEARCH_REQUEST, query });
|
|
||||||
return getClient(getState).search.searchLocation(query, { signal }).then((locations) => {
|
|
||||||
dispatch({ type: LOCATION_SEARCH_SUCCESS, locations });
|
|
||||||
return locations;
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: LOCATION_SEARCH_FAIL });
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitEvent = ({
|
const submitEvent = ({
|
||||||
statusId,
|
statusId,
|
||||||
name,
|
name,
|
||||||
|
@ -509,9 +493,6 @@ type EventsAction =
|
||||||
| EventFormSetAction;
|
| EventFormSetAction;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
LOCATION_SEARCH_REQUEST,
|
|
||||||
LOCATION_SEARCH_SUCCESS,
|
|
||||||
LOCATION_SEARCH_FAIL,
|
|
||||||
EVENT_SUBMIT_REQUEST,
|
EVENT_SUBMIT_REQUEST,
|
||||||
EVENT_SUBMIT_SUCCESS,
|
EVENT_SUBMIT_SUCCESS,
|
||||||
EVENT_SUBMIT_FAIL,
|
EVENT_SUBMIT_FAIL,
|
||||||
|
@ -547,7 +528,6 @@ export {
|
||||||
JOINED_EVENTS_FETCH_REQUEST,
|
JOINED_EVENTS_FETCH_REQUEST,
|
||||||
JOINED_EVENTS_FETCH_SUCCESS,
|
JOINED_EVENTS_FETCH_SUCCESS,
|
||||||
JOINED_EVENTS_FETCH_FAIL,
|
JOINED_EVENTS_FETCH_FAIL,
|
||||||
locationSearch,
|
|
||||||
submitEvent,
|
submitEvent,
|
||||||
submitEventRequest,
|
submitEventRequest,
|
||||||
submitEventSuccess,
|
submitEventSuccess,
|
||||||
|
|
16
packages/pl-fe/src/api/hooks/search/use-search-location.ts
Normal file
16
packages/pl-fe/src/api/hooks/search/use-search-location.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useClient } from 'pl-fe/hooks/use-client';
|
||||||
|
|
||||||
|
const useSearchLocation = (query: string) => {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['search', 'location', query],
|
||||||
|
queryFn: ({ signal }) => client.search.searchLocation(query, { signal }),
|
||||||
|
gcTime: 60 * 1000,
|
||||||
|
enabled: !!query.trim(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useSearchLocation };
|
|
@ -8,15 +8,18 @@ import Portal from 'pl-fe/components/ui/portal';
|
||||||
import AutosuggestAccount from 'pl-fe/features/compose/components/autosuggest-account';
|
import AutosuggestAccount from 'pl-fe/features/compose/components/autosuggest-account';
|
||||||
import { textAtCursorMatchesToken } from 'pl-fe/utils/suggestions';
|
import { textAtCursorMatchesToken } from 'pl-fe/utils/suggestions';
|
||||||
|
|
||||||
|
import AutosuggestLocation from './autosuggest-location';
|
||||||
|
|
||||||
|
import type { Location } from 'pl-api';
|
||||||
import type { Menu, MenuItem } from 'pl-fe/components/dropdown-menu';
|
import type { Menu, MenuItem } from 'pl-fe/components/dropdown-menu';
|
||||||
import type { InputThemes } from 'pl-fe/components/ui/input';
|
import type { InputThemes } from 'pl-fe/components/ui/input';
|
||||||
import type { Emoji } from 'pl-fe/features/emoji';
|
import type { Emoji } from 'pl-fe/features/emoji';
|
||||||
|
|
||||||
type AutoSuggestion = string | Emoji;
|
type AutoSuggestion = string | Emoji | Location;
|
||||||
|
|
||||||
interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
|
interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
|
||||||
value: string;
|
value: string;
|
||||||
suggestions: Array<any>;
|
suggestions: Array<AutoSuggestion>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void;
|
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void;
|
||||||
|
@ -29,7 +32,6 @@ interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>,
|
||||||
searchTokens?: string[];
|
searchTokens?: string[];
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
menu?: Menu;
|
menu?: Menu;
|
||||||
renderSuggestion?: React.FC<{ id: string }>;
|
|
||||||
hidePortal?: boolean;
|
hidePortal?: boolean;
|
||||||
theme?: InputThemes;
|
theme?: InputThemes;
|
||||||
}
|
}
|
||||||
|
@ -165,16 +167,12 @@ const AutosuggestInput: React.FC<IAutosuggestInput> = ({
|
||||||
const renderSuggestion = (suggestion: AutoSuggestion, i: number) => {
|
const renderSuggestion = (suggestion: AutoSuggestion, i: number) => {
|
||||||
let inner, key;
|
let inner, key;
|
||||||
|
|
||||||
if (props.renderSuggestion && typeof suggestion === 'string') {
|
if (typeof suggestion === 'object' && 'origin_id' in suggestion) {
|
||||||
const RenderSuggestion = props.renderSuggestion;
|
inner = <AutosuggestLocation location={suggestion} />;
|
||||||
inner = <RenderSuggestion id={suggestion} />;
|
key = suggestion.origin_id;
|
||||||
key = suggestion;
|
|
||||||
} else if (typeof suggestion === 'object') {
|
} else if (typeof suggestion === 'object') {
|
||||||
inner = <AutosuggestEmoji emoji={suggestion} />;
|
inner = <AutosuggestEmoji emoji={suggestion} />;
|
||||||
key = suggestion.id;
|
key = suggestion.id;
|
||||||
} else if (suggestion[0] === '#') {
|
|
||||||
inner = suggestion;
|
|
||||||
key = suggestion;
|
|
||||||
} else {
|
} else {
|
||||||
inner = <AutosuggestAccount id={suggestion} />;
|
inner = <AutosuggestAccount id={suggestion} />;
|
||||||
key = suggestion;
|
key = suggestion;
|
||||||
|
|
|
@ -4,7 +4,8 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Icon from 'pl-fe/components/ui/icon';
|
import Icon from 'pl-fe/components/ui/icon';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
|
||||||
|
import type { Location } from 'pl-api';
|
||||||
|
|
||||||
const buildingCommunityIcon = require('@tabler/icons/outline/building-community.svg');
|
const buildingCommunityIcon = require('@tabler/icons/outline/building-community.svg');
|
||||||
const homeIcon = require('@tabler/icons/outline/home-2.svg');
|
const homeIcon = require('@tabler/icons/outline/home-2.svg');
|
||||||
|
@ -21,12 +22,10 @@ const ADDRESS_ICONS: Record<string, string> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IAutosuggestLocation {
|
interface IAutosuggestLocation {
|
||||||
id: string;
|
location: Location;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ id }) => {
|
const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ location }) => {
|
||||||
const location = useAppSelector((state) => state.locations.get(id));
|
|
||||||
|
|
||||||
if (!location) return null;
|
if (!location) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
|
import { useDebounce } from '@uidotdev/usehooks';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import throttle from 'lodash/throttle';
|
import React, { useState } from 'react';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { locationSearch } from 'pl-fe/actions/events';
|
import { useSearchLocation } from 'pl-fe/api/hooks/search/use-search-location';
|
||||||
import AutosuggestInput, { AutoSuggestion } from 'pl-fe/components/autosuggest-input';
|
import AutosuggestInput, { AutoSuggestion } from 'pl-fe/components/autosuggest-input';
|
||||||
import Icon from 'pl-fe/components/icon';
|
import Icon from 'pl-fe/components/icon';
|
||||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
|
||||||
|
|
||||||
import AutosuggestLocation from './autosuggest-location';
|
import type { Location } from 'pl-api';
|
||||||
|
|
||||||
const noOp = () => {};
|
const noOp = () => {};
|
||||||
|
|
||||||
|
@ -17,27 +16,24 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ILocationSearch {
|
interface ILocationSearch {
|
||||||
onSelected: (locationId: string) => void;
|
onSelected: (location: Location) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
|
const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const [locationIds, setLocationIds] = useState<Array<string>>([]);
|
|
||||||
const controller = useRef(new AbortController());
|
|
||||||
|
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
const debouncedValue = useDebounce(value, 400);
|
||||||
|
const locationsQuery = useSearchLocation(debouncedValue);
|
||||||
|
|
||||||
const empty = !(value.length > 0);
|
const empty = !(value.length > 0);
|
||||||
|
|
||||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
const handleChange: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
||||||
refreshCancelToken();
|
|
||||||
handleLocationSearch(target.value);
|
|
||||||
setValue(target.value);
|
setValue(target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelected = (_tokenStart: number, _lastToken: string | null, suggestion: AutoSuggestion) => {
|
const handleSelected = (_tokenStart: number, _lastToken: string | null, suggestion: AutoSuggestion) => {
|
||||||
if (typeof suggestion === 'string') {
|
if (typeof suggestion === 'object' && 'origin_id' in suggestion) {
|
||||||
onSelected(suggestion);
|
onSelected(suggestion);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -56,30 +52,6 @@ const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshCancelToken = () => {
|
|
||||||
controller.current.abort();
|
|
||||||
controller.current = new AbortController();
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearResults = () => {
|
|
||||||
setLocationIds([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLocationSearch = useCallback(throttle(q => {
|
|
||||||
dispatch(locationSearch(q, controller.current.signal))
|
|
||||||
.then((locations: { origin_id: string }[]) => {
|
|
||||||
const locationIds = locations.map(location => location.origin_id);
|
|
||||||
setLocationIds(locationIds);
|
|
||||||
})
|
|
||||||
.catch(noOp);
|
|
||||||
}, 900, { leading: true, trailing: true }), []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (value === '') {
|
|
||||||
clearResults();
|
|
||||||
}
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<AutosuggestInput
|
<AutosuggestInput
|
||||||
|
@ -87,13 +59,12 @@ const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
suggestions={locationIds}
|
suggestions={locationsQuery.data || []}
|
||||||
onSuggestionsFetchRequested={noOp}
|
onSuggestionsFetchRequested={noOp}
|
||||||
onSuggestionsClearRequested={noOp}
|
onSuggestionsClearRequested={noOp}
|
||||||
onSuggestionSelected={handleSelected}
|
onSuggestionSelected={handleSelected}
|
||||||
searchTokens={[]}
|
searchTokens={[]}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
renderSuggestion={AutosuggestLocation}
|
|
||||||
/>
|
/>
|
||||||
<div role='button' tabIndex={0} className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3 rtl:left-0 rtl:right-auto' onClick={handleClear}>
|
<div role='button' tabIndex={0} className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3 rtl:left-0 rtl:right-auto' onClick={handleClear}>
|
||||||
<Icon src={require('@tabler/icons/outline/search.svg')} className={clsx('size-5 text-gray-600', { 'hidden': !empty })} />
|
<Icon src={require('@tabler/icons/outline/search.svg')} className={clsx('size-5 text-gray-600', { 'hidden': !empty })} />
|
||||||
|
|
|
@ -93,16 +93,8 @@ const EditEvent: React.FC<IEditEvent> = ({ statusId }) => {
|
||||||
setApprovalRequired(target.checked);
|
setApprovalRequired(target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeLocation = (value: string | null) => {
|
const onChangeLocation = (location: Location | null) => {
|
||||||
dispatch((_, getState) => {
|
setLocation(location);
|
||||||
let location = null;
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
location = getState().locations.get(value, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLocation(location);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFiles = (files: FileList) => {
|
const handleFiles = (files: FileList) => {
|
||||||
|
|
|
@ -44,7 +44,9 @@ import AutosuggestAccount from '../../components/autosuggest-account';
|
||||||
import { $createEmojiNode } from '../nodes/emoji-node';
|
import { $createEmojiNode } from '../nodes/emoji-node';
|
||||||
import { $createMentionNode } from '../nodes/mention-node';
|
import { $createMentionNode } from '../nodes/mention-node';
|
||||||
|
|
||||||
import type { AutoSuggestion } from 'pl-fe/components/autosuggest-input';
|
import type { Emoji } from 'pl-fe/features/emoji';
|
||||||
|
|
||||||
|
type AutoSuggestion = string | Emoji;
|
||||||
|
|
||||||
type QueryMatch = {
|
type QueryMatch = {
|
||||||
leadOffset: number;
|
leadOffset: number;
|
||||||
|
|
|
@ -23,7 +23,6 @@ import instance from './instance';
|
||||||
import listAdder from './list-adder';
|
import listAdder from './list-adder';
|
||||||
import listEditor from './list-editor';
|
import listEditor from './list-editor';
|
||||||
import lists from './lists';
|
import lists from './lists';
|
||||||
import locations from './locations';
|
|
||||||
import me from './me';
|
import me from './me';
|
||||||
import meta from './meta';
|
import meta from './meta';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
|
@ -65,7 +64,6 @@ const reducers = {
|
||||||
listAdder,
|
listAdder,
|
||||||
listEditor,
|
listEditor,
|
||||||
lists,
|
lists,
|
||||||
locations,
|
|
||||||
me,
|
me,
|
||||||
meta,
|
meta,
|
||||||
notifications,
|
notifications,
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
import { Location } from 'pl-api';
|
|
||||||
import { AnyAction } from 'redux';
|
|
||||||
|
|
||||||
import { LOCATION_SEARCH_SUCCESS } from 'pl-fe/actions/events';
|
|
||||||
|
|
||||||
type State = ImmutableMap<any, Location>;
|
|
||||||
|
|
||||||
const initialState: State = ImmutableMap();
|
|
||||||
|
|
||||||
const normalizeLocations = (state: State, locations: Array<Location>) =>
|
|
||||||
locations.reduce(
|
|
||||||
(state: State, location: Location) => state.set(location.origin_id, location),
|
|
||||||
state,
|
|
||||||
);
|
|
||||||
|
|
||||||
const locations = (state: State = initialState, action: AnyAction): State => {
|
|
||||||
switch (action.type) {
|
|
||||||
case LOCATION_SEARCH_SUCCESS:
|
|
||||||
return normalizeLocations(state, action.locations);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export { locations as default };
|
|
Loading…
Reference in a new issue