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
This commit is contained in:
SleeplessOne1917 2023-10-30 20:22:51 +00:00 committed by GitHub
parent c22358e0d2
commit acfcd86b9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 204 additions and 165 deletions

@ -1 +1 @@
Subproject commit 6fbc86932a03c4d40829ee4a3395259b2a7660e5 Subproject commit 6b373bf7665ed58a81d8285009ad147248acfd7c

View file

@ -109,6 +109,7 @@ interface ModlogState {
loadingUserSearch: boolean; loadingUserSearch: boolean;
modSearchOptions: Choice[]; modSearchOptions: Choice[];
userSearchOptions: Choice[]; userSearchOptions: Choice[];
isIsomorphic: boolean;
} }
interface ModlogProps { interface ModlogProps {
@ -617,27 +618,15 @@ async function createNewOptions({
oldOptions: Choice[]; oldOptions: Choice[];
text: string; text: string;
}) { }) {
const newOptions: Choice[] = [];
if (id) {
const selectedUser = oldOptions.find(
({ value }) => value === id.toString(),
);
if (selectedUser) {
newOptions.push(selectedUser);
}
}
if (text.length > 0) { if (text.length > 0) {
newOptions.push( return oldOptions
...(await fetchUsers(text)) .filter(choice => parseInt(choice.value, 10) === id)
.slice(0, Number(fetchLimit)) .concat(
.map<Choice>(personToChoice), (await fetchUsers(text)).slice(0, fetchLimit).map(personToChoice),
); );
} else {
return oldOptions;
} }
return newOptions;
} }
export class Modlog extends Component< export class Modlog extends Component<
@ -653,6 +642,7 @@ export class Modlog extends Component<
loadingUserSearch: false, loadingUserSearch: false,
userSearchOptions: [], userSearchOptions: [],
modSearchOptions: [], modSearchOptions: [],
isIsomorphic: false,
}; };
constructor( constructor(
@ -673,6 +663,7 @@ export class Modlog extends Component<
...this.state, ...this.state,
res, res,
communityRes, communityRes,
isIsomorphic: true,
}; };
if (modUserResponse.state === "success") { if (modUserResponse.state === "success") {
@ -692,7 +683,40 @@ export class Modlog extends Component<
} }
async componentDidMount() { 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() { get combined() {

View file

@ -15,6 +15,7 @@ import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
import { import {
capitalizeFirstLetter, capitalizeFirstLetter,
debounce, debounce,
dedupByProperty,
getIdFromString, getIdFromString,
getPageFromString, getPageFromString,
getQueryParams, getQueryParams,
@ -33,7 +34,6 @@ import {
GetPersonDetails, GetPersonDetails,
GetPersonDetailsResponse, GetPersonDetailsResponse,
GetSiteResponse, GetSiteResponse,
ListCommunities,
ListCommunitiesResponse, ListCommunitiesResponse,
ListingType, ListingType,
PersonView, PersonView,
@ -88,9 +88,6 @@ type FilterType = "creator" | "community";
interface SearchState { interface SearchState {
searchRes: RequestState<SearchResponse>; searchRes: RequestState<SearchResponse>;
resolveObjectRes: RequestState<ResolveObjectResponse>; resolveObjectRes: RequestState<ResolveObjectResponse>;
creatorDetailsRes: RequestState<GetPersonDetailsResponse>;
communitiesRes: RequestState<ListCommunitiesResponse>;
communityRes: RequestState<GetCommunityResponse>;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
searchText?: string; searchText?: string;
communitySearchOptions: Choice[]; communitySearchOptions: Choice[];
@ -197,7 +194,7 @@ const Filter = ({
label: I18NextService.i18n.t("all"), label: I18NextService.i18n.t("all"),
value: "0", value: "0",
}, },
].concat(options)} ].concat(dedupByProperty(options, option => option.value))}
value={value ?? 0} value={value ?? 0}
onSearch={onSearch} onSearch={onSearch}
onChange={onChange} onChange={onChange}
@ -245,9 +242,6 @@ export class Search extends Component<any, SearchState> {
state: SearchState = { state: SearchState = {
resolveObjectRes: EMPTY_REQUEST, resolveObjectRes: EMPTY_REQUEST,
creatorDetailsRes: EMPTY_REQUEST,
communitiesRes: EMPTY_REQUEST,
communityRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
creatorSearchOptions: [], creatorSearchOptions: [],
communitySearchOptions: [], communitySearchOptions: [],
@ -269,10 +263,7 @@ export class Search extends Component<any, SearchState> {
const { q } = getSearchQueryParams(); const { q } = getSearchQueryParams();
this.state = { this.state.searchText = q;
...this.state,
searchText: q,
};
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
@ -284,78 +275,107 @@ export class Search extends Component<any, SearchState> {
searchResponse: searchRes, searchResponse: searchRes,
} = this.isoData.routeData; } = this.isoData.routeData;
this.state = { this.state.isIsomorphic = true;
...this.state,
isIsomorphic: true,
};
if (creatorDetailsRes?.state === "success") { if (creatorDetailsRes?.state === "success") {
this.state = { this.state.creatorSearchOptions =
...this.state, creatorDetailsRes.state === "success"
creatorSearchOptions: ? [personToChoice(creatorDetailsRes.data.person_view)]
creatorDetailsRes?.state === "success" : [];
? [personToChoice(creatorDetailsRes.data.person_view)]
: [],
creatorDetailsRes,
};
} }
if (communitiesRes?.state === "success") { if (communitiesRes?.state === "success") {
this.state = { this.state.communitySearchOptions =
...this.state, communitiesRes.data.communities.map(communityToChoice);
communitiesRes,
};
} }
if (communityRes?.state === "success") { if (communityRes?.state === "success") {
this.state = { this.state.communitySearchOptions.unshift(
...this.state, communityToChoice(communityRes.data.community_view),
communityRes, );
};
} }
if (q !== "") { if (searchRes?.state === "success") {
this.state = { this.state.searchRes = searchRes;
...this.state, }
};
if (searchRes?.state === "success") { if (resolveObjectRes?.state === "success") {
this.state = { this.state.resolveObjectRes = resolveObjectRes;
...this.state,
searchRes,
};
}
if (resolveObjectRes?.state === "success") {
this.state = {
...this.state,
resolveObjectRes,
};
}
} }
} }
} }
async componentDidMount() { async componentDidMount() {
if (!this.state.isIsomorphic) { 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) { if (this.state.searchText) {
promises.push(this.search()); promises.push(this.search());
} }
await Promise.all(promises); await Promise.all(promises);
}
}
async fetchCommunities() { this.setState({
this.setState({ communitiesRes: LOADING_REQUEST }); searchCommunitiesLoading: false,
this.setState({ searchCreatorLoading: false,
communitiesRes: await HttpService.client.listCommunities({ });
type_: defaultListingType, }
sort: defaultSortType,
limit: fetchLimit,
}),
});
} }
componentWillUnmount() { componentWillUnmount() {
@ -368,26 +388,20 @@ export class Search extends Component<any, SearchState> {
}: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> { }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> {
const community_id = getIdFromString(communityId); const community_id = getIdFromString(communityId);
let communityResponse: RequestState<GetCommunityResponse> = EMPTY_REQUEST; let communityResponse: RequestState<GetCommunityResponse> = EMPTY_REQUEST;
let listCommunitiesResponse: RequestState<ListCommunitiesResponse> =
EMPTY_REQUEST;
if (community_id) { if (community_id) {
const getCommunityForm: GetCommunity = { const getCommunityForm: GetCommunity = {
id: community_id, id: community_id,
}; };
communityResponse = await client.getCommunity(getCommunityForm); 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); const creator_id = getIdFromString(creatorId);
let creatorDetailsResponse: RequestState<GetPersonDetailsResponse> = let creatorDetailsResponse: RequestState<GetPersonDetailsResponse> =
EMPTY_REQUEST; EMPTY_REQUEST;
@ -417,21 +431,19 @@ export class Search extends Component<any, SearchState> {
limit: fetchLimit, limit: fetchLimit,
}; };
if (query !== "") { searchResponse = await client.search(form);
searchResponse = await client.search(form); if (myAuth()) {
if (myAuth()) { const resolveObjectForm: ResolveObject = {
const resolveObjectForm: ResolveObject = { q: query,
q: query, };
}; resolveObjectResponse = await HttpService.silent_client.resolveObject(
resolveObjectResponse = await HttpService.silent_client.resolveObject( resolveObjectForm,
resolveObjectForm, );
);
// If we return this object with a state of failed, the catch-all-handler will redirect // 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. // to an error page, so we ignore it by covering up the error with the empty state.
if (resolveObjectResponse.state === "failed") { if (resolveObjectResponse.state === "failed") {
resolveObjectResponse = EMPTY_REQUEST; resolveObjectResponse = EMPTY_REQUEST;
}
} }
} }
} }
@ -541,13 +553,8 @@ export class Search extends Component<any, SearchState> {
creatorSearchOptions, creatorSearchOptions,
searchCommunitiesLoading, searchCommunitiesLoading,
searchCreatorLoading, searchCreatorLoading,
communitiesRes,
} = this.state; } = this.state;
const hasCommunities =
communitiesRes.state === "success" &&
communitiesRes.data.communities.length > 0;
return ( return (
<> <>
<div className="row row-cols-auto g-2 g-sm-3 mb-2 mb-sm-3"> <div className="row row-cols-auto g-2 g-sm-3 mb-2 mb-sm-3">
@ -588,16 +595,14 @@ export class Search extends Component<any, SearchState> {
</div> </div>
</div> </div>
<div className="row gy-2 gx-4 mb-3"> <div className="row gy-2 gx-4 mb-3">
{hasCommunities && ( <Filter
<Filter filterType="community"
filterType="community" onChange={this.handleCommunityFilterChange}
onChange={this.handleCommunityFilterChange} onSearch={this.handleCommunitySearch}
onSearch={this.handleCommunitySearch} options={communitySearchOptions}
options={communitySearchOptions} value={communityId}
value={communityId} loading={searchCommunitiesLoading}
loading={searchCommunitiesLoading} />
/>
)}
<Filter <Filter
filterType="creator" filterType="creator"
onChange={this.handleCreatorFilterChange} onChange={this.handleCreatorFilterChange}
@ -976,55 +981,41 @@ export class Search extends Component<any, SearchState> {
} }
handleCreatorSearch = debounce(async (text: string) => { 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) { if (text.length > 0) {
newOptions.push(...(await fetchUsers(text)).map(personToChoice)); const { creatorId } = getSearchQueryParams();
} const { creatorSearchOptions } = this.state;
this.setState({ this.setState({ searchCreatorLoading: true });
searchCreatorLoading: false,
creatorSearchOptions: newOptions, 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) => { 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) { if (text.length > 0) {
newOptions.push(...(await fetchCommunities(text)).map(communityToChoice)); const { communityId } = getSearchQueryParams();
} const { communitySearchOptions } = this.state;
this.setState({ this.setState({
searchCommunitiesLoading: false, searchCommunitiesLoading: true,
communitySearchOptions: newOptions, });
});
const newOptions = communitySearchOptions
.filter(choice => getIdFromString(choice.value) === communityId)
.concat((await fetchCommunities(text)).map(communityToChoice));
this.setState({
searchCommunitiesLoading: false,
communitySearchOptions: newOptions,
});
}
}); });
handleSortChange(sort: SortType) { handleSortChange(sort: SortType) {

View file

@ -0,0 +1,22 @@
function dedupByProperty<
T extends Record<string, any>,
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<R>(),
},
).output;
}
export default dedupByProperty;

View file

@ -22,6 +22,7 @@ import validEmail from "./valid-email";
import validInstanceTLD from "./valid-instance-tld"; import validInstanceTLD from "./valid-instance-tld";
import validTitle from "./valid-title"; import validTitle from "./valid-title";
import validURL from "./valid-url"; import validURL from "./valid-url";
import dedupByProperty from "./dedup-by-property";
export { export {
capitalizeFirstLetter, capitalizeFirstLetter,
@ -48,4 +49,5 @@ export {
validInstanceTLD, validInstanceTLD,
validTitle, validTitle,
validURL, validURL,
dedupByProperty,
}; };