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,27 +1,28 @@
// @flow
import { observer } from "mobx-react";
import { ArchiveIcon } from "outline-icons";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import SidebarLink from "./SidebarLink";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
import { archivePath } from "utils/routeHelpers";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import { archivePath } from "~/utils/routeHelpers";
import SidebarLink, { DragObject } from "./SidebarLink";
function ArchiveLink({ documents }) {
const { policies } = useStores();
function ArchiveLink() {
const { policies, documents } = useStores();
const { t } = useTranslation();
const { showToast } = useToasts();
const [{ isDocumentDropping }, dropToArchiveDocument] = useDrop({
accept: "document",
drop: async (item, monitor) => {
drop: async (item: DragObject) => {
const document = documents.get(item.id);
await document.archive();
showToast(t("Document archived"), { type: "success" });
await document?.archive();
showToast(t("Document archived"), {
type: "success",
});
},
canDrop: (item, monitor) => policies.abilities(item.id).archive,
canDrop: (item) => policies.abilities(item.id).archive,
collect: (monitor) => ({
isDocumentDropping: monitor.isOver(),
}),

View File

@@ -1,4 +1,3 @@
// @flow
import fractionalIndex from "fractional-index";
import { observer } from "mobx-react";
import * as React from "react";
@@ -6,30 +5,31 @@ import { useDrop, useDrag } from "react-dnd";
import { useTranslation } from "react-i18next";
import { useLocation, useHistory } from "react-router-dom";
import styled from "styled-components";
import Collection from "models/Collection";
import Document from "models/Document";
import DocumentReparent from "scenes/DocumentReparent";
import CollectionIcon from "components/CollectionIcon";
import Modal from "components/Modal";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import DocumentReparent from "~/scenes/DocumentReparent";
import CollectionIcon from "~/components/CollectionIcon";
import Modal from "~/components/Modal";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
import CollectionMenu from "~/menus/CollectionMenu";
import CollectionSortMenu from "~/menus/CollectionSortMenu";
import { NavigationNode } from "~/types";
import DocumentLink from "./DocumentLink";
import DropCursor from "./DropCursor";
import DropToImport from "./DropToImport";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
import useBoolean from "hooks/useBoolean";
import useStores from "hooks/useStores";
import CollectionMenu from "menus/CollectionMenu";
import CollectionSortMenu from "menus/CollectionSortMenu";
import SidebarLink, { DragObject } from "./SidebarLink";
type Props = {|
collection: Collection,
canUpdate: boolean,
activeDocument: ?Document,
prefetchDocument: (id: string) => Promise<void>,
belowCollection: Collection | void,
isDraggingAnyCollection: boolean,
onChangeDragging: (dragging: boolean) => void,
|};
type Props = {
collection: Collection;
canUpdate: boolean;
activeDocument: Document | null | undefined;
prefetchDocument: (id: string) => Promise<any>;
belowCollection: Collection | void;
isDraggingAnyCollection: boolean;
onChangeDragging: (dragging: boolean) => void;
};
function CollectionLink({
collection,
@@ -49,18 +49,21 @@ function CollectionLink({
handlePermissionOpen,
handlePermissionClose,
] = useBoolean();
const itemRef = React.useRef();
const itemRef = React.useRef<
NavigationNode & { depth: number; active: boolean; collectionId: string }
>();
const handleTitleChange = React.useCallback(
async (name: string) => {
await collection.save({ name });
await collection.save({
name,
});
history.push(collection.url);
},
[collection, history]
);
const { ui, documents, policies, collections } = useStores();
const [expanded, setExpanded] = React.useState(
collection.id === ui.activeCollectionId
);
@@ -71,13 +74,13 @@ function CollectionLink({
if (search === "?starred") {
return;
}
if (isDraggingAnyCollection) {
setExpanded(false);
} else {
setExpanded(collection.id === ui.activeCollectionId);
}
}, [isDraggingAnyCollection, collection.id, ui.activeCollectionId, search]);
const manualSort = collection.sort.field === "index";
const can = policies.abilities(collection.id);
const belowCollectionIndex = belowCollection ? belowCollection.index : null;
@@ -85,7 +88,7 @@ function CollectionLink({
// Drop to re-parent
const [{ isOver, canDrop }, drop] = useDrop({
accept: "document",
drop: (item, monitor) => {
drop: (item: DragObject, monitor) => {
const { id, collectionId } = item;
if (monitor.didDrop()) return;
if (!collection) return;
@@ -103,11 +106,13 @@ function CollectionLink({
documents.move(id, collection.id);
}
},
canDrop: (item, monitor) => {
canDrop: () => {
return policies.abilities(collection.id).update;
},
collect: (monitor) => ({
isOver: !!monitor.isOver({ shallow: true }),
isOver: !!monitor.isOver({
shallow: true,
}),
canDrop: monitor.canDrop(),
}),
});
@@ -115,7 +120,7 @@ function CollectionLink({
// Drop to reorder
const [{ isOverReorder }, dropToReorder] = useDrop({
accept: "document",
drop: async (item, monitor) => {
drop: async (item: DragObject) => {
if (!collection) return;
documents.move(item.id, collection.id, undefined, 0);
},
@@ -127,13 +132,13 @@ function CollectionLink({
// Drop to reorder Collection
const [{ isCollectionDropping }, dropToReorderCollection] = useDrop({
accept: "collection",
drop: async (item, monitor) => {
drop: async (item: DragObject) => {
collections.move(
item.id,
fractionalIndex(collection.index, belowCollectionIndex)
);
},
canDrop: (item, monitor) => {
canDrop: (item) => {
return (
collection.id !== item.id &&
(!belowCollection || item.id !== belowCollection.id)
@@ -156,17 +161,22 @@ function CollectionLink({
collect: (monitor) => ({
isCollectionDragging: monitor.isDragging(),
}),
canDrag: (monitor) => {
canDrag: () => {
return can.move;
},
end: (monitor) => {
end: () => {
onChangeDragging(false);
},
});
return (
<>
<div ref={drop} style={{ position: "relative" }}>
<div
ref={drop}
style={{
position: "relative",
}}
>
<Draggable
key={collection.id}
ref={dragToReorderCollection}
@@ -238,18 +248,20 @@ function CollectionLink({
onRequestClose={handlePermissionClose}
isOpen={permissionOpen}
>
<DocumentReparent
item={itemRef.current}
collection={collection}
onSubmit={handlePermissionClose}
onCancel={handlePermissionClose}
/>
{itemRef.current && (
<DocumentReparent
item={itemRef.current}
collection={collection}
onSubmit={handlePermissionClose}
onCancel={handlePermissionClose}
/>
)}
</Modal>
</>
);
}
const Draggable = styled("div")`
const Draggable = styled("div")<{ $isDragging: boolean; $isMoving: boolean }>`
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
pointer-events: ${(props) => (props.$isMoving ? "none" : "auto")};
`;

View File

@@ -1,4 +1,3 @@
// @flow
import fractionalIndex from "fractional-index";
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
@@ -6,24 +5,25 @@ import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Fade from "components/Fade";
import Flex from "components/Flex";
import useStores from "../../../hooks/useStores";
import Collection from "~/models/Collection";
import Fade from "~/components/Fade";
import Flex from "~/components/Flex";
import { createCollection } from "~/actions/definitions/collections";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import CollectionLink from "./CollectionLink";
import DropCursor from "./DropCursor";
import PlaceholderCollections from "./PlaceholderCollections";
import SidebarAction from "./SidebarAction";
import SidebarLink from "./SidebarLink";
import { createCollection } from "actions/definitions/collections";
import useToasts from "hooks/useToasts";
import SidebarLink, { DragObject } from "./SidebarLink";
function Collections() {
const [isFetching, setFetching] = React.useState(false);
const [fetchError, setFetchError] = React.useState();
const { ui, policies, documents, collections } = useStores();
const { policies, documents, collections } = useStores();
const { showToast } = useToasts();
const [expanded, setExpanded] = React.useState(true);
const isPreloaded: boolean = !!collections.orderedData.length;
const isPreloaded = !!collections.orderedData.length;
const { t } = useTranslation();
const orderedCollections = collections.orderedData;
const [isDraggingAnyCollection, setIsDraggingAnyCollection] = React.useState(
@@ -35,7 +35,9 @@ function Collections() {
if (!collections.isLoaded && !isFetching && !fetchError) {
try {
setFetching(true);
await collections.fetchPage({ limit: 100 });
await collections.fetchPage({
limit: 100,
});
} catch (error) {
showToast(
t("Collections could not be loaded, please reload the app"),
@@ -49,18 +51,19 @@ function Collections() {
}
}
}
load();
}, [collections, isFetching, showToast, fetchError, t]);
const [{ isCollectionDropping }, dropToReorderCollection] = useDrop({
accept: "collection",
drop: async (item, monitor) => {
drop: async (item: DragObject) => {
collections.move(
item.id,
fractionalIndex(null, orderedCollections[0].index)
);
},
canDrop: (item, monitor) => {
canDrop: (item) => {
return item.id !== orderedCollections[0].id;
},
collect: (monitor) => ({
@@ -75,14 +78,13 @@ function Collections() {
innerRef={dropToReorderCollection}
from="collections"
/>
{orderedCollections.map((collection, index) => (
{orderedCollections.map((collection: Collection, index: number) => (
<CollectionLink
key={collection.id}
collection={collection}
activeDocument={documents.active}
prefetchDocument={documents.prefetchDocument}
canUpdate={policies.abilities(collection.id).update}
ui={ui}
isDraggingAnyCollection={isDraggingAnyCollection}
onChangeDragging={setIsDraggingAnyCollection}
belowCollection={orderedCollections[index + 1]}

View File

@@ -1,4 +1,3 @@
// @flow
import { CollapsedIcon } from "outline-icons";
import styled from "styled-components";

View File

@@ -1,33 +1,32 @@
// @flow
import { observer } from "mobx-react";
import * as React from "react";
import { useDrag, useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { MAX_TITLE_LENGTH } from "shared/constants";
import Collection from "models/Collection";
import Document from "models/Document";
import Fade from "components/Fade";
import { MAX_TITLE_LENGTH } from "@shared/constants";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import Fade from "~/components/Fade";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import { NavigationNode } from "~/types";
import Disclosure from "./Disclosure";
import DropCursor from "./DropCursor";
import DropToImport from "./DropToImport";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
import useBoolean from "hooks/useBoolean";
import useStores from "hooks/useStores";
import DocumentMenu from "menus/DocumentMenu";
import { type NavigationNode } from "types";
import SidebarLink, { DragObject } from "./SidebarLink";
type Props = {|
node: NavigationNode,
canUpdate: boolean,
collection?: Collection,
activeDocument: ?Document,
prefetchDocument: (documentId: string) => Promise<void>,
depth: number,
index: number,
parentId?: string,
|};
type Props = {
node: NavigationNode;
canUpdate: boolean;
collection?: Collection;
activeDocument: Document | null | undefined;
prefetchDocument: (documentId: string) => Promise<any>;
depth: number;
index: number;
parentId?: string;
};
function DocumentLink(
{
@@ -40,14 +39,12 @@ function DocumentLink(
index,
parentId,
}: Props,
ref
ref: React.RefObject<HTMLAnchorElement>
) {
const { documents, policies } = useStores();
const { t } = useTranslation();
const isActiveDocument = activeDocument && activeDocument.id === node.id;
const hasChildDocuments = !!node.children.length;
const document = documents.get(node.id);
const { fetchChildDocuments } = documents;
@@ -75,7 +72,6 @@ function DocumentLink(
isActiveDocument)
);
}, [hasChildDocuments, activeDocument, isActiveDocument, node, collection]);
const [expanded, setExpanded] = React.useState(showChildren);
React.useEffect(() => {
@@ -93,7 +89,7 @@ function DocumentLink(
}, [expanded, hasChildDocuments]);
const handleDisclosureClick = React.useCallback(
(ev: SyntheticEvent<>) => {
(ev: React.SyntheticEvent) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded(!expanded);
@@ -101,27 +97,26 @@ function DocumentLink(
[expanded]
);
const handleMouseEnter = React.useCallback(
(ev: SyntheticEvent<>) => {
prefetchDocument(node.id);
},
[prefetchDocument, node]
);
const handleMouseEnter = React.useCallback(() => {
prefetchDocument(node.id);
}, [prefetchDocument, node]);
const handleTitleChange = React.useCallback(
async (title: string) => {
if (!document) return;
await documents.update({
id: document.id,
lastRevision: document.revision,
text: document.text,
title,
});
await documents.update(
{
id: document.id,
text: document.text,
title,
},
{
lastRevision: document.revision,
}
);
},
[documents, document]
);
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const isMoving = documents.movingDocumentId === node.id;
const manualSort = collection?.sort.field === "index";
@@ -138,7 +133,7 @@ function DocumentLink(
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
canDrag: (monitor) => {
canDrag: () => {
return (
policies.abilities(node.id).move ||
policies.abilities(node.id).archive ||
@@ -147,50 +142,56 @@ function DocumentLink(
},
});
const hoverExpanding = React.useRef(null);
const hoverExpanding = React.useRef<ReturnType<typeof setTimeout>>();
// We set a timeout when the user first starts hovering over the document link,
// to trigger expansion of children. Clear this timeout when they stop hovering.
const resetHoverExpanding = React.useCallback(() => {
if (hoverExpanding.current) {
clearTimeout(hoverExpanding.current);
hoverExpanding.current = null;
hoverExpanding.current = undefined;
}
}, []);
// Drop to re-parent
const [{ isOverReparent, canDropToReparent }, dropToReparent] = useDrop({
accept: "document",
drop: (item, monitor) => {
drop: (item: DragObject, monitor) => {
if (monitor.didDrop()) return;
if (!collection) return;
documents.move(item.id, collection.id, node.id);
},
canDrop: (item, monitor) =>
pathToNode && !pathToNode.includes(monitor.getItem().id),
canDrop: (_item, monitor) =>
!!pathToNode && !pathToNode.includes(monitor.getItem<DragObject>().id),
hover: (item, monitor) => {
// Enables expansion of document children when hovering over the document
// for more than half a second.
if (
hasChildDocuments &&
monitor.canDrop() &&
monitor.isOver({ shallow: true })
monitor.isOver({
shallow: true,
})
) {
if (!hoverExpanding.current) {
hoverExpanding.current = setTimeout(() => {
hoverExpanding.current = null;
if (monitor.isOver({ shallow: true })) {
hoverExpanding.current = undefined;
if (
monitor.isOver({
shallow: true,
})
) {
setExpanded(true);
}
}, 500);
}
}
},
collect: (monitor) => ({
isOverReparent: !!monitor.isOver({ shallow: true }),
isOverReparent: !!monitor.isOver({
shallow: true,
}),
canDropToReparent: monitor.canDrop(),
}),
});
@@ -198,7 +199,7 @@ function DocumentLink(
// Drop to reorder
const [{ isOverReorder }, dropToReorder] = useDrop({
accept: "document",
drop: (item, monitor) => {
drop: (item: DragObject) => {
if (!collection) return;
if (item.id === node.id) return;
@@ -229,7 +230,9 @@ function DocumentLink(
onMouseEnter={handleMouseEnter}
to={{
pathname: node.url,
state: { title: node.title },
state: {
title: node.title,
},
}}
label={
<>
@@ -247,6 +250,7 @@ function DocumentLink(
/>
</>
}
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'match' implicitly has an 'any' type.
isActive={(match, location) =>
match && location.search !== "?starred"
}
@@ -300,7 +304,7 @@ const Relative = styled.div`
position: relative;
`;
const Draggable = styled.div`
const Draggable = styled.div<{ $isDragging?: boolean; $isMoving?: boolean }>`
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
`;

View File

@@ -1,24 +1,20 @@
// @flow
import * as React from "react";
import styled, { withTheme } from "styled-components";
import { type Theme } from "types";
import styled from "styled-components";
function DropCursor({
isActiveDrop,
innerRef,
theme,
from,
}: {
isActiveDrop: boolean,
innerRef: React.Ref<any>,
theme: Theme,
from: string,
isActiveDrop: boolean;
innerRef: React.Ref<HTMLDivElement>;
from?: string;
}) {
return <Cursor isOver={isActiveDrop} ref={innerRef} from={from} />;
}
// transparent hover zone with a thin visible band vertically centered
const Cursor = styled("div")`
const Cursor = styled.div<{ isOver?: boolean; from?: string }>`
opacity: ${(props) => (props.isOver ? 1 : 0)};
transition: opacity 150ms;
@@ -41,4 +37,4 @@ const Cursor = styled("div")`
}
`;
export default withTheme(DropCursor);
export default DropCursor;

View File

@@ -1,20 +1,21 @@
// @flow
import invariant from "invariant";
import { observer } from "mobx-react";
import * as React from "react";
import Dropzone from "react-dropzone";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import LoadingIndicator from "components/LoadingIndicator";
import useImportDocument from "hooks/useImportDocument";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
import LoadingIndicator from "~/components/LoadingIndicator";
import useImportDocument from "~/hooks/useImportDocument";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
type Props = {|
children: React.Node,
collectionId: string,
documentId?: string,
disabled: boolean,
|};
type Props = {
children: JSX.Element;
collectionId?: string;
documentId?: string;
disabled?: boolean;
activeClassName?: string;
};
function DropToImport({ disabled, children, collectionId, documentId }: Props) {
const { t } = useTranslation();
@@ -24,13 +25,16 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) {
collectionId,
documentId
);
const targetId = collectionId || documentId;
invariant(targetId, "Must provide either collectionId or documentId");
const can = policies.abilities(collectionId);
const can = policies.abilities(targetId);
const handleRejection = React.useCallback(() => {
showToast(
t("Document not supported try Markdown, Plain text, HTML, or Word"),
{ type: "error" }
{
type: "error",
}
);
}, [t, showToast]);
@@ -46,17 +50,11 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) {
noClick
multiple
>
{({
getRootProps,
getInputProps,
isDragActive,
isDragAccept,
isDragReject,
}) => (
{({ getRootProps, getInputProps, isDragActive }) => (
<DropzoneContainer
{...getRootProps()}
$isDragActive={isDragActive}
tabIndex="-1"
tabIndex={-1}
>
<input {...getInputProps()} />
{isImporting && <LoadingIndicator />}
@@ -67,7 +65,7 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) {
);
}
const DropzoneContainer = styled.div`
const DropzoneContainer = styled.div<{ $isDragActive: boolean }>`
border-radius: 4px;
${({ $isDragActive, theme }) =>

View File

@@ -1,14 +1,13 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import useToasts from "hooks/useToasts";
import useToasts from "~/hooks/useToasts";
type Props = {|
onSubmit: (title: string) => Promise<void>,
title: string,
canUpdate: boolean,
maxLength?: number,
|};
type Props = {
onSubmit: (title: string) => Promise<void>;
title: string;
canUpdate: boolean;
maxLength?: number;
};
function EditableTitle({ title, onSubmit, canUpdate, ...rest }: Props) {
const [isEditing, setIsEditing] = React.useState(false);
@@ -43,10 +42,9 @@ function EditableTitle({ title, onSubmit, canUpdate, ...rest }: Props) {
const handleSave = React.useCallback(
async (ev) => {
ev.preventDefault();
setIsEditing(false);
const trimmedValue = value.trim();
if (trimmedValue === originalValue || trimmedValue.length === 0) {
setValue(originalValue);
return;

View File

@@ -1,6 +1,5 @@
// @flow
import styled from "styled-components";
import Flex from "components/Flex";
import Flex from "~/components/Flex";
const Header = styled(Flex)`
font-size: 11px;

View File

@@ -1,44 +1,43 @@
// @flow
// ref: https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/NavLink.js
// This file is pulled almost 100% from react-router with the addition of one
// thing, automatic scroll to the active link. It's worth the copy paste because
// it avoids recalculating the link match again.
import { createLocation } from "history";
import { Location, createLocation } from "history";
import * as React from "react";
import {
__RouterContext as RouterContext,
matchPath,
type Location,
} from "react-router";
import { __RouterContext as RouterContext, matchPath } from "react-router";
import { Link } from "react-router-dom";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
const resolveToLocation = (to, currentLocation) =>
typeof to === "function" ? to(currentLocation) : to;
const resolveToLocation = (
to: string | Record<string, any>,
currentLocation: Location
) => (typeof to === "function" ? to(currentLocation) : to);
const normalizeToLocation = (to, currentLocation) => {
const normalizeToLocation = (
to: string | Record<string, any>,
currentLocation: Location
) => {
return typeof to === "string"
? createLocation(to, null, null, currentLocation)
? createLocation(to, null, undefined, currentLocation)
: to;
};
const joinClassnames = (...classnames) => {
const joinClassnames = (...classnames: (string | undefined)[]) => {
return classnames.filter((i) => i).join(" ");
};
export type Props = {|
activeClassName?: String,
activeStyle?: Object,
className?: string,
scrollIntoViewIfNeeded?: boolean,
exact?: boolean,
isActive?: any,
location?: Location,
strict?: boolean,
style?: Object,
to: string,
|};
export type Props = React.HTMLAttributes<HTMLAnchorElement> & {
activeClassName?: string;
activeStyle?: React.CSSProperties;
className?: string;
scrollIntoViewIfNeeded?: boolean;
exact?: boolean;
isActive?: any;
location?: Location;
strict?: boolean;
style?: React.CSSProperties;
to: string | Record<string, any>;
};
/**
* A <Link> wrapper that knows if it's "active" or not.
@@ -57,7 +56,7 @@ const NavLink = ({
to,
...rest
}: Props) => {
const linkRef = React.useRef();
const linkRef = React.useRef(null);
const context = React.useContext(RouterContext);
const currentLocation = locationProp || context.location;
const toLocation = normalizeToLocation(
@@ -65,9 +64,9 @@ const NavLink = ({
currentLocation
);
const { pathname: path } = toLocation;
// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
const escapedPath = path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
const match = escapedPath
? matchPath(currentLocation.pathname, {
path: escapedPath,
@@ -78,7 +77,6 @@ const NavLink = ({
const isActive = !!(isActiveProp
? isActiveProp(match, currentLocation)
: match);
const className = isActive
? joinClassnames(classNameProp, activeClassName)
: classNameProp;
@@ -88,13 +86,13 @@ const NavLink = ({
if (isActive && linkRef.current && scrollIntoViewIfNeeded !== false) {
scrollIntoView(linkRef.current, {
scrollMode: "if-needed",
behavior: "instant",
behavior: "auto",
});
}
}, [linkRef, scrollIntoViewIfNeeded, isActive]);
const props = {
"aria-current": (isActive && ariaCurrent) || null,
"aria-current": (isActive && ariaCurrent) || undefined,
className,
style,
to: toLocation,

View File

@@ -1,7 +1,6 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import PlaceholderText from "components/PlaceholderText";
import PlaceholderText from "~/components/PlaceholderText";
function PlaceholderCollections() {
return (

View File

@@ -1,4 +1,3 @@
// @flow
import styled from "styled-components";
const ResizeBorder = styled.div`

View File

@@ -1,6 +1,5 @@
// @flow
import styled from "styled-components";
import Flex from "components/Flex";
import Flex from "~/components/Flex";
const Section = styled(Flex)`
position: relative;

View File

@@ -1,23 +1,22 @@
// @flow
import invariant from "invariant";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";
import { actionToMenuItem } from "~/actions";
import useStores from "~/hooks/useStores";
import { Action } from "~/types";
import SidebarLink from "./SidebarLink";
import { actionToMenuItem } from "actions";
import useStores from "hooks/useStores";
import type { Action } from "types";
type Props = {|
action: Action,
|};
type Props = {
action: Action;
depth?: number;
};
function SidebarAction({ action, ...rest }: Props) {
const stores = useStores();
const { t } = useTranslation();
const location = useLocation();
const context = {
isContextMenu: false,
isCommandBar: false,
@@ -27,9 +26,8 @@ function SidebarAction({ action, ...rest }: Props) {
stores,
t,
};
const menuItem = actionToMenuItem(action, context);
invariant(menuItem.onClick, "passed action must have perform");
invariant(menuItem.type === "button", "passed action must be a button");
return (
<SidebarLink

View File

@@ -1,28 +1,32 @@
// @flow
import { transparentize } from "polished";
import * as React from "react";
import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import EventBoundary from "components/EventBoundary";
import NavLink, { type Props as NavLinkProps } from "./NavLink";
import EventBoundary from "~/components/EventBoundary";
import { NavigationNode } from "~/types";
import NavLink, { Props as NavLinkProps } from "./NavLink";
type Props = {|
...NavLinkProps,
to?: string | Object,
href?: string | Object,
innerRef?: (?HTMLElement) => void,
onClick?: (SyntheticEvent<>) => mixed,
onMouseEnter?: (SyntheticEvent<>) => void,
children?: React.Node,
icon?: React.Node,
label?: React.Node,
menu?: React.Node,
showActions?: boolean,
active?: boolean,
isActiveDrop?: boolean,
depth?: number,
scrollIntoViewIfNeeded?: boolean,
|};
export type DragObject = NavigationNode & {
depth: number;
active: boolean;
collectionId: string;
};
type Props = Omit<NavLinkProps, "to"> & {
to?: string | Record<string, any>;
href?: string | Record<string, any>;
innerRef?: (arg0: HTMLElement | null | undefined) => void;
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
onMouseEnter?: React.MouseEventHandler<HTMLAnchorElement>;
icon?: React.ReactNode;
label?: React.ReactNode;
menu?: React.ReactNode;
showActions?: boolean;
active?: boolean;
isActiveDrop?: boolean;
depth?: number;
scrollIntoViewIfNeeded?: boolean;
};
const activeDropStyle = {
fontWeight: 600,
@@ -31,7 +35,6 @@ const activeDropStyle = {
function SidebarLink(
{
icon,
children,
onClick,
onMouseEnter,
to,
@@ -44,10 +47,9 @@ function SidebarLink(
href,
depth,
className,
scrollIntoViewIfNeeded,
...rest
}: Props,
ref
ref: React.RefObject<HTMLAnchorElement>
) {
const theme = useTheme();
const style = React.useMemo(
@@ -71,11 +73,11 @@ function SidebarLink(
<>
<Link
$isActiveDrop={isActiveDrop}
scrollIntoViewIfNeeded={scrollIntoViewIfNeeded}
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
onMouseEnter={onMouseEnter}
// @ts-expect-error exact does not exist on div
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
@@ -101,7 +103,7 @@ const IconWrapper = styled.span`
flex-shrink: 0;
`;
const Actions = styled(EventBoundary)`
const Actions = styled(EventBoundary)<{ showActions?: boolean }>`
display: ${(props) => (props.showActions ? "inline-flex" : "none")};
position: absolute;
top: 4px;
@@ -124,7 +126,7 @@ const Actions = styled(EventBoundary)`
}
`;
const Link = styled(NavLink)`
const Link = styled(NavLink)<{ $isActiveDrop?: boolean }>`
display: flex;
position: relative;
text-overflow: ellipsis;
@@ -182,4 +184,4 @@ const Label = styled.div`
}
`;
export default React.forwardRef<Props, HTMLAnchorElement>(SidebarLink);
export default React.forwardRef<HTMLAnchorElement, Props>(SidebarLink);

View File

@@ -1,17 +1,16 @@
// @flow
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Flex from "components/Flex";
import Flex from "~/components/Flex";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import PlaceholderCollections from "./PlaceholderCollections";
import Section from "./Section";
import SidebarLink from "./SidebarLink";
import StarredLink from "./StarredLink";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
const STARRED_PAGINATION_LIMIT = 10;
const STARRED = "STARRED";
@@ -63,6 +62,7 @@ function Starred() {
useEffect(() => {
setOffset(starred.length);
if (starred.length <= STARRED_PAGINATION_LIMIT) {
setShow("Nothing");
} else if (starred.length >= upperBound) {
@@ -78,17 +78,14 @@ function Starred() {
}
}, [fetchResults, offset]);
const handleShowMore = React.useCallback(
async (ev) => {
setUpperBound(
(previousUpperBound) => previousUpperBound + STARRED_PAGINATION_LIMIT
);
await fetchResults();
},
[fetchResults]
);
const handleShowMore = React.useCallback(async () => {
setUpperBound(
(previousUpperBound) => previousUpperBound + STARRED_PAGINATION_LIMIT
);
await fetchResults();
}, [fetchResults]);
const handleShowLess = React.useCallback((ev) => {
const handleShowLess = React.useCallback(() => {
setUpperBound(STARRED_PAGINATION_LIMIT);
setShow("More");
}, []);
@@ -103,12 +100,13 @@ function Starred() {
} catch (_) {
// no-op Safari private mode
}
setExpanded((prev) => !prev);
},
[expanded]
);
const content = starred.slice(0, upperBound).map((document, index) => {
const content = starred.slice(0, upperBound).map((document) => {
return (
<StarredLink
key={document.id}
@@ -116,7 +114,6 @@ function Starred() {
collectionId={document.collectionId}
to={document.url}
title={document.title}
url={document.url}
depth={2}
/>
);
@@ -151,7 +148,7 @@ function Starred() {
depth={2}
/>
)}
{(isFetching || fetchError) && (
{(isFetching || fetchError) && !starred.length && (
<Flex column>
<PlaceholderCollections />
</Flex>

View File

@@ -1,25 +1,24 @@
// @flow
import { observer } from "mobx-react";
import * as React from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { MAX_TITLE_LENGTH } from "shared/constants";
import Fade from "components/Fade";
import useStores from "../../../hooks/useStores";
import { MAX_TITLE_LENGTH } from "@shared/constants";
import Fade from "~/components/Fade";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import Disclosure from "./Disclosure";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
import useBoolean from "hooks/useBoolean";
import DocumentMenu from "menus/DocumentMenu";
type Props = {|
depth: number,
title: string,
to: string,
documentId: string,
collectionId: string,
|};
type Props = {
depth: number;
title: string;
to: string;
documentId: string;
collectionId: string;
};
function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
const { t } = useTranslation();
@@ -29,11 +28,9 @@ function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
const [expanded, setExpanded] = useState(false);
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const canUpdate = policies.abilities(documentId).update;
const childDocuments = collection
? collection.getDocumentChildren(documentId)
: [];
const hasChildDocuments = childDocuments.length > 0;
useEffect(() => {
@@ -42,25 +39,32 @@ function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
await documents.fetch(documentId);
}
}
load();
}, [collection, collectionId, collections, document, documentId, documents]);
const handleDisclosureClick = React.useCallback((ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded((prevExpanded) => !prevExpanded);
}, []);
const handleDisclosureClick = React.useCallback(
(ev: React.MouseEvent<HTMLDivElement>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded((prevExpanded) => !prevExpanded);
},
[]
);
const handleTitleChange = React.useCallback(
async (title: string) => {
if (!document) return;
await documents.update({
id: document.id,
lastRevision: document.revision,
text: document.text,
title,
});
await documents.update(
{
id: document.id,
text: document.text,
title,
},
{
lastRevision: document.revision,
}
);
},
[documents, document]
);
@@ -71,6 +75,7 @@ function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
<SidebarLink
depth={depth}
to={`${to}?starred`}
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'match' implicitly has an 'any' type.
isActive={(match, location) =>
match && location.search === "?starred"
}

View File

@@ -1,23 +1,22 @@
// @flow
import { observer } from "mobx-react";
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
import TeamLogo from "components/TeamLogo";
import Flex from "~/components/Flex";
import TeamLogo from "~/components/TeamLogo";
type Props = {|
teamName: string,
subheading: React.Node,
showDisclosure?: boolean,
onClick: (event: SyntheticEvent<>) => void,
logoUrl: string,
|};
type Props = {
teamName: string;
subheading: React.ReactNode;
showDisclosure?: boolean;
onClick: React.MouseEventHandler<HTMLButtonElement>;
logoUrl: string;
};
const TeamButton = React.forwardRef<Props, any>(
const TeamButton = React.forwardRef<HTMLButtonElement, Props>(
({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => (
<Wrapper>
<Header justify="flex-start" align="center" ref={ref} {...rest}>
<Header ref={ref} {...rest}>
<TeamLogo
alt={`${teamName} logo`}
src={logoUrl}
@@ -25,7 +24,7 @@ const TeamButton = React.forwardRef<Props, any>(
height={38}
/>
<Flex align="flex-start" column>
<TeamName showDisclosure>
<TeamName>
{teamName} {showDisclosure && <Disclosure color="currentColor" />}
</TeamName>
<Subheading>{subheading}</Subheading>

View File

@@ -1,20 +1,18 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Arrow from "components/Arrow";
import Arrow from "~/components/Arrow";
type Props = {
direction: "left" | "right",
style?: Object,
onClick?: () => any,
direction: "left" | "right";
style?: React.CSSProperties;
onClick?: () => any;
};
const Toggle = React.forwardRef<Props, HTMLButtonElement>(
const Toggle = React.forwardRef<HTMLButtonElement, Props>(
({ direction = "left", onClick, style }: Props, ref) => {
const { t } = useTranslation();
return (
<Positioner style={style}>
<ToggleButton
@@ -30,7 +28,7 @@ const Toggle = React.forwardRef<Props, HTMLButtonElement>(
}
);
export const ToggleButton = styled.button`
export const ToggleButton = styled.button<{ $direction?: "left" | "right" }>`
opacity: 0;
background: none;
transition: opacity 100ms ease-in-out;

View File

@@ -1,30 +1,31 @@
// @flow
import { observer } from "mobx-react";
import { TrashIcon } from "outline-icons";
import * as React from "react";
import { useState } from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import DocumentDelete from "scenes/DocumentDelete";
import Modal from "components/Modal";
import SidebarLink from "./SidebarLink";
import useStores from "hooks/useStores";
import { trashPath } from "utils/routeHelpers";
import Document from "~/models/Document";
import DocumentDelete from "~/scenes/DocumentDelete";
import Modal from "~/components/Modal";
import useStores from "~/hooks/useStores";
import { trashPath } from "~/utils/routeHelpers";
import SidebarLink, { DragObject } from "./SidebarLink";
function TrashLink({ documents }) {
const { policies } = useStores();
function TrashLink() {
const { policies, documents } = useStores();
const { t } = useTranslation();
const [document, setDocument] = useState();
const [document, setDocument] = useState<Document>();
const [{ isDocumentDropping }, dropToTrashDocument] = useDrop({
accept: "document",
drop: (item, monitor) => {
drop: (item: DragObject) => {
const doc = documents.get(item.id);
// without setTimeout it was not working in firefox v89.0.2-ubuntu
// on dropping mouseup is considered as clicking outside the modal, and it immediately closes
setTimeout(() => setDocument(doc), 1);
setTimeout(() => doc && setDocument(doc), 1);
},
canDrop: (item, monitor) => policies.abilities(item.id).delete,
canDrop: (item) => policies.abilities(item.id).delete,
collect: (monitor) => ({
isDocumentDropping: monitor.isOver(),
}),

View File

@@ -1,7 +1,6 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import Badge from "components/Badge";
import Badge from "~/components/Badge";
import { version } from "../../../../package.json";
import SidebarLink from "./SidebarLink";
@@ -15,6 +14,7 @@ export default function Version() {
"https://api.github.com/repos/outline/outline/releases"
);
const releases = await res.json();
for (const release of releases) {
if (release.tag_name === `v${version}`) {
return setReleasesBehind(out);
@@ -23,6 +23,7 @@ export default function Version() {
}
}
}
loadReleases();
}, []);