mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-29 07:41:13 +00:00
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:
parent
c22358e0d2
commit
acfcd86b9b
5 changed files with 204 additions and 165 deletions
|
@ -1 +1 @@
|
||||||
Subproject commit 6fbc86932a03c4d40829ee4a3395259b2a7660e5
|
Subproject commit 6b373bf7665ed58a81d8285009ad147248acfd7c
|
|
@ -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() {
|
||||||
|
|
|
@ -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,79 +275,108 @@ 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:
|
|
||||||
creatorDetailsRes?.state === "success"
|
|
||||||
? [personToChoice(creatorDetailsRes.data.person_view)]
|
? [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 !== "") {
|
|
||||||
this.state = {
|
|
||||||
...this.state,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (searchRes?.state === "success") {
|
if (searchRes?.state === "success") {
|
||||||
this.state = {
|
this.state.searchRes = searchRes;
|
||||||
...this.state,
|
|
||||||
searchRes,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolveObjectRes?.state === "success") {
|
if (resolveObjectRes?.state === "success") {
|
||||||
this.state = {
|
this.state.resolveObjectRes = resolveObjectRes;
|
||||||
...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({ communitiesRes: LOADING_REQUEST });
|
|
||||||
this.setState({
|
this.setState({
|
||||||
communitiesRes: await HttpService.client.listCommunities({
|
searchCommunitiesLoading: false,
|
||||||
type_: defaultListingType,
|
searchCreatorLoading: false,
|
||||||
sort: defaultSortType,
|
|
||||||
limit: fetchLimit,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
saveScrollPosition(this.context);
|
saveScrollPosition(this.context);
|
||||||
|
@ -368,25 +388,19 @@ 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 = {
|
|
||||||
|
const listCommunitiesResponse = await client.listCommunities({
|
||||||
type_: defaultListingType,
|
type_: defaultListingType,
|
||||||
sort: defaultSortType,
|
sort: defaultSortType,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
};
|
});
|
||||||
|
|
||||||
listCommunitiesResponse = await client.listCommunities(
|
|
||||||
listCommunitiesForm,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const creator_id = getIdFromString(creatorId);
|
const creator_id = getIdFromString(creatorId);
|
||||||
let creatorDetailsResponse: RequestState<GetPersonDetailsResponse> =
|
let creatorDetailsResponse: RequestState<GetPersonDetailsResponse> =
|
||||||
|
@ -417,7 +431,6 @@ 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 = {
|
||||||
|
@ -434,7 +447,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
communityResponse,
|
communityResponse,
|
||||||
|
@ -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,7 +595,6 @@ 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}
|
||||||
|
@ -597,7 +603,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
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) => {
|
||||||
|
if (text.length > 0) {
|
||||||
const { creatorId } = getSearchQueryParams();
|
const { creatorId } = getSearchQueryParams();
|
||||||
const { creatorSearchOptions } = this.state;
|
const { creatorSearchOptions } = this.state;
|
||||||
const newOptions: Choice[] = [];
|
|
||||||
|
|
||||||
this.setState({ searchCreatorLoading: true });
|
this.setState({ searchCreatorLoading: true });
|
||||||
|
|
||||||
const selectedChoice = creatorSearchOptions.find(
|
const newOptions = creatorSearchOptions
|
||||||
choice => getIdFromString(choice.value) === creatorId,
|
.filter(choice => getIdFromString(choice.value) === creatorId)
|
||||||
);
|
.concat((await fetchUsers(text)).map(personToChoice));
|
||||||
|
|
||||||
if (selectedChoice) {
|
|
||||||
newOptions.push(selectedChoice);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text.length > 0) {
|
|
||||||
newOptions.push(...(await fetchUsers(text)).map(personToChoice));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
searchCreatorLoading: false,
|
searchCreatorLoading: false,
|
||||||
creatorSearchOptions: newOptions,
|
creatorSearchOptions: newOptions,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
handleCommunitySearch = debounce(async (text: string) => {
|
handleCommunitySearch = debounce(async (text: string) => {
|
||||||
|
if (text.length > 0) {
|
||||||
const { communityId } = getSearchQueryParams();
|
const { communityId } = getSearchQueryParams();
|
||||||
const { communitySearchOptions } = this.state;
|
const { communitySearchOptions } = this.state;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
searchCommunitiesLoading: true,
|
searchCommunitiesLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newOptions: Choice[] = [];
|
const newOptions = communitySearchOptions
|
||||||
|
.filter(choice => getIdFromString(choice.value) === communityId)
|
||||||
const selectedChoice = communitySearchOptions.find(
|
.concat((await fetchCommunities(text)).map(communityToChoice));
|
||||||
choice => getIdFromString(choice.value) === communityId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedChoice) {
|
|
||||||
newOptions.push(selectedChoice);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text.length > 0) {
|
|
||||||
newOptions.push(...(await fetchCommunities(text)).map(communityToChoice));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
searchCommunitiesLoading: false,
|
searchCommunitiesLoading: false,
|
||||||
communitySearchOptions: newOptions,
|
communitySearchOptions: newOptions,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSortChange(sort: SortType) {
|
handleSortChange(sort: SortType) {
|
||||||
|
|
22
src/shared/utils/helpers/dedup-by-property.ts
Normal file
22
src/shared/utils/helpers/dedup-by-property.ts
Normal 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;
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue