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:
Tom Moor
2021-11-29 06:40:55 -08:00
committed by GitHub
parent 25ccfb5d04
commit 15b1069bcc
1017 changed files with 17410 additions and 54942 deletions

View File

@@ -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 were 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)));

View File

@@ -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}

View File

@@ -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;

View 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;

View File

@@ -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();
}, []);

View File

@@ -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")}
/>
);

View File

@@ -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: "",

View File

@@ -1,3 +1,3 @@
// @flow
import Search from "./Search";
export default Search;