diff --git a/app/components/Flex.js b/app/components/Flex.js index cb9d7967..c85ebc84 100644 --- a/app/components/Flex.js +++ b/app/components/Flex.js @@ -16,7 +16,7 @@ type AlignValues = | "flex-start" | "flex-end"; -type Props = { +type Props = {| column?: ?boolean, shrink?: ?boolean, align?: AlignValues, @@ -24,12 +24,18 @@ type Props = { auto?: ?boolean, className?: string, children?: React.Node, -}; + role?: string, +|}; -const Flex = (props: Props) => { +const Flex = React.forwardRef((props: Props, ref) => { const { children, ...restProps } = props; - return {children}; -}; + + return ( + + {children} + + ); +}); const Container = styled.div` display: flex; diff --git a/app/components/Sidebar/Main.js b/app/components/Sidebar/Main.js index eb563117..a90dc9a4 100644 --- a/app/components/Sidebar/Main.js +++ b/app/components/Sidebar/Main.js @@ -12,6 +12,8 @@ import { SettingsIcon, } from "outline-icons"; import * as React from "react"; +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; import CollectionNew from "scenes/CollectionNew"; @@ -63,126 +65,142 @@ function MainSidebar() { setInviteModalOpen(false); }, []); + const [dndArea, setDndArea] = React.useState(); + const handleSidebarRef = React.useCallback((node) => setDndArea(node), []); + const html5Options = React.useMemo(() => ({ rootElement: dndArea }), [ + dndArea, + ]); + const { user, team } = auth; if (!user || !team) return null; const can = policies.abilities(team.id); return ( - - - {(props) => ( - - )} - - -
- } - exact={false} - label={t("Home")} - /> - } - label={t("Search")} - exact={false} - /> - } - exact={false} - label={t("Starred")} - /> - } - exact={false} - label={t("Templates")} - active={documents.active ? documents.active.template : undefined} - /> - } - label={ - - {t("Drafts")} - - - } - active={ - documents.active - ? !documents.active.publishedAt && - !documents.active.isDeleted && - !documents.active.isTemplate - : undefined - } - /> -
-
- -
-
- } - exact={false} - label={t("Archive")} - active={ - documents.active - ? documents.active.isArchived && !documents.active.isDeleted - : undefined - } - /> - } - exact={false} - label={t("Trash")} - active={documents.active ? documents.active.isDeleted : undefined} - /> - } - exact={false} - label={t("Settings")} - /> + + {dndArea && ( + + + {(props) => ( + + )} + + +
+ } + exact={false} + label={t("Home")} + /> + } + label={t("Search")} + exact={false} + /> + } + exact={false} + label={t("Starred")} + /> + } + exact={false} + label={t("Templates")} + active={ + documents.active ? documents.active.template : undefined + } + /> + } + label={ + + {t("Drafts")} + + + } + active={ + documents.active + ? !documents.active.publishedAt && + !documents.active.isDeleted && + !documents.active.isTemplate + : undefined + } + /> +
+
+ +
+
+ } + exact={false} + label={t("Archive")} + active={ + documents.active + ? documents.active.isArchived && !documents.active.isDeleted + : undefined + } + /> + } + exact={false} + label={t("Trash")} + active={ + documents.active ? documents.active.isDeleted : undefined + } + /> + } + exact={false} + label={t("Settings")} + /> + {can.invite && ( + } + label={`${t("Invite people")}…`} + /> + )} +
+
{can.invite && ( - } - label={`${t("Invite people")}…`} - /> + + + )} -
-
- {can.invite && ( - - - + + + + )} - - -
); } diff --git a/app/components/Sidebar/Sidebar.js b/app/components/Sidebar/Sidebar.js index 296be79b..a123cc7a 100644 --- a/app/components/Sidebar/Sidebar.js +++ b/app/components/Sidebar/Sidebar.js @@ -13,153 +13,145 @@ import Toggle, { ToggleButton, Positioner } from "./components/Toggle"; import usePrevious from "hooks/usePrevious"; import useStores from "hooks/useStores"; -let firstRender = true; let ANIMATION_MS = 250; +let isFirstRender = true; -type Props = { +type Props = {| children: React.Node, -}; +|}; -function Sidebar({ children }: Props) { - const [isCollapsing, setCollapsing] = React.useState(false); - const theme = useTheme(); - const { t } = useTranslation(); - const { ui } = useStores(); - const location = useLocation(); - const previousLocation = usePrevious(location); +const Sidebar = React.forwardRef( + ({ children }: Props, ref) => { + const [isCollapsing, setCollapsing] = React.useState(false); + const theme = useTheme(); + const { t } = useTranslation(); + const { ui } = useStores(); + const location = useLocation(); + const previousLocation = usePrevious(location); - const width = ui.sidebarWidth; - const collapsed = ui.isEditing || ui.sidebarCollapsed; - const maxWidth = theme.sidebarMaxWidth; - const minWidth = theme.sidebarMinWidth + 16; // padding - const setWidth = ui.setSidebarWidth; + const width = ui.sidebarWidth; + const collapsed = ui.isEditing || ui.sidebarCollapsed; + const maxWidth = theme.sidebarMaxWidth; + const minWidth = theme.sidebarMinWidth + 16; // padding + const setWidth = ui.setSidebarWidth; - const [offset, setOffset] = React.useState(0); - const [isAnimating, setAnimating] = React.useState(false); - const [isResizing, setResizing] = React.useState(false); - const isSmallerThanMinimum = width < minWidth; + const [offset, setOffset] = React.useState(0); + const [isAnimating, setAnimating] = React.useState(false); + const [isResizing, setResizing] = React.useState(false); + const isSmallerThanMinimum = width < minWidth; - const handleDrag = React.useCallback( - (event: MouseEvent) => { - // suppresses text selection - event.preventDefault(); + const handleDrag = React.useCallback( + (event: MouseEvent) => { + // suppresses text selection + event.preventDefault(); - // this is simple because the sidebar is always against the left edge - const width = Math.min(event.pageX - offset, maxWidth); - const isSmallerThanCollapsePoint = width < minWidth / 2; - - if (isSmallerThanCollapsePoint) { - setWidth(theme.sidebarCollapsedWidth); - } else { - setWidth(width); - } - }, - [theme, offset, minWidth, maxWidth, setWidth] - ); - - const handleStopDrag = React.useCallback( - (event: MouseEvent) => { - setResizing(false); - - if (document.activeElement) { - document.activeElement.blur(); - } - - if (isSmallerThanMinimum) { + // this is simple because the sidebar is always against the left edge + const width = Math.min(event.pageX - offset, maxWidth); const isSmallerThanCollapsePoint = width < minWidth / 2; if (isSmallerThanCollapsePoint) { - setAnimating(false); - setCollapsing(true); - ui.collapseSidebar(); + setWidth(theme.sidebarCollapsedWidth); } else { - setWidth(minWidth); - setAnimating(true); + setWidth(width); } - } else { - setWidth(width); + }, + [theme, offset, minWidth, maxWidth, setWidth] + ); + + const handleStopDrag = React.useCallback( + (event: MouseEvent) => { + setResizing(false); + + if (document.activeElement) { + document.activeElement.blur(); + } + + if (isSmallerThanMinimum) { + const isSmallerThanCollapsePoint = width < minWidth / 2; + + if (isSmallerThanCollapsePoint) { + setAnimating(false); + setCollapsing(true); + ui.collapseSidebar(); + } else { + setWidth(minWidth); + setAnimating(true); + } + } else { + setWidth(width); + } + }, + [ui, isSmallerThanMinimum, minWidth, width, setWidth] + ); + + const handleMouseDown = React.useCallback( + (event: MouseEvent) => { + setOffset(event.pageX - width); + setResizing(true); + setAnimating(false); + }, + [width] + ); + + React.useEffect(() => { + if (isAnimating) { + setTimeout(() => setAnimating(false), ANIMATION_MS); } - }, - [ui, isSmallerThanMinimum, minWidth, width, setWidth] - ); + }, [isAnimating]); - const handleMouseDown = React.useCallback( - (event: MouseEvent) => { - setOffset(event.pageX - width); - setResizing(true); - setAnimating(false); - }, - [width] - ); + React.useEffect(() => { + if (isCollapsing) { + setTimeout(() => { + setWidth(minWidth); + setCollapsing(false); + }, ANIMATION_MS); + } + }, [setWidth, minWidth, isCollapsing]); - React.useEffect(() => { - if (isAnimating) { - setTimeout(() => setAnimating(false), ANIMATION_MS); - } - }, [isAnimating]); + React.useEffect(() => { + if (isResizing) { + document.addEventListener("mousemove", handleDrag); + document.addEventListener("mouseup", handleStopDrag); + } - React.useEffect(() => { - if (isCollapsing) { - setTimeout(() => { - setWidth(minWidth); - setCollapsing(false); - }, ANIMATION_MS); - } - }, [setWidth, minWidth, isCollapsing]); + return () => { + document.removeEventListener("mousemove", handleDrag); + document.removeEventListener("mouseup", handleStopDrag); + }; + }, [isResizing, handleDrag, handleStopDrag]); - React.useEffect(() => { - if (isResizing) { - document.addEventListener("mousemove", handleDrag); - document.addEventListener("mouseup", handleStopDrag); - } + const handleReset = React.useCallback(() => { + ui.setSidebarWidth(theme.sidebarWidth); + }, [ui, theme.sidebarWidth]); - return () => { - document.removeEventListener("mousemove", handleDrag); - document.removeEventListener("mouseup", handleStopDrag); - }; - }, [isResizing, handleDrag, handleStopDrag]); + React.useEffect(() => { + ui.setSidebarResizing(isResizing); + }, [ui, isResizing]); - const handleReset = React.useCallback(() => { - ui.setSidebarWidth(theme.sidebarWidth); - }, [ui, theme.sidebarWidth]); + React.useEffect(() => { + if (location !== previousLocation) { + isFirstRender = false; + ui.hideMobileSidebar(); + } + }, [ui, location, previousLocation]); - React.useEffect(() => { - ui.setSidebarResizing(isResizing); - }, [ui, isResizing]); + const style = React.useMemo( + () => ({ + width: `${width}px`, + }), + [width] + ); - React.useEffect(() => { - if (location !== previousLocation) { - ui.hideMobileSidebar(); - } - }, [ui, location, previousLocation]); + const toggleStyle = React.useMemo( + () => ({ + right: "auto", + marginLeft: `${collapsed ? theme.sidebarCollapsedWidth : width}px`, + }), + [width, theme.sidebarCollapsedWidth, collapsed] + ); - const style = React.useMemo( - () => ({ - width: `${width}px`, - }), - [width] - ); - - const toggleStyle = React.useMemo( - () => ({ - right: "auto", - marginLeft: `${collapsed ? theme.sidebarCollapsedWidth : width}px`, - }), - [width, theme.sidebarCollapsedWidth, collapsed] - ); - - const content = ( - <> - + const content = ( + <> {ui.mobileSidebarVisible && ( @@ -180,26 +172,36 @@ function Sidebar({ children }: Props) { aria-label={t("Expand")} /> )} - - {!ui.isEditing && ( - - )} - - ); + + ); - // Fade in the sidebar on first render after page load - if (firstRender) { - firstRender = false; - return {content}; + return ( + <> + + {isFirstRender ? {content} : content} + + {!ui.isEditing && ( + + )} + + ); } - - return content; -} +); const Background = styled.a` position: fixed; diff --git a/app/components/Sidebar/components/Collections.js b/app/components/Sidebar/components/Collections.js index eefe5672..759d7958 100644 --- a/app/components/Sidebar/components/Collections.js +++ b/app/components/Sidebar/components/Collections.js @@ -35,7 +35,7 @@ class Collections extends React.Component { componentDidMount() { const { collections } = this.props; - if (!collections.isFetching && !collections.isLoaded) { + if (!collections.isLoaded) { collections.fetchPage({ limit: 100 }); } } diff --git a/app/components/Sidebar/components/DocumentLink.js b/app/components/Sidebar/components/DocumentLink.js index f0bf5e47..02296e5d 100644 --- a/app/components/Sidebar/components/DocumentLink.js +++ b/app/components/Sidebar/components/DocumentLink.js @@ -123,7 +123,8 @@ function DocumentLink({ // Draggable const [{ isDragging }, drag] = useDrag({ - item: { type: "document", ...node, depth, active: isActiveDocument }, + type: "document", + item: () => ({ ...node, depth, active: isActiveDocument }), collect: (monitor) => ({ isDragging: !!monitor.isDragging(), }), @@ -146,7 +147,7 @@ function DocumentLink({ // Drop to re-parent const [{ isOverReparent, canDropToReparent }, dropToReparent] = useDrop({ accept: "document", - drop: async (item, monitor) => { + drop: (item, monitor) => { if (monitor.didDrop()) return; if (!collection) return; documents.move(item.id, collection.id, node.id); @@ -183,7 +184,7 @@ function DocumentLink({ // Drop to reorder const [{ isOverReorder }, dropToReorder] = useDrop({ accept: "document", - drop: async (item, monitor) => { + drop: (item, monitor) => { if (!collection) return; if (item.id === node.id) return; diff --git a/app/index.js b/app/index.js index 3fab57f9..6c7d1731 100644 --- a/app/index.js +++ b/app/index.js @@ -3,8 +3,6 @@ import "focus-visible"; import { createBrowserHistory } from "history"; import { Provider } from "mobx-react"; import * as React from "react"; -import { DndProvider } from "react-dnd"; -import { HTML5Backend } from "react-dnd-html5-backend"; import { render } from "react-dom"; import { Router } from "react-router-dom"; import { initI18n } from "shared/i18n"; @@ -47,17 +45,15 @@ if (element) { - - - <> - - - - - - - - + + <> + + + + + + + , diff --git a/package.json b/package.json index 0ab21521..419a7ea6 100644 --- a/package.json +++ b/package.json @@ -140,8 +140,8 @@ "react-autosize-textarea": "^6.0.0", "react-avatar-editor": "^10.3.0", "react-color": "^2.17.3", - "react-dnd": "^11.1.3", - "react-dnd-html5-backend": "^11.1.3", + "react-dnd": "^14.0.1", + "react-dnd-html5-backend": "^14.0.0", "react-dom": "^16.8.6", "react-dropzone": "^11.2.4", "react-helmet": "^5.2.0", diff --git a/yarn.lock b/yarn.lock index 1f45f50a..3c315689 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2033,14 +2033,6 @@ dependencies: "@types/unist" "*" -"@types/hoist-non-react-statics@^3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -2105,19 +2097,6 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ== -"@types/prop-types@*": - version "15.7.3" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" - integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== - -"@types/react@*": - version "17.0.0" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8" - integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -4339,11 +4318,6 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" -csstype@^3.0.2: - version "3.0.5" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.5.tgz#7fdec6a28a67ae18647c51668a9ff95bb2fa7bb8" - integrity sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ== - cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -4624,14 +4598,14 @@ direction@^0.1.5: resolved "https://registry.yarnpkg.com/direction/-/direction-0.1.5.tgz#ce5d797f97e26f8be7beff53f7dc40e1c1a9ec4c" integrity sha1-zl15f5fib4vnvv9T99xA4cGp7Ew= -dnd-core@^11.1.3: - version "11.1.3" - resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-11.1.3.tgz#f92099ba7245e49729d2433157031a6267afcc98" - integrity sha512-QugF55dNW+h+vzxVJ/LSJeTeUw9MCJ2cllhmVThVPEtF16ooBkxj0WBE5RB+AceFxMFo1rO6bJKXtqKl+JNnyA== +dnd-core@14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.0.tgz#973ab3470d0a9ac5a0fa9021c4feba93ad12347d" + integrity sha512-wTDYKyjSqWuYw3ZG0GJ7k+UIfzxTNoZLjDrut37PbcPGNfwhlKYlPUqjAKUjOOv80izshUiqusaKgJPItXSevA== dependencies: "@react-dnd/asap" "^4.0.0" "@react-dnd/invariant" "^2.0.0" - redux "^4.0.4" + redux "^4.0.5" doctrine@1.5.0: version "1.5.0" @@ -5526,7 +5500,7 @@ fast-deep-equal@^1.0.0: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -6453,7 +6427,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -10686,22 +10660,23 @@ react-color@^2.17.3: reactcss "^1.2.0" tinycolor2 "^1.4.1" -react-dnd-html5-backend@^11.1.3: - version "11.1.3" - resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz#2749f04f416ec230ea193f5c1fbea2de7dffb8f7" - integrity sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw== +react-dnd-html5-backend@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.0.tgz#28d660a2ad1e07447c34a65cd25f7de8f1657194" + integrity sha512-2wAQqRFC1hbRGmk6+dKhOXsyQQOn3cN8PSZyOUeOun9J8t3tjZ7PS2+aFu7CVu2ujMDwTJR3VTwZh8pj2kCv7g== dependencies: - dnd-core "^11.1.3" + dnd-core "14.0.0" -react-dnd@^11.1.3: - version "11.1.3" - resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-11.1.3.tgz#f9844f5699ccc55dfc81462c2c19f726e670c1af" - integrity sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ== +react-dnd@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.1.tgz#012a855b0fb35a27c7c84aba7ff6c495e0baddb2" + integrity sha512-r57KKBfmAYTwmQ/cREQehNjEX9U9Xi4AUWykLX92fB9JkY9z90DMWZhSE1M7o6Y71Y2/a2SBvSPQ385QboNrIQ== dependencies: + "@react-dnd/invariant" "^2.0.0" "@react-dnd/shallowequal" "^2.0.0" - "@types/hoist-non-react-statics" "^3.3.1" - dnd-core "^11.1.3" - hoist-non-react-statics "^3.3.0" + dnd-core "14.0.0" + fast-deep-equal "^3.1.3" + hoist-non-react-statics "^3.3.2" react-dom@^16.8.6: version "16.14.0" @@ -11028,7 +11003,7 @@ redis@^3.0.0: redis-errors "^1.2.0" redis-parser "^3.0.0" -redux@^4.0.4: +redux@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==