chore: Move to Typescript (#2783)
This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously. closes #1282
This commit is contained in:
@@ -1,69 +1,74 @@
|
||||
// @flow
|
||||
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
|
||||
import { isEqual } from "lodash";
|
||||
import { observable, action } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import queryString from "query-string";
|
||||
import * as React from "react";
|
||||
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
||||
import { withRouter, Link } from "react-router-dom";
|
||||
import type { RouterHistory, Match } from "react-router-dom";
|
||||
import { WithTranslation, withTranslation, Trans } from "react-i18next";
|
||||
import { RouteComponentProps, StaticContext, withRouter } from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
import UsersStore from "stores/UsersStore";
|
||||
|
||||
import Button from "components/Button";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import DocumentListItem from "components/DocumentListItem";
|
||||
import Empty from "components/Empty";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import LoadingIndicator from "components/LoadingIndicator";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import RegisterKeyDown from "components/RegisterKeyDown";
|
||||
import { DateFilter as TDateFilter } from "@shared/types";
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "~/stores/BaseStore";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import Button from "~/components/Button";
|
||||
import CenteredContent from "~/components/CenteredContent";
|
||||
import DocumentListItem from "~/components/DocumentListItem";
|
||||
import Empty from "~/components/Empty";
|
||||
import Fade from "~/components/Fade";
|
||||
import Flex from "~/components/Flex";
|
||||
import HelpText from "~/components/HelpText";
|
||||
import LoadingIndicator from "~/components/LoadingIndicator";
|
||||
import PageTitle from "~/components/PageTitle";
|
||||
import RegisterKeyDown from "~/components/RegisterKeyDown";
|
||||
import withStores from "~/components/withStores";
|
||||
import NewDocumentMenu from "~/menus/NewDocumentMenu";
|
||||
import { newDocumentPath, searchUrl } from "~/utils/routeHelpers";
|
||||
import { decodeURIComponentSafe } from "~/utils/urls";
|
||||
import CollectionFilter from "./components/CollectionFilter";
|
||||
import DateFilter from "./components/DateFilter";
|
||||
import SearchInput from "./components/SearchInput";
|
||||
import StatusFilter from "./components/StatusFilter";
|
||||
import UserFilter from "./components/UserFilter";
|
||||
import NewDocumentMenu from "menus/NewDocumentMenu";
|
||||
import { type LocationWithState } from "types";
|
||||
import { newDocumentPath, searchUrl } from "utils/routeHelpers";
|
||||
import { decodeURIComponentSafe } from "utils/urls";
|
||||
|
||||
type Props = {
|
||||
history: RouterHistory,
|
||||
match: Match,
|
||||
location: LocationWithState,
|
||||
documents: DocumentsStore,
|
||||
auth: AuthStore,
|
||||
users: UsersStore,
|
||||
policies: PoliciesStore,
|
||||
notFound: ?boolean,
|
||||
t: TFunction,
|
||||
};
|
||||
type Props = RouteComponentProps<
|
||||
{ term: string },
|
||||
StaticContext,
|
||||
{ search: string; fromMenu?: boolean }
|
||||
> &
|
||||
WithTranslation &
|
||||
RootStore & {
|
||||
notFound?: boolean;
|
||||
};
|
||||
|
||||
@observer
|
||||
class Search extends React.Component<Props> {
|
||||
firstDocument: ?React.Component<any>;
|
||||
lastQuery: string = "";
|
||||
lastParams: Object;
|
||||
firstDocument: React.Component<any> | null | undefined;
|
||||
|
||||
lastQuery = "";
|
||||
|
||||
lastParams: Record<string, any>;
|
||||
|
||||
@observable
|
||||
query: string = decodeURIComponentSafe(this.props.match.params.term || "");
|
||||
@observable params: URLSearchParams = new URLSearchParams();
|
||||
@observable offset: number = 0;
|
||||
@observable allowLoadMore: boolean = true;
|
||||
@observable isLoading: boolean = false;
|
||||
@observable pinToTop: boolean = !!this.props.match.params.term;
|
||||
|
||||
@observable
|
||||
params: URLSearchParams = new URLSearchParams();
|
||||
|
||||
@observable
|
||||
offset = 0;
|
||||
|
||||
@observable
|
||||
allowLoadMore = true;
|
||||
|
||||
@observable
|
||||
isLoading = false;
|
||||
|
||||
@observable
|
||||
pinToTop = !!this.props.match.params.term;
|
||||
|
||||
componentDidMount() {
|
||||
this.handleTermChange();
|
||||
@@ -77,6 +82,7 @@ class Search extends React.Component<Props> {
|
||||
if (prevProps.location.search !== this.props.location.search) {
|
||||
this.handleQueryChange();
|
||||
}
|
||||
|
||||
if (prevProps.match.params.term !== this.props.match.params.term) {
|
||||
this.handleTermChange();
|
||||
}
|
||||
@@ -86,7 +92,7 @@ class Search extends React.Component<Props> {
|
||||
this.props.history.goBack();
|
||||
};
|
||||
|
||||
handleKeyDown = (ev: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
||||
handleKeyDown = (ev: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (ev.key === "Enter") {
|
||||
this.updateLocation(ev.currentTarget.value);
|
||||
this.fetchResults();
|
||||
@@ -100,6 +106,7 @@ class Search extends React.Component<Props> {
|
||||
|
||||
if (ev.key === "ArrowDown") {
|
||||
ev.preventDefault();
|
||||
|
||||
if (this.firstDocument) {
|
||||
if (this.firstDocument instanceof HTMLElement) {
|
||||
this.firstDocument.focus();
|
||||
@@ -112,10 +119,8 @@ class Search extends React.Component<Props> {
|
||||
this.params = new URLSearchParams(this.props.location.search);
|
||||
this.offset = 0;
|
||||
this.allowLoadMore = true;
|
||||
|
||||
// To prevent "no results" showing before debounce kicks in
|
||||
this.isLoading = true;
|
||||
|
||||
this.fetchResults();
|
||||
};
|
||||
|
||||
@@ -124,27 +129,24 @@ class Search extends React.Component<Props> {
|
||||
this.query = query ? query : "";
|
||||
this.offset = 0;
|
||||
this.allowLoadMore = true;
|
||||
|
||||
// To prevent "no results" showing before debounce kicks in
|
||||
this.isLoading = true;
|
||||
|
||||
this.fetchResults();
|
||||
};
|
||||
|
||||
handleFilterChange = (search: {
|
||||
collectionId?: ?string,
|
||||
userId?: ?string,
|
||||
dateFilter?: ?string,
|
||||
includeArchived?: ?string,
|
||||
collectionId?: string | undefined;
|
||||
userId?: string | undefined;
|
||||
dateFilter?: TDateFilter;
|
||||
includeArchived?: boolean | undefined;
|
||||
}) => {
|
||||
this.props.history.replace({
|
||||
pathname: this.props.location.pathname,
|
||||
search: queryString.stringify(
|
||||
{ ...queryString.parse(this.props.location.search), ...search },
|
||||
{
|
||||
...queryString.parse(this.props.location.search),
|
||||
...search,
|
||||
},
|
||||
{ skipEmptyString: true }
|
||||
skipEmptyString: true,
|
||||
}
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -171,7 +173,7 @@ class Search extends React.Component<Props> {
|
||||
|
||||
get dateFilter() {
|
||||
const id = this.params.get("dateFilter");
|
||||
return id ? id : undefined;
|
||||
return id ? (id as TDateFilter) : undefined;
|
||||
}
|
||||
|
||||
get isFiltered() {
|
||||
@@ -194,7 +196,6 @@ class Search extends React.Component<Props> {
|
||||
loadMoreResults = async () => {
|
||||
// Don't paginate if there aren't more results or we’re in the middle of fetching
|
||||
if (!this.allowLoadMore || this.isLoading) return;
|
||||
|
||||
// Fetch more results
|
||||
await this.fetchResults();
|
||||
};
|
||||
@@ -224,7 +225,6 @@ class Search extends React.Component<Props> {
|
||||
|
||||
try {
|
||||
const results = await this.props.documents.search(this.query, params);
|
||||
|
||||
this.pinToTop = true;
|
||||
|
||||
if (results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT) {
|
||||
@@ -260,13 +260,12 @@ class Search extends React.Component<Props> {
|
||||
const { documents, notFound, location, t, auth, policies } = this.props;
|
||||
const results = documents.searchResults(this.query);
|
||||
const showEmpty = !this.isLoading && this.query && results.length === 0;
|
||||
const showShortcutTip =
|
||||
!this.pinToTop && location.state && location.state.fromMenu;
|
||||
const showShortcutTip = !this.pinToTop && location.state?.fromMenu;
|
||||
const can = policies.abilities(auth.team?.id ? auth.team.id : "");
|
||||
const canCollection = policies.abilities(this.collectionId || "");
|
||||
|
||||
return (
|
||||
<Container auto>
|
||||
<Container>
|
||||
<PageTitle title={this.title} />
|
||||
<RegisterKeyDown trigger="Escape" handler={this.goBack} />
|
||||
{this.isLoading && <LoadingIndicator />}
|
||||
@@ -289,8 +288,12 @@ class Search extends React.Component<Props> {
|
||||
<HelpText small>
|
||||
<Trans
|
||||
defaults="Use the <em>{{ shortcut }}</em> shortcut to search from anywhere in your knowledge base"
|
||||
values={{ shortcut: "/" }}
|
||||
components={{ em: <strong /> }}
|
||||
values={{
|
||||
shortcut: "/",
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</HelpText>
|
||||
</Fade>
|
||||
@@ -300,23 +303,33 @@ class Search extends React.Component<Props> {
|
||||
<StatusFilter
|
||||
includeArchived={this.includeArchived}
|
||||
onSelect={(includeArchived) =>
|
||||
this.handleFilterChange({ includeArchived })
|
||||
this.handleFilterChange({
|
||||
includeArchived,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<CollectionFilter
|
||||
collectionId={this.collectionId}
|
||||
onSelect={(collectionId) =>
|
||||
this.handleFilterChange({ collectionId })
|
||||
this.handleFilterChange({
|
||||
collectionId,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<UserFilter
|
||||
userId={this.userId}
|
||||
onSelect={(userId) => this.handleFilterChange({ userId })}
|
||||
onSelect={(userId) =>
|
||||
this.handleFilterChange({
|
||||
userId,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<DateFilter
|
||||
dateFilter={this.dateFilter}
|
||||
onSelect={(dateFilter) =>
|
||||
this.handleFilterChange({ dateFilter })
|
||||
this.handleFilterChange({
|
||||
dateFilter,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Filters>
|
||||
@@ -360,7 +373,6 @@ class Search extends React.Component<Props> {
|
||||
{results.map((result, index) => {
|
||||
const document = documents.data.get(result.document.id);
|
||||
if (!document) return null;
|
||||
|
||||
return (
|
||||
<DocumentListItem
|
||||
ref={(ref) => index === 0 && this.setFirstDocumentRef(ref)}
|
||||
@@ -403,18 +415,18 @@ const Container = styled(CenteredContent)`
|
||||
}
|
||||
`;
|
||||
|
||||
const ResultsWrapper = styled(Flex)`
|
||||
const ResultsWrapper = styled(Flex)<{ pinToTop: boolean }>`
|
||||
position: absolute;
|
||||
transition: all 300ms cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
top: ${(props) => (props.pinToTop ? "0%" : "50%")};
|
||||
width: 100%;
|
||||
|
||||
${breakpoint("tablet")`
|
||||
margin-top: ${(props) => (props.pinToTop ? "40px" : "-75px")};
|
||||
margin-top: ${(props: any) => (props.pinToTop ? "40px" : "-75px")};
|
||||
`};
|
||||
`;
|
||||
|
||||
const ResultList = styled(Flex)`
|
||||
const ResultList = styled(Flex)<{ visible: boolean }>`
|
||||
margin-bottom: 150px;
|
||||
opacity: ${(props) => (props.visible ? "1" : "0")};
|
||||
transition: all 400ms cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
@@ -443,6 +455,4 @@ const Filters = styled(Flex)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default withTranslation()<Search>(
|
||||
withRouter(inject("documents", "auth", "policies")(Search))
|
||||
);
|
||||
export default withTranslation()(withStores(withRouter(Search)));
|
||||
@@ -1,26 +1,23 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FilterOptions from "components/FilterOptions";
|
||||
import useStores from "hooks/useStores";
|
||||
import FilterOptions from "~/components/FilterOptions";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
collectionId: ?string,
|
||||
onSelect: (key: ?string) => void,
|
||||
|};
|
||||
type Props = {
|
||||
collectionId: string | undefined;
|
||||
onSelect: (key: string | undefined) => void;
|
||||
};
|
||||
|
||||
function CollectionFilter(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { collections } = useStores();
|
||||
const { onSelect, collectionId } = props;
|
||||
|
||||
const options = React.useMemo(() => {
|
||||
const collectionOptions = collections.orderedData.map((user) => ({
|
||||
key: user.id,
|
||||
label: user.name,
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
key: "",
|
||||
@@ -29,7 +26,6 @@ function CollectionFilter(props: Props) {
|
||||
...collectionOptions,
|
||||
];
|
||||
}, [collections.orderedData, t]);
|
||||
|
||||
return (
|
||||
<FilterOptions
|
||||
options={options}
|
||||
@@ -1,35 +0,0 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FilterOptions from "components/FilterOptions";
|
||||
|
||||
type Props = {|
|
||||
dateFilter: ?string,
|
||||
onSelect: (key: ?string) => void,
|
||||
|};
|
||||
|
||||
const DateFilter = ({ dateFilter, onSelect }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = React.useMemo(
|
||||
() => [
|
||||
{ key: "", label: t("Any time") },
|
||||
{ key: "day", label: t("Past day") },
|
||||
{ key: "week", label: t("Past week") },
|
||||
{ key: "month", label: t("Past month") },
|
||||
{ key: "year", label: t("Past year") },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterOptions
|
||||
options={options}
|
||||
activeKey={dateFilter}
|
||||
onSelect={onSelect}
|
||||
defaultLabel={t("Any time")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateFilter;
|
||||
49
app/scenes/Search/components/DateFilter.tsx
Normal file
49
app/scenes/Search/components/DateFilter.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DateFilter as TDateFilter } from "@shared/types";
|
||||
import FilterOptions from "~/components/FilterOptions";
|
||||
|
||||
type Props = {
|
||||
dateFilter: string | null | undefined;
|
||||
onSelect: (key: TDateFilter) => void;
|
||||
};
|
||||
|
||||
const DateFilter = ({ dateFilter, onSelect }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const options = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "",
|
||||
label: t("Any time"),
|
||||
},
|
||||
{
|
||||
key: "day",
|
||||
label: t("Past day"),
|
||||
},
|
||||
{
|
||||
key: "week",
|
||||
label: t("Past week"),
|
||||
},
|
||||
{
|
||||
key: "month",
|
||||
label: t("Past month"),
|
||||
},
|
||||
{
|
||||
key: "year",
|
||||
label: t("Past year"),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterOptions
|
||||
options={options}
|
||||
activeKey={dateFilter}
|
||||
onSelect={onSelect}
|
||||
defaultLabel={t("Any time")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateFilter;
|
||||
@@ -1,17 +1,16 @@
|
||||
// @flow
|
||||
import { SearchIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
type Props = {
|
||||
defaultValue?: string,
|
||||
placeholder?: string,
|
||||
type Props = React.HTMLAttributes<HTMLInputElement> & {
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
function SearchInput({ defaultValue, ...rest }: Props) {
|
||||
const theme = useTheme();
|
||||
const inputRef = React.useRef();
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
// ensure that focus is placed at end of input
|
||||
@@ -19,7 +18,7 @@ function SearchInput({ defaultValue, ...rest }: Props) {
|
||||
inputRef.current?.setSelectionRange(len, len);
|
||||
}, [defaultValue]);
|
||||
|
||||
const focusInput = React.useCallback((ev: SyntheticEvent<>) => {
|
||||
const focusInput = React.useCallback(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FilterOptions from "components/FilterOptions";
|
||||
import FilterOptions from "~/components/FilterOptions";
|
||||
|
||||
type Props = {|
|
||||
includeArchived?: boolean,
|
||||
onSelect: (key: ?string) => void,
|
||||
|};
|
||||
type Props = {
|
||||
includeArchived?: boolean;
|
||||
onSelect: (key: boolean) => void;
|
||||
};
|
||||
|
||||
const StatusFilter = ({ includeArchived, onSelect }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -31,7 +29,7 @@ const StatusFilter = ({ includeArchived, onSelect }: Props) => {
|
||||
<FilterOptions
|
||||
options={options}
|
||||
activeKey={includeArchived ? "true" : undefined}
|
||||
onSelect={onSelect}
|
||||
onSelect={(key) => onSelect(key === "true")}
|
||||
defaultLabel={t("Active documents")}
|
||||
/>
|
||||
);
|
||||
@@ -1,14 +1,13 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FilterOptions from "components/FilterOptions";
|
||||
import useStores from "hooks/useStores";
|
||||
import FilterOptions from "~/components/FilterOptions";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
userId: ?string,
|
||||
onSelect: (key: ?string) => void,
|
||||
|};
|
||||
type Props = {
|
||||
userId: string | undefined;
|
||||
onSelect: (key: string | undefined) => void;
|
||||
};
|
||||
|
||||
function UserFilter(props: Props) {
|
||||
const { onSelect, userId } = props;
|
||||
@@ -16,7 +15,9 @@ function UserFilter(props: Props) {
|
||||
const { users } = useStores();
|
||||
|
||||
React.useEffect(() => {
|
||||
users.fetchPage({ limit: 100 });
|
||||
users.fetchPage({
|
||||
limit: 100,
|
||||
});
|
||||
}, [users]);
|
||||
|
||||
const options = React.useMemo(() => {
|
||||
@@ -24,7 +25,6 @@ function UserFilter(props: Props) {
|
||||
key: user.id,
|
||||
label: user.name,
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
key: "",
|
||||
@@ -1,3 +1,3 @@
|
||||
// @flow
|
||||
import Search from "./Search";
|
||||
|
||||
export default Search;
|
||||
Reference in New Issue
Block a user