chore: Upgrade flow (#1854)

* wip: upgrade flow

* chore: More sealed props improvements

* Final fixes
This commit is contained in:
Tom Moor 2021-01-29 21:36:09 -08:00 committed by GitHub
parent ce2b246e60
commit 32f0589190
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 191 additions and 96 deletions

View File

@ -1,6 +1,7 @@
{
"javascript.validate.enable": false,
"javascript.format.enable": false,
"typescript.validate.enable": false,
"typescript.format.enable": false,
"editor.formatOnSave": true,
"typescript.format.enable": false
}

View File

@ -3,13 +3,17 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import User from "models/User";
import placeholder from "./placeholder.png";
type Props = {
type Props = {|
src: string,
size: number,
icon?: React.Node,
};
user?: User,
onClick?: () => void,
className?: string,
|};
@observer
class Avatar extends React.Component<Props> {

View File

@ -108,8 +108,8 @@ export const Inner = styled.span`
${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"};
`;
export type Props = {
type?: string,
export type Props = {|
type?: "button" | "submit",
value?: string,
icon?: React.Node,
iconColor?: string,
@ -118,9 +118,21 @@ export type Props = {
innerRef?: React.ElementRef<any>,
disclosure?: boolean,
neutral?: boolean,
danger?: boolean,
primary?: boolean,
disabled?: boolean,
fullwidth?: boolean,
autoFocus?: boolean,
style?: Object,
as?: React.ComponentType<any>,
to?: string,
onClick?: (event: SyntheticEvent<>) => mixed,
borderOnHover?: boolean,
};
"data-on"?: string,
"data-event-category"?: string,
"data-event-action"?: string,
|};
function Button({
type = "text",

View File

@ -4,15 +4,18 @@ import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import HelpText from "components/HelpText";
export type Props = {
export type Props = {|
checked?: boolean,
label?: string,
labelHidden?: boolean,
className?: string,
name?: string,
disabled?: boolean,
onChange: (event: SyntheticInputEvent<HTMLInputElement>) => mixed,
note?: string,
short?: boolean,
small?: boolean,
};
|};
const LabelText = styled.span`
font-weight: 500;

View File

@ -4,13 +4,16 @@ import * as React from "react";
import { MenuItem as BaseMenuItem } from "reakit/Menu";
import styled from "styled-components";
type Props = {
type Props = {|
onClick?: (SyntheticEvent<>) => void | Promise<void>,
children?: React.Node,
selected?: boolean,
disabled?: boolean,
to?: string,
href?: string,
target?: "_blank",
as?: string | React.ComponentType<*>,
};
|};
const MenuItem = ({
onClick,

View File

@ -4,10 +4,15 @@ import * as React from "react";
import Document from "models/Document";
import DocumentListItem from "components/DocumentListItem";
type Props = {
type Props = {|
documents: Document[],
limit?: number,
};
showCollection?: boolean,
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
|};
export default function DocumentList({ limit, documents, ...rest }: Props) {
const items = limit ? documents.splice(0, limit) : documents;

View File

@ -23,14 +23,14 @@ const Modified = styled.span`
font-weight: ${(props) => (props.highlight ? "600" : "400")};
`;
type Props = {
type Props = {|
showCollection?: boolean,
showPublished?: boolean,
showLastViewed?: boolean,
document: Document,
children: React.Node,
to?: string,
};
|};
function DocumentMeta({
showPublished,

View File

@ -16,14 +16,31 @@ const RichMarkdownEditor = React.lazy(() => import("rich-markdown-editor"));
const EMPTY_ARRAY = [];
type Props = {
export type Props = {|
id?: string,
value?: string,
defaultValue?: string,
readOnly?: boolean,
grow?: boolean,
disableEmbeds?: boolean,
ui?: UiStore,
};
autoFocus?: boolean,
template?: boolean,
placeholder?: string,
scrollTo?: string,
readOnlyWriteCheckboxes?: boolean,
onBlur?: (event: SyntheticEvent<>) => any,
onFocus?: (event: SyntheticEvent<>) => any,
onPublish?: (event: SyntheticEvent<>) => any,
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
onCancel?: () => any,
onChange?: (getValue: () => string) => any,
onSearchLink?: (title: string) => any,
onHoverLink?: (event: MouseEvent) => any,
onCreateLink?: (title: string) => Promise<string>,
onImageUploadStart?: () => any,
onImageUploadStop?: () => any,
|};
type PropsWithRef = Props & {
forwardedRef: React.Ref<any>,

View File

@ -2,10 +2,13 @@
import * as React from "react";
import { cdnPath } from "utils/urls";
type Props = {
type Props = {|
alt: string,
src: string,
};
title?: string,
width?: number,
height?: number,
|};
export default function Image({ src, alt, ...rest }: Props) {
return <img src={cdnPath(src)} alt={alt} {...rest} />;

View File

@ -75,8 +75,8 @@ export const LabelText = styled.div`
display: inline-block;
`;
export type Props = {
type?: string,
export type Props = {|
type?: "text" | "email" | "checkbox" | "search",
value?: string,
label?: string,
className?: string,
@ -85,9 +85,18 @@ export type Props = {
short?: boolean,
margin?: string | number,
icon?: React.Node,
name?: string,
minLength?: number,
maxLength?: number,
autoFocus?: boolean,
autoComplete?: boolean | string,
readOnly?: boolean,
required?: boolean,
placeholder?: string,
onChange?: (ev: SyntheticInputEvent<HTMLInputElement>) => mixed,
onFocus?: (ev: SyntheticEvent<>) => void,
onBlur?: (ev: SyntheticEvent<>) => void,
};
|};
@observer
class Input extends React.Component<Props> {

View File

@ -8,13 +8,13 @@ import Editor from "components/Editor";
import HelpText from "components/HelpText";
import { LabelText, Outline } from "components/Input";
type Props = {
type Props = {|
label: string,
minHeight?: number,
maxHeight?: number,
readOnly?: boolean,
ui: UiStore,
};
|};
@observer
class InputRich extends React.Component<Props> {

View File

@ -36,6 +36,8 @@ export type Props = {
className?: string,
labelHidden?: boolean,
options: Option[],
onBlur?: () => void,
onFocus?: () => void,
};
@observer

View File

@ -4,10 +4,10 @@ import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
type Props = {
type Props = {|
label: React.Node | string,
children: React.Node,
};
|};
const Labeled = ({ label, children, ...props }: Props) => (
<Flex column {...props}>

View File

@ -5,10 +5,10 @@ import { randomInteger } from "shared/random";
import { pulsate } from "shared/styles/animations";
import Flex from "components/Flex";
type Props = {
type Props = {|
header?: boolean,
height?: number,
};
|};
class Mask extends React.Component<Props> {
width: number;
@ -23,7 +23,7 @@ class Mask extends React.Component<Props> {
}
render() {
return <Redacted width={this.width} {...this.props} />;
return <Redacted width={this.width} />;
}
}

View File

@ -13,12 +13,12 @@ import Scrollable from "components/Scrollable";
ReactModal.setAppElement("#root");
type Props = {
type Props = {|
children?: React.Node,
isOpen: boolean,
title?: string,
onRequestClose: () => void,
};
|};
const GlobalStyles = createGlobalStyle`
.ReactModal__Overlay {

View File

@ -5,13 +5,18 @@ import Document from "models/Document";
import DocumentListItem from "components/DocumentListItem";
import PaginatedList from "components/PaginatedList";
type Props = {
type Props = {|
documents: Document[],
fetch: (options: ?Object) => Promise<void>,
options?: Object,
heading?: React.Node,
empty?: React.Node,
};
showCollection?: boolean,
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
|};
@observer
class PaginatedDocumentList extends React.Component<Props> {

View File

@ -5,12 +5,13 @@ import styled from "styled-components";
import Flex from "components/Flex";
import TeamLogo from "components/TeamLogo";
type Props = {
type Props = {|
teamName: string,
subheading: React.Node,
showDisclosure?: boolean,
onClick: (event: SyntheticEvent<>) => void,
logoUrl: string,
};
|};
const HeaderBlock = React.forwardRef<Props, any>(
({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => (

View File

@ -3,12 +3,15 @@ import * as React from "react";
import styled from "styled-components";
import { LabelText } from "components/Input";
type Props = {
type Props = {|
width?: number,
height?: number,
label?: string,
checked?: boolean,
disabled?: boolean,
onChange: (event: SyntheticInputEvent<HTMLInputElement>) => mixed,
id?: string,
};
|};
function Switch({ width = 38, height = 20, label, ...props }: Props) {
const component = (

View File

@ -3,14 +3,14 @@ import Tippy from "@tippy.js/react";
import * as React from "react";
import styled from "styled-components";
type Props = {
type Props = {|
tooltip: React.Node,
shortcut?: React.Node,
placement?: "top" | "bottom" | "left" | "right",
children: React.Node,
delay?: number,
className?: string,
};
|};
class Tooltip extends React.Component<Props> {
render() {

View File

@ -27,7 +27,6 @@ function NewDocumentMenu() {
as={Link}
to={newDocumentUrl(collections.orderedData[0].id)}
icon={<PlusIcon />}
small
>
{t("New doc")}
</Button>

View File

@ -11,7 +11,7 @@ export default class BaseModel {
this.store = store;
}
save = async (params: ?Object) => {
save = async (params: Object = {}) => {
this.isSaving = true;
try {

View File

@ -142,7 +142,7 @@ export default class Document extends BaseModel {
};
@action
updateFromJson = (data) => {
updateFromJson = (data: Object) => {
set(this, data);
};
@ -150,7 +150,7 @@ export default class Document extends BaseModel {
return this.store.archive(this);
};
restore = (options) => {
restore = (options: { revisionId?: string, collectionId?: string }) => {
return this.store.restore(this, options);
};
@ -233,7 +233,7 @@ export default class Document extends BaseModel {
};
@action
save = async (options: SaveOptions) => {
save = async (options: SaveOptions = {}) => {
if (this.isSaving) return this;
const isCreating = !this.id;
@ -246,7 +246,9 @@ export default class Document extends BaseModel {
collectionId: this.collectionId,
title: this.title,
text: this.text,
...options,
publish: options.publish,
done: options.done,
autosave: options.autosave,
});
}
@ -257,7 +259,9 @@ export default class Document extends BaseModel {
text: this.text,
templateId: this.templateId,
lastRevision: options.lastRevision,
...options,
publish: options.publish,
done: options.done,
autosave: options.autosave,
});
}

View File

@ -61,7 +61,7 @@ type Props = {
document: Document,
revision: Revision,
readOnly: boolean,
onCreateLink: (title: string) => string,
onCreateLink: (title: string) => Promise<string>,
onSearchLink: (term: string) => any,
theme: Theme,
auth: AuthStore,

View File

@ -9,24 +9,24 @@ import parseTitle from "shared/utils/parseTitle";
import Document from "models/Document";
import ClickablePadding from "components/ClickablePadding";
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor from "components/Editor";
import Editor, { type Props as EditorProps } from "components/Editor";
import Flex from "components/Flex";
import HoverPreview from "components/HoverPreview";
import Star, { AnimatedStar } from "components/Star";
import { isModKey } from "utils/keyboard";
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {
type Props = {|
...EditorProps,
onChangeTitle: (event: SyntheticInputEvent<>) => void,
title: string,
defaultValue: string,
document: Document,
isDraft: boolean,
isShare: boolean,
readOnly?: boolean,
onSave: ({ publish?: boolean, done?: boolean, autosave?: boolean }) => mixed,
grow?: boolean,
onSave: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
innerRef: { current: any },
};
|};
@observer
class DocumentEditor extends React.Component<Props> {
@ -98,6 +98,7 @@ class DocumentEditor extends React.Component<Props> {
isShare,
readOnly,
innerRef,
...rest
} = this.props;
const { emoji } = parseTitle(title);
@ -135,12 +136,12 @@ class DocumentEditor extends React.Component<Props> {
/>
<Editor
ref={innerRef}
autoFocus={title && !this.props.defaultValue}
autoFocus={!!title && !this.props.defaultValue}
placeholder="…the rest is up to you"
onHoverLink={this.handleLinkActive}
scrollTo={window.location.hash}
grow
{...this.props}
{...rest}
/>
{!readOnly && <ClickablePadding onClick={this.focusAtEnd} grow />}
{this.activeLinkEvent && !isShare && readOnly && (

View File

@ -240,7 +240,6 @@ class Header extends React.Component<Props> {
<Button
onClick={this.handleSave}
disabled={savingIsDisabled}
isSaving={isSaving}
neutral={isDraft}
>
{isDraft ? t("Save Draft") : t("Done Editing")}
@ -311,7 +310,6 @@ class Header extends React.Component<Props> {
>
<Button
onClick={this.handlePublish}
title={t("Publish document")}
disabled={publishingIsDisabled}
>
{isPublishing ? `${t("Publishing")}` : t("Publish")}

View File

@ -7,11 +7,11 @@ import Document from "models/Document";
import DocumentMeta from "components/DocumentMeta";
import type { NavigationNode } from "types";
type Props = {
type Props = {|
document: Document | NavigationNode,
anchor?: string,
showCollection?: boolean,
};
|};
const DocumentLink = styled(Link)`
display: block;

View File

@ -43,7 +43,7 @@ class DocumentShare extends React.Component<Props> {
this.isSaving = true;
try {
await share.save({ published: event.target.checked });
await share.save({ published: event.currentTarget.checked });
} catch (err) {
this.props.ui.showToast(err.message, { type: "error" });
} finally {

View File

@ -87,7 +87,6 @@ class AddPeopleToGroup extends React.Component<Props> {
</ButtonLink>
.
</HelpText>
<Input
type="search"
placeholder={`${t("Search by name")}`}

View File

@ -17,10 +17,9 @@ function Zapier() {
</HelpText>
<p>
<Button
as="a"
href="https://zapier.com/apps/outline"
rel="noopener noreferrer"
target="_blank"
onClick={() =>
(window.location.href = "https://zapier.com/apps/outline")
}
>
Open Zapier
</Button>

View File

@ -34,10 +34,10 @@ class UserDelete extends React.Component<Props> {
};
render() {
const { auth, ...rest } = this.props;
const { onRequestClose } = this.props;
return (
<Modal isOpen title="Delete Account" {...rest}>
<Modal isOpen title="Delete Account" onRequestClose={onRequestClose}>
<Flex column>
<form onSubmit={this.handleSubmit}>
<HelpText>

View File

@ -19,11 +19,11 @@ import Subheading from "components/Subheading";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
type Props = {
type Props = {|
user: User,
history: RouterHistory,
onRequestClose: () => void,
};
|};
function UserProfile(props: Props) {
const { t } = useTranslation();

View File

@ -4,6 +4,7 @@ import invariant from "invariant";
import { observable, action, computed, autorun, runInAction } from "mobx";
import { getCookie, setCookie, removeCookie } from "tiny-cookie";
import RootStore from "stores/RootStore";
import Policy from "models/Policy";
import Team from "models/Team";
import User from "models/User";
import env from "env";
@ -13,17 +14,17 @@ import { getCookieDomain } from "utils/domains";
const AUTH_STORE = "AUTH_STORE";
const NO_REDIRECT_PATHS = ["/", "/create", "/home"];
type Service = {
type Service = {|
id: string,
name: string,
authUrl: string,
};
|};
type Config = {
type Config = {|
name?: string,
hostname?: string,
services: Service[],
};
|};
export default class AuthStore {
@observable user: ?User;
@ -88,7 +89,7 @@ export default class AuthStore {
}
}
addPolicies = (policies) => {
addPolicies = (policies: Policy[]) => {
if (policies) {
policies.forEach((policy) => this.rootStore.policies.add(policy));
}

View File

@ -174,7 +174,13 @@ export default class DocumentsStore extends BaseStore<Document> {
return this.drafts().length;
}
drafts = (options = {}): Document[] => {
drafts = (
options: {
...PaginationParams,
dateFilter?: "day" | "week" | "month" | "year",
collectionId?: string,
} = {}
): Document[] => {
let drafts = filter(
orderBy(this.all, "updatedAt", "desc"),
(doc) => !doc.publishedAt
@ -185,7 +191,7 @@ export default class DocumentsStore extends BaseStore<Document> {
drafts,
(draft) =>
new Date(draft.updatedAt) >=
subtractDate(new Date(), options.dateFilter)
subtractDate(new Date(), options.dateFilter || "year")
);
}
@ -245,7 +251,7 @@ export default class DocumentsStore extends BaseStore<Document> {
@action
fetchNamedPage = async (
request: string = "list",
options: ?PaginationParams
options: ?Object
): Promise<?(Document[])> => {
this.isFetching = true;
@ -338,10 +344,9 @@ export default class DocumentsStore extends BaseStore<Document> {
};
@action
searchTitles = async (query: string, options: PaginationParams = {}) => {
searchTitles = async (query: string) => {
const res = await client.get("/documents.search_titles", {
query,
...options,
});
invariant(res && res.data, "Search response should be available");
@ -354,7 +359,15 @@ export default class DocumentsStore extends BaseStore<Document> {
@action
search = async (
query: string,
options: PaginationParams = {}
options: {
offset?: number,
limit?: number,
dateFilter?: "day" | "week" | "month" | "year",
includeArchived?: boolean,
includeDrafts?: boolean,
collectionId?: string,
userId?: string,
}
): Promise<SearchResult[]> => {
const compactedOptions = omitBy(options, (o) => !o);
const res = await client.get("/documents.search", {
@ -601,10 +614,14 @@ export default class DocumentsStore extends BaseStore<Document> {
};
@action
restore = async (document: Document, options = {}) => {
restore = async (
document: Document,
options: { revisionId?: string, collectionId?: string } = {}
) => {
const res = await client.post("/documents.restore", {
id: document.id,
...options,
revisionId: options.revisionId,
collectionId: options.collectionId,
});
runInAction("Document#restore", () => {
invariant(res && res.data, "Data should be available");

View File

@ -182,13 +182,15 @@ class UiStore {
@action
showToast = (
message: string,
options?: {
type?: "warning" | "error" | "info" | "success",
options: {
type: "warning" | "error" | "info" | "success",
timeout?: number,
action?: {
text: string,
onClick: () => void,
},
} = {
type: "info",
}
) => {
if (!message) return;
@ -204,7 +206,14 @@ class UiStore {
const id = v4();
const createdAt = new Date().toISOString();
this.toasts.set(id, { message, createdAt, id, ...options });
this.toasts.set(id, {
id,
message,
createdAt,
type: options.type,
timeout: options.timeout,
action: options.action,
});
this.lastToastId = id;
return id;
};

View File

@ -11,7 +11,7 @@ export type LocationWithState = Location & {
},
};
export type Toast = {
export type Toast = {|
id: string,
createdAt: string,
message: string,
@ -22,7 +22,7 @@ export type Toast = {
text: string,
onClick: () => void,
},
};
|};
export type FetchOptions = {
prefetch?: boolean,
@ -31,12 +31,12 @@ export type FetchOptions = {
force?: boolean,
};
export type NavigationNode = {
export type NavigationNode = {|
id: string,
title: string,
url: string,
children: NavigationNode[],
};
|};
// Pagination response in an API call
export type Pagination = {
@ -46,12 +46,12 @@ export type Pagination = {
};
// Pagination request params
export type PaginationParams = {
export type PaginationParams = {|
limit?: number,
offset?: number,
sort?: string,
direction?: "ASC" | "DESC",
};
|};
export type SearchResult = {
ranking: number,

View File

@ -110,6 +110,7 @@ export default function download(
// $FlowIssue
if (navigator.msSaveBlob) {
// IE10+ : (has Blob, but not a[download] or URL)
// $FlowIssue
return navigator.msSaveBlob(blob, fn);
}

View File

@ -12,7 +12,6 @@
"start": "node ./build/server/index.js",
"dev": "nodemon --exec \"yarn build:server && yarn build:i18n && node build/server/index.js\" -e js --ignore build/ --ignore app/",
"lint": "eslint app server shared",
"flow": "flow",
"deploy": "git push heroku master",
"heroku-postbuild": "yarn build && yarn sequelize:migrate",
"sequelize:create-migration": "sequelize migration:create",
@ -192,7 +191,7 @@
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.1.0",
"fetch-test-server": "^1.1.0",
"flow-bin": "^0.104.0",
"flow-bin": "^0.124.0",
"html-webpack-plugin": "3.2.0",
"i18next-parser": "^3.3.0",
"jest-cli": "^26.0.0",

View File

@ -5181,10 +5181,10 @@ flatted@^2.0.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
flow-bin@^0.104.0:
version "0.104.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.104.0.tgz#ef5b3600dfd36abe191a87d19f66e481bad2e235"
integrity sha512-EZXRRmf7m7ET5Lcnwm/I/T8G3d427Bq34vmO3qIlRcPIYloGuVoqRCwjaeezLRDntHkdciagAKbhJ+NTbDjnkw==
flow-bin@^0.124.0:
version "0.124.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.124.0.tgz#24b2e55874e1e2041f9247f42473b3db2ef32758"
integrity sha512-KEtDJ7CFUjcuhw6N52FTZshDd1krf1fxpp4APSIrwhVm+IrlcKJ+EMXpeXKM1kKNSZ347dYGh8wEvXQl4pHZEA==
flow-typed@^2.6.2:
version "2.6.2"