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.validate.enable": false,
"javascript.format.enable": false,
"typescript.validate.enable": false, "typescript.validate.enable": false,
"typescript.format.enable": false,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"typescript.format.enable": false
} }

View File

@ -3,13 +3,17 @@ import { observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import User from "models/User";
import placeholder from "./placeholder.png"; import placeholder from "./placeholder.png";
type Props = { type Props = {|
src: string, src: string,
size: number, size: number,
icon?: React.Node, icon?: React.Node,
}; user?: User,
onClick?: () => void,
className?: string,
|};
@observer @observer
class Avatar extends React.Component<Props> { 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;"}; ${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"};
`; `;
export type Props = { export type Props = {|
type?: string, type?: "button" | "submit",
value?: string, value?: string,
icon?: React.Node, icon?: React.Node,
iconColor?: string, iconColor?: string,
@ -118,9 +118,21 @@ export type Props = {
innerRef?: React.ElementRef<any>, innerRef?: React.ElementRef<any>,
disclosure?: boolean, disclosure?: boolean,
neutral?: boolean, neutral?: boolean,
danger?: boolean,
primary?: boolean,
disabled?: boolean,
fullwidth?: boolean, fullwidth?: boolean,
autoFocus?: boolean,
style?: Object,
as?: React.ComponentType<any>,
to?: string,
onClick?: (event: SyntheticEvent<>) => mixed,
borderOnHover?: boolean, borderOnHover?: boolean,
};
"data-on"?: string,
"data-event-category"?: string,
"data-event-action"?: string,
|};
function Button({ function Button({
type = "text", type = "text",

View File

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

View File

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

View File

@ -4,10 +4,15 @@ import * as React from "react";
import Document from "models/Document"; import Document from "models/Document";
import DocumentListItem from "components/DocumentListItem"; import DocumentListItem from "components/DocumentListItem";
type Props = { type Props = {|
documents: Document[], documents: Document[],
limit?: number, limit?: number,
}; showCollection?: boolean,
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
|};
export default function DocumentList({ limit, documents, ...rest }: Props) { export default function DocumentList({ limit, documents, ...rest }: Props) {
const items = limit ? documents.splice(0, limit) : documents; 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")}; font-weight: ${(props) => (props.highlight ? "600" : "400")};
`; `;
type Props = { type Props = {|
showCollection?: boolean, showCollection?: boolean,
showPublished?: boolean, showPublished?: boolean,
showLastViewed?: boolean, showLastViewed?: boolean,
document: Document, document: Document,
children: React.Node, children: React.Node,
to?: string, to?: string,
}; |};
function DocumentMeta({ function DocumentMeta({
showPublished, showPublished,

View File

@ -16,14 +16,31 @@ const RichMarkdownEditor = React.lazy(() => import("rich-markdown-editor"));
const EMPTY_ARRAY = []; const EMPTY_ARRAY = [];
type Props = { export type Props = {|
id?: string, id?: string,
value?: string,
defaultValue?: string, defaultValue?: string,
readOnly?: boolean, readOnly?: boolean,
grow?: boolean, grow?: boolean,
disableEmbeds?: boolean, disableEmbeds?: boolean,
ui?: UiStore, 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 & { type PropsWithRef = Props & {
forwardedRef: React.Ref<any>, forwardedRef: React.Ref<any>,

View File

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

View File

@ -75,8 +75,8 @@ export const LabelText = styled.div`
display: inline-block; display: inline-block;
`; `;
export type Props = { export type Props = {|
type?: string, type?: "text" | "email" | "checkbox" | "search",
value?: string, value?: string,
label?: string, label?: string,
className?: string, className?: string,
@ -85,9 +85,18 @@ export type Props = {
short?: boolean, short?: boolean,
margin?: string | number, margin?: string | number,
icon?: React.Node, 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, onFocus?: (ev: SyntheticEvent<>) => void,
onBlur?: (ev: SyntheticEvent<>) => void, onBlur?: (ev: SyntheticEvent<>) => void,
}; |};
@observer @observer
class Input extends React.Component<Props> { class Input extends React.Component<Props> {

View File

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

View File

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

View File

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

View File

@ -5,10 +5,10 @@ import { randomInteger } from "shared/random";
import { pulsate } from "shared/styles/animations"; import { pulsate } from "shared/styles/animations";
import Flex from "components/Flex"; import Flex from "components/Flex";
type Props = { type Props = {|
header?: boolean, header?: boolean,
height?: number, height?: number,
}; |};
class Mask extends React.Component<Props> { class Mask extends React.Component<Props> {
width: number; width: number;
@ -23,7 +23,7 @@ class Mask extends React.Component<Props> {
} }
render() { 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"); ReactModal.setAppElement("#root");
type Props = { type Props = {|
children?: React.Node, children?: React.Node,
isOpen: boolean, isOpen: boolean,
title?: string, title?: string,
onRequestClose: () => void, onRequestClose: () => void,
}; |};
const GlobalStyles = createGlobalStyle` const GlobalStyles = createGlobalStyle`
.ReactModal__Overlay { .ReactModal__Overlay {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import invariant from "invariant";
import { observable, action, computed, autorun, runInAction } from "mobx"; import { observable, action, computed, autorun, runInAction } from "mobx";
import { getCookie, setCookie, removeCookie } from "tiny-cookie"; import { getCookie, setCookie, removeCookie } from "tiny-cookie";
import RootStore from "stores/RootStore"; import RootStore from "stores/RootStore";
import Policy from "models/Policy";
import Team from "models/Team"; import Team from "models/Team";
import User from "models/User"; import User from "models/User";
import env from "env"; import env from "env";
@ -13,17 +14,17 @@ import { getCookieDomain } from "utils/domains";
const AUTH_STORE = "AUTH_STORE"; const AUTH_STORE = "AUTH_STORE";
const NO_REDIRECT_PATHS = ["/", "/create", "/home"]; const NO_REDIRECT_PATHS = ["/", "/create", "/home"];
type Service = { type Service = {|
id: string, id: string,
name: string, name: string,
authUrl: string, authUrl: string,
}; |};
type Config = { type Config = {|
name?: string, name?: string,
hostname?: string, hostname?: string,
services: Service[], services: Service[],
}; |};
export default class AuthStore { export default class AuthStore {
@observable user: ?User; @observable user: ?User;
@ -88,7 +89,7 @@ export default class AuthStore {
} }
} }
addPolicies = (policies) => { addPolicies = (policies: Policy[]) => {
if (policies) { if (policies) {
policies.forEach((policy) => this.rootStore.policies.add(policy)); 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; return this.drafts().length;
} }
drafts = (options = {}): Document[] => { drafts = (
options: {
...PaginationParams,
dateFilter?: "day" | "week" | "month" | "year",
collectionId?: string,
} = {}
): Document[] => {
let drafts = filter( let drafts = filter(
orderBy(this.all, "updatedAt", "desc"), orderBy(this.all, "updatedAt", "desc"),
(doc) => !doc.publishedAt (doc) => !doc.publishedAt
@ -185,7 +191,7 @@ export default class DocumentsStore extends BaseStore<Document> {
drafts, drafts,
(draft) => (draft) =>
new Date(draft.updatedAt) >= 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 @action
fetchNamedPage = async ( fetchNamedPage = async (
request: string = "list", request: string = "list",
options: ?PaginationParams options: ?Object
): Promise<?(Document[])> => { ): Promise<?(Document[])> => {
this.isFetching = true; this.isFetching = true;
@ -338,10 +344,9 @@ export default class DocumentsStore extends BaseStore<Document> {
}; };
@action @action
searchTitles = async (query: string, options: PaginationParams = {}) => { searchTitles = async (query: string) => {
const res = await client.get("/documents.search_titles", { const res = await client.get("/documents.search_titles", {
query, query,
...options,
}); });
invariant(res && res.data, "Search response should be available"); invariant(res && res.data, "Search response should be available");
@ -354,7 +359,15 @@ export default class DocumentsStore extends BaseStore<Document> {
@action @action
search = async ( search = async (
query: string, query: string,
options: PaginationParams = {} options: {
offset?: number,
limit?: number,
dateFilter?: "day" | "week" | "month" | "year",
includeArchived?: boolean,
includeDrafts?: boolean,
collectionId?: string,
userId?: string,
}
): Promise<SearchResult[]> => { ): Promise<SearchResult[]> => {
const compactedOptions = omitBy(options, (o) => !o); const compactedOptions = omitBy(options, (o) => !o);
const res = await client.get("/documents.search", { const res = await client.get("/documents.search", {
@ -601,10 +614,14 @@ export default class DocumentsStore extends BaseStore<Document> {
}; };
@action @action
restore = async (document: Document, options = {}) => { restore = async (
document: Document,
options: { revisionId?: string, collectionId?: string } = {}
) => {
const res = await client.post("/documents.restore", { const res = await client.post("/documents.restore", {
id: document.id, id: document.id,
...options, revisionId: options.revisionId,
collectionId: options.collectionId,
}); });
runInAction("Document#restore", () => { runInAction("Document#restore", () => {
invariant(res && res.data, "Data should be available"); invariant(res && res.data, "Data should be available");

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@
"start": "node ./build/server/index.js", "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/", "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", "lint": "eslint app server shared",
"flow": "flow",
"deploy": "git push heroku master", "deploy": "git push heroku master",
"heroku-postbuild": "yarn build && yarn sequelize:migrate", "heroku-postbuild": "yarn build && yarn sequelize:migrate",
"sequelize:create-migration": "sequelize migration:create", "sequelize:create-migration": "sequelize migration:create",
@ -192,7 +191,7 @@
"eslint-plugin-react": "^7.20.0", "eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.1.0", "eslint-plugin-react-hooks": "^4.1.0",
"fetch-test-server": "^1.1.0", "fetch-test-server": "^1.1.0",
"flow-bin": "^0.104.0", "flow-bin": "^0.124.0",
"html-webpack-plugin": "3.2.0", "html-webpack-plugin": "3.2.0",
"i18next-parser": "^3.3.0", "i18next-parser": "^3.3.0",
"jest-cli": "^26.0.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" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
flow-bin@^0.104.0: flow-bin@^0.124.0:
version "0.104.0" version "0.124.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.104.0.tgz#ef5b3600dfd36abe191a87d19f66e481bad2e235" resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.124.0.tgz#24b2e55874e1e2041f9247f42473b3db2ef32758"
integrity sha512-EZXRRmf7m7ET5Lcnwm/I/T8G3d427Bq34vmO3qIlRcPIYloGuVoqRCwjaeezLRDntHkdciagAKbhJ+NTbDjnkw== integrity sha512-KEtDJ7CFUjcuhw6N52FTZshDd1krf1fxpp4APSIrwhVm+IrlcKJ+EMXpeXKM1kKNSZ347dYGh8wEvXQl4pHZEA==
flow-typed@^2.6.2: flow-typed@^2.6.2:
version "2.6.2" version "2.6.2"