From acfcd86b9b0498550988d987ad075feab0f17259 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 Date: Mon, 30 Oct 2023 20:22:51 +0000 Subject: [PATCH] Search page select fix (#2201) * Fix search page community searchable select * Fix bug with search page creator select * Add stricter typing to dedup function * Fix modlog searchable selects --- lemmy-translations | 2 +- src/shared/components/modlog.tsx | 64 ++-- src/shared/components/search.tsx | 279 +++++++++--------- src/shared/utils/helpers/dedup-by-property.ts | 22 ++ src/shared/utils/helpers/index.ts | 2 + 5 files changed, 204 insertions(+), 165 deletions(-) create mode 100644 src/shared/utils/helpers/dedup-by-property.ts diff --git a/lemmy-translations b/lemmy-translations index 6fbc8693..6b373bf7 160000 --- a/lemmy-translations +++ b/lemmy-translations @@ -1 +1 @@ -Subproject commit 6fbc86932a03c4d40829ee4a3395259b2a7660e5 +Subproject commit 6b373bf7665ed58a81d8285009ad147248acfd7c diff --git a/src/shared/components/modlog.tsx b/src/shared/components/modlog.tsx index 58bf27ef..e4e8f5fb 100644 --- a/src/shared/components/modlog.tsx +++ b/src/shared/components/modlog.tsx @@ -109,6 +109,7 @@ interface ModlogState { loadingUserSearch: boolean; modSearchOptions: Choice[]; userSearchOptions: Choice[]; + isIsomorphic: boolean; } interface ModlogProps { @@ -617,27 +618,15 @@ async function createNewOptions({ oldOptions: Choice[]; text: string; }) { - const newOptions: Choice[] = []; - - if (id) { - const selectedUser = oldOptions.find( - ({ value }) => value === id.toString(), - ); - - if (selectedUser) { - newOptions.push(selectedUser); - } - } - if (text.length > 0) { - newOptions.push( - ...(await fetchUsers(text)) - .slice(0, Number(fetchLimit)) - .map(personToChoice), - ); + return oldOptions + .filter(choice => parseInt(choice.value, 10) === id) + .concat( + (await fetchUsers(text)).slice(0, fetchLimit).map(personToChoice), + ); + } else { + return oldOptions; } - - return newOptions; } export class Modlog extends Component< @@ -653,6 +642,7 @@ export class Modlog extends Component< loadingUserSearch: false, userSearchOptions: [], modSearchOptions: [], + isIsomorphic: false, }; constructor( @@ -673,6 +663,7 @@ export class Modlog extends Component< ...this.state, res, communityRes, + isIsomorphic: true, }; if (modUserResponse.state === "success") { @@ -692,7 +683,40 @@ export class Modlog extends Component< } async componentDidMount() { - await this.refetch(); + if (!this.state.isIsomorphic) { + const { modId, userId } = getModlogQueryParams(); + const promises = [this.refetch()]; + + if (userId) { + promises.push( + HttpService.client + .getPersonDetails({ person_id: userId }) + .then(res => { + if (res.state === "success") { + this.setState({ + userSearchOptions: [personToChoice(res.data.person_view)], + }); + } + }), + ); + } + + if (modId) { + promises.push( + HttpService.client + .getPersonDetails({ person_id: modId }) + .then(res => { + if (res.state === "success") { + this.setState({ + modSearchOptions: [personToChoice(res.data.person_view)], + }); + } + }), + ); + } + + await Promise.all(promises); + } } get combined() { diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx index 08d70727..46bb5119 100644 --- a/src/shared/components/search.tsx +++ b/src/shared/components/search.tsx @@ -15,6 +15,7 @@ import { restoreScrollPosition, saveScrollPosition } from "@utils/browser"; import { capitalizeFirstLetter, debounce, + dedupByProperty, getIdFromString, getPageFromString, getQueryParams, @@ -33,7 +34,6 @@ import { GetPersonDetails, GetPersonDetailsResponse, GetSiteResponse, - ListCommunities, ListCommunitiesResponse, ListingType, PersonView, @@ -88,9 +88,6 @@ type FilterType = "creator" | "community"; interface SearchState { searchRes: RequestState; resolveObjectRes: RequestState; - creatorDetailsRes: RequestState; - communitiesRes: RequestState; - communityRes: RequestState; siteRes: GetSiteResponse; searchText?: string; communitySearchOptions: Choice[]; @@ -197,7 +194,7 @@ const Filter = ({ label: I18NextService.i18n.t("all"), value: "0", }, - ].concat(options)} + ].concat(dedupByProperty(options, option => option.value))} value={value ?? 0} onSearch={onSearch} onChange={onChange} @@ -245,9 +242,6 @@ export class Search extends Component { state: SearchState = { resolveObjectRes: EMPTY_REQUEST, - creatorDetailsRes: EMPTY_REQUEST, - communitiesRes: EMPTY_REQUEST, - communityRes: EMPTY_REQUEST, siteRes: this.isoData.site_res, creatorSearchOptions: [], communitySearchOptions: [], @@ -269,10 +263,7 @@ export class Search extends Component { const { q } = getSearchQueryParams(); - this.state = { - ...this.state, - searchText: q, - }; + this.state.searchText = q; // Only fetch the data if coming from another route if (FirstLoadService.isFirstLoad) { @@ -284,78 +275,107 @@ export class Search extends Component { searchResponse: searchRes, } = this.isoData.routeData; - this.state = { - ...this.state, - isIsomorphic: true, - }; + this.state.isIsomorphic = true; if (creatorDetailsRes?.state === "success") { - this.state = { - ...this.state, - creatorSearchOptions: - creatorDetailsRes?.state === "success" - ? [personToChoice(creatorDetailsRes.data.person_view)] - : [], - creatorDetailsRes, - }; + this.state.creatorSearchOptions = + creatorDetailsRes.state === "success" + ? [personToChoice(creatorDetailsRes.data.person_view)] + : []; } if (communitiesRes?.state === "success") { - this.state = { - ...this.state, - communitiesRes, - }; + this.state.communitySearchOptions = + communitiesRes.data.communities.map(communityToChoice); } if (communityRes?.state === "success") { - this.state = { - ...this.state, - communityRes, - }; + this.state.communitySearchOptions.unshift( + communityToChoice(communityRes.data.community_view), + ); } - if (q !== "") { - this.state = { - ...this.state, - }; + if (searchRes?.state === "success") { + this.state.searchRes = searchRes; + } - if (searchRes?.state === "success") { - this.state = { - ...this.state, - searchRes, - }; - } - - if (resolveObjectRes?.state === "success") { - this.state = { - ...this.state, - resolveObjectRes, - }; - } + if (resolveObjectRes?.state === "success") { + this.state.resolveObjectRes = resolveObjectRes; } } } async componentDidMount() { if (!this.state.isIsomorphic) { - const promises = [this.fetchCommunities()]; + this.setState({ + searchCommunitiesLoading: true, + searchCreatorLoading: true, + }); + + const promises = [ + HttpService.client + .listCommunities({ + type_: defaultListingType, + sort: defaultSortType, + limit: fetchLimit, + }) + .then(res => { + if (res.state === "success") { + this.setState({ + communitySearchOptions: + res.data.communities.map(communityToChoice), + }); + } + }), + ]; + + const { communityId, creatorId } = getSearchQueryParams(); + + if (communityId) { + promises.push( + HttpService.client.getCommunity({ id: communityId }).then(res => { + if (res.state === "success") { + this.setState(prev => { + prev.communitySearchOptions.unshift( + communityToChoice(res.data.community_view), + ); + + return prev; + }); + } + }), + ); + } + + if (creatorId) { + promises.push( + HttpService.client + .getPersonDetails({ + person_id: creatorId, + }) + .then(res => { + if (res.state === "success") { + this.setState(prev => { + prev.creatorSearchOptions.push( + personToChoice(res.data.person_view), + ); + }); + } + }), + ); + } + if (this.state.searchText) { promises.push(this.search()); } await Promise.all(promises); - } - } - async fetchCommunities() { - this.setState({ communitiesRes: LOADING_REQUEST }); - this.setState({ - communitiesRes: await HttpService.client.listCommunities({ - type_: defaultListingType, - sort: defaultSortType, - limit: fetchLimit, - }), - }); + this.setState({ + searchCommunitiesLoading: false, + searchCreatorLoading: false, + }); + } } componentWillUnmount() { @@ -368,26 +388,20 @@ export class Search extends Component { }: InitialFetchRequest>): Promise { const community_id = getIdFromString(communityId); let communityResponse: RequestState = EMPTY_REQUEST; - let listCommunitiesResponse: RequestState = - EMPTY_REQUEST; if (community_id) { const getCommunityForm: GetCommunity = { id: community_id, }; communityResponse = await client.getCommunity(getCommunityForm); - } else { - const listCommunitiesForm: ListCommunities = { - type_: defaultListingType, - sort: defaultSortType, - limit: fetchLimit, - }; - - listCommunitiesResponse = await client.listCommunities( - listCommunitiesForm, - ); } + const listCommunitiesResponse = await client.listCommunities({ + type_: defaultListingType, + sort: defaultSortType, + limit: fetchLimit, + }); + const creator_id = getIdFromString(creatorId); let creatorDetailsResponse: RequestState = EMPTY_REQUEST; @@ -417,21 +431,19 @@ export class Search extends Component { limit: fetchLimit, }; - if (query !== "") { - searchResponse = await client.search(form); - if (myAuth()) { - const resolveObjectForm: ResolveObject = { - q: query, - }; - resolveObjectResponse = await HttpService.silent_client.resolveObject( - resolveObjectForm, - ); + searchResponse = await client.search(form); + if (myAuth()) { + const resolveObjectForm: ResolveObject = { + q: query, + }; + resolveObjectResponse = await HttpService.silent_client.resolveObject( + resolveObjectForm, + ); - // If we return this object with a state of failed, the catch-all-handler will redirect - // to an error page, so we ignore it by covering up the error with the empty state. - if (resolveObjectResponse.state === "failed") { - resolveObjectResponse = EMPTY_REQUEST; - } + // If we return this object with a state of failed, the catch-all-handler will redirect + // to an error page, so we ignore it by covering up the error with the empty state. + if (resolveObjectResponse.state === "failed") { + resolveObjectResponse = EMPTY_REQUEST; } } } @@ -541,13 +553,8 @@ export class Search extends Component { creatorSearchOptions, searchCommunitiesLoading, searchCreatorLoading, - communitiesRes, } = this.state; - const hasCommunities = - communitiesRes.state === "success" && - communitiesRes.data.communities.length > 0; - return ( <>
@@ -588,16 +595,14 @@ export class Search extends Component {
- {hasCommunities && ( - - )} + { } handleCreatorSearch = debounce(async (text: string) => { - const { creatorId } = getSearchQueryParams(); - const { creatorSearchOptions } = this.state; - const newOptions: Choice[] = []; - - this.setState({ searchCreatorLoading: true }); - - const selectedChoice = creatorSearchOptions.find( - choice => getIdFromString(choice.value) === creatorId, - ); - - if (selectedChoice) { - newOptions.push(selectedChoice); - } - if (text.length > 0) { - newOptions.push(...(await fetchUsers(text)).map(personToChoice)); - } + const { creatorId } = getSearchQueryParams(); + const { creatorSearchOptions } = this.state; - this.setState({ - searchCreatorLoading: false, - creatorSearchOptions: newOptions, - }); + this.setState({ searchCreatorLoading: true }); + + const newOptions = creatorSearchOptions + .filter(choice => getIdFromString(choice.value) === creatorId) + .concat((await fetchUsers(text)).map(personToChoice)); + + this.setState({ + searchCreatorLoading: false, + creatorSearchOptions: newOptions, + }); + } }); handleCommunitySearch = debounce(async (text: string) => { - const { communityId } = getSearchQueryParams(); - const { communitySearchOptions } = this.state; - this.setState({ - searchCommunitiesLoading: true, - }); - - const newOptions: Choice[] = []; - - const selectedChoice = communitySearchOptions.find( - choice => getIdFromString(choice.value) === communityId, - ); - - if (selectedChoice) { - newOptions.push(selectedChoice); - } - if (text.length > 0) { - newOptions.push(...(await fetchCommunities(text)).map(communityToChoice)); - } + const { communityId } = getSearchQueryParams(); + const { communitySearchOptions } = this.state; - this.setState({ - searchCommunitiesLoading: false, - communitySearchOptions: newOptions, - }); + this.setState({ + searchCommunitiesLoading: true, + }); + + const newOptions = communitySearchOptions + .filter(choice => getIdFromString(choice.value) === communityId) + .concat((await fetchCommunities(text)).map(communityToChoice)); + + this.setState({ + searchCommunitiesLoading: false, + communitySearchOptions: newOptions, + }); + } }); handleSortChange(sort: SortType) { diff --git a/src/shared/utils/helpers/dedup-by-property.ts b/src/shared/utils/helpers/dedup-by-property.ts new file mode 100644 index 00000000..7ddb195a --- /dev/null +++ b/src/shared/utils/helpers/dedup-by-property.ts @@ -0,0 +1,22 @@ +function dedupByProperty< + T extends Record, + R extends number | string | boolean, +>(collection: T[], keyFn: (obj: T) => R) { + return collection.reduce( + (acc, cur) => { + const key = keyFn(cur); + if (!acc.foundSet.has(key)) { + acc.output.push(cur); + acc.foundSet.add(key); + } + + return acc; + }, + { + output: [] as T[], + foundSet: new Set(), + }, + ).output; +} + +export default dedupByProperty; diff --git a/src/shared/utils/helpers/index.ts b/src/shared/utils/helpers/index.ts index 3420adbc..c519bfc1 100644 --- a/src/shared/utils/helpers/index.ts +++ b/src/shared/utils/helpers/index.ts @@ -22,6 +22,7 @@ import validEmail from "./valid-email"; import validInstanceTLD from "./valid-instance-tld"; import validTitle from "./valid-title"; import validURL from "./valid-url"; +import dedupByProperty from "./dedup-by-property"; export { capitalizeFirstLetter, @@ -48,4 +49,5 @@ export { validInstanceTLD, validTitle, validURL, + dedupByProperty, };