From 4c138ed5857d4307771adaa24f3ef5bbe558a2ed Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 26 Feb 2022 11:48:32 -0800 Subject: [PATCH] feat: Add "new doc" button on collections in sidebar (#3174) * feat: Add new icon button on collections in sidebar, move sort into menu * Remove unused menu, add warning when dragging in a-z collection * fix: Add hover background to sidebar actions, add tooltip to new doc button * Retain 'active' state on buttons when related context menu is open * fix: Two more spots that deserve active background --- app/components/Button.tsx | 9 ++- app/components/DocumentListItem.tsx | 8 ++ .../Sidebar/components/CollectionLink.tsx | 29 +++++--- .../Sidebar/components/DocumentLink.tsx | 46 +++++++++--- .../Sidebar/components/DropCursor.tsx | 31 +++++--- .../Sidebar/components/SidebarButton.tsx | 3 +- .../Sidebar/components/SidebarLink.tsx | 11 ++- app/menus/CollectionMenu.tsx | 42 +++++++++++ app/menus/CollectionSortMenu.tsx | 73 ------------------- shared/i18n/locales/en_US/translation.json | 8 +- shared/theme.ts | 2 +- 11 files changed, 143 insertions(+), 119 deletions(-) delete mode 100644 app/menus/CollectionSortMenu.tsx diff --git a/app/components/Button.tsx b/app/components/Button.tsx index faf7dd607..e8171c2f5 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -41,7 +41,8 @@ const RealButton = styled.button<{ border: 0; } - &:hover:not(:disabled) { + &:hover:not(:disabled), + &[aria-expanded="true"] { background: ${(props) => darken(0.05, props.theme.buttonBackground)}; } @@ -76,7 +77,8 @@ const RealButton = styled.button<{ } - &:hover:not(:disabled) { + &:hover:not(:disabled), + &[aria-expanded="true"] { background: ${ props.borderOnHover ? props.theme.buttonNeutralBackground @@ -103,7 +105,8 @@ const RealButton = styled.button<{ background: ${props.theme.danger}; color: ${props.theme.white}; - &:hover:not(:disabled) { + &:hover:not(:disabled), + &[aria-expanded="true"] { background: ${darken(0.05, props.theme.danger)}; } diff --git a/app/components/DocumentListItem.tsx b/app/components/DocumentListItem.tsx index a62aec441..e8426cebb 100644 --- a/app/components/DocumentListItem.tsx +++ b/app/components/DocumentListItem.tsx @@ -12,6 +12,7 @@ import DocumentMeta from "~/components/DocumentMeta"; import EventBoundary from "~/components/EventBoundary"; import Flex from "~/components/Flex"; import Highlight from "~/components/Highlight"; +import NudeButton from "~/components/NudeButton"; import StarButton, { AnimatedStar } from "~/components/Star"; import Tooltip from "~/components/Tooltip"; import useBoolean from "~/hooks/useBoolean"; @@ -171,6 +172,13 @@ const Actions = styled(EventBoundary)` flex-shrink: 0; flex-grow: 0; + ${NudeButton} { + &:hover, + &[aria-expanded="true"] { + background: ${(props) => props.theme.sidebarControlHoverBackground}; + } + } + ${breakpoint("tablet")` display: flex; `}; diff --git a/app/components/Sidebar/components/CollectionLink.tsx b/app/components/Sidebar/components/CollectionLink.tsx index 5e2f1c01d..da094d60d 100644 --- a/app/components/Sidebar/components/CollectionLink.tsx +++ b/app/components/Sidebar/components/CollectionLink.tsx @@ -1,22 +1,26 @@ import fractionalIndex from "fractional-index"; import { observer } from "mobx-react"; +import { PlusIcon } from "outline-icons"; import * as React from "react"; import { useDrop, useDrag, DropTargetMonitor } from "react-dnd"; import { useTranslation } from "react-i18next"; -import { useLocation, useHistory } from "react-router-dom"; +import { useLocation, useHistory, Link } from "react-router-dom"; import styled from "styled-components"; import { sortNavigationNodes } from "@shared/utils/collections"; import Collection from "~/models/Collection"; import Document from "~/models/Document"; import DocumentReparent from "~/scenes/DocumentReparent"; import CollectionIcon from "~/components/CollectionIcon"; +import Fade from "~/components/Fade"; import Modal from "~/components/Modal"; +import NudeButton from "~/components/NudeButton"; +import Tooltip from "~/components/Tooltip"; import useBoolean from "~/hooks/useBoolean"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; import CollectionMenu from "~/menus/CollectionMenu"; -import CollectionSortMenu from "~/menus/CollectionSortMenu"; import { NavigationNode } from "~/types"; +import { newDocumentPath } from "~/utils/routeHelpers"; import DocumentLink from "./DocumentLink"; import DropCursor from "./DropCursor"; import DropToImport from "./DropToImport"; @@ -254,20 +258,25 @@ function CollectionLink({ menu={ !isEditing && !isDraggingAnyCollection && ( - <> - {can.update && displayDocumentLinks && ( - + + {can.update && ( + + + + + )} - + ) } /> diff --git a/app/components/Sidebar/components/DocumentLink.tsx b/app/components/Sidebar/components/DocumentLink.tsx index 2d4285428..d903a423a 100644 --- a/app/components/Sidebar/components/DocumentLink.tsx +++ b/app/components/Sidebar/components/DocumentLink.tsx @@ -11,8 +11,10 @@ import Collection from "~/models/Collection"; import Document from "~/models/Document"; import Fade from "~/components/Fade"; import NudeButton from "~/components/NudeButton"; +import Tooltip from "~/components/Tooltip"; import useBoolean from "~/hooks/useBoolean"; import useStores from "~/hooks/useStores"; +import useToasts from "~/hooks/useToasts"; import DocumentMenu from "~/menus/DocumentMenu"; import { NavigationNode } from "~/types"; import { newDocumentPath } from "~/utils/routeHelpers"; @@ -47,6 +49,7 @@ function DocumentLink( }: Props, ref: React.RefObject ) { + const { showToast } = useToasts(); const { documents, policies } = useStores(); const { t } = useTranslation(); const isActiveDocument = activeDocument && activeDocument.id === node.id; @@ -225,6 +228,19 @@ function DocumentLink( const [{ isOverReorder, isDraggingAnyDocument }, dropToReorder] = useDrop({ accept: "document", drop: (item: DragObject) => { + if (!manualSort) { + showToast( + t( + "You can't reorder documents in an alphabetically sorted collection" + ), + { + type: "info", + timeout: 5000, + } + ); + return; + } + if (!collection) { return; } @@ -327,16 +343,18 @@ function DocumentLink( !isDraggingAnyDocument ? ( {can.createChildDocument && ( - - - + + + + + )} - {manualSort && isDraggingAnyDocument && ( - + {isDraggingAnyDocument && ( + )} {openedOnce && ( diff --git a/app/components/Sidebar/components/DropCursor.tsx b/app/components/Sidebar/components/DropCursor.tsx index c4b4bd443..d8bab427a 100644 --- a/app/components/Sidebar/components/DropCursor.tsx +++ b/app/components/Sidebar/components/DropCursor.tsx @@ -1,20 +1,30 @@ import * as React from "react"; import styled from "styled-components"; -function DropCursor({ - isActiveDrop, - innerRef, - position, -}: { +type Props = { + disabled?: boolean; isActiveDrop: boolean; innerRef: React.Ref; position?: "top"; -}) { - return ; +}; + +function DropCursor({ isActiveDrop, innerRef, position, disabled }: Props) { + return ( + + ); } // transparent hover zone with a thin visible band vertically centered -const Cursor = styled.div<{ isOver?: boolean; position?: "top" }>` +const Cursor = styled.div<{ + isOver?: boolean; + disabled?: boolean; + position?: "top"; +}>` opacity: ${(props) => (props.isOver ? 1 : 0)}; transition: opacity 150ms; position: absolute; @@ -26,7 +36,10 @@ const Cursor = styled.div<{ isOver?: boolean; position?: "top" }>` ${(props) => (props.position === "top" ? "top: -7px;" : "bottom: -7px;")} ::after { - background: ${(props) => props.theme.slateDark}; + background: ${(props) => + props.disabled + ? props.theme.sidebarActiveBackground + : props.theme.slateDark}; position: absolute; top: 6px; content: ""; diff --git a/app/components/Sidebar/components/SidebarButton.tsx b/app/components/Sidebar/components/SidebarButton.tsx index 1307ea63e..fc3378f98 100644 --- a/app/components/Sidebar/components/SidebarButton.tsx +++ b/app/components/Sidebar/components/SidebarButton.tsx @@ -71,7 +71,8 @@ const Wrapper = styled(Flex)<{ minHeight: number }>` cursor: pointer; &:active, - &:hover { + &:hover, + &[aria-expanded="true"] { color: ${(props) => props.theme.sidebarText}; transition: background 100ms ease-in-out; background: ${(props) => props.theme.sidebarActiveBackground}; diff --git a/app/components/Sidebar/components/SidebarLink.tsx b/app/components/Sidebar/components/SidebarLink.tsx index 21b57ea19..88cf5a39f 100644 --- a/app/components/Sidebar/components/SidebarLink.tsx +++ b/app/components/Sidebar/components/SidebarLink.tsx @@ -190,13 +190,12 @@ const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>` & + ${Actions} { ${NudeButton} { - background: ${(props) => props.theme.sidebarBackground}; - } - } + background: transparent; - &[aria-current="page"] + ${Actions} { - ${NudeButton} { - background: ${(props) => props.theme.sidebarActiveBackground}; + &:hover, + &[aria-expanded="true"] { + background: ${(props) => props.theme.sidebarControlHoverBackground}; + } } } diff --git a/app/menus/CollectionMenu.tsx b/app/menus/CollectionMenu.tsx index 39d8e9d5f..9ad476231 100644 --- a/app/menus/CollectionMenu.tsx +++ b/app/menus/CollectionMenu.tsx @@ -6,6 +6,8 @@ import { ImportIcon, ExportIcon, PadlockIcon, + AlphabeticalSortIcon, + ManualSortIcon, } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; @@ -124,6 +126,20 @@ function CollectionMenu({ [history, showToast, collection.id, documents] ); + const handleChangeSort = React.useCallback( + (field: string) => { + menu.hide(); + return collection.save({ + sort: { + field, + direction: "asc", + }, + }); + }, + [collection, menu] + ); + + const alphabeticalSort = collection.sort.field === "title"; const can = usePolicy(collection.id); const canUserInTeam = usePolicy(team.id); const items: MenuItem[] = React.useMemo( @@ -145,6 +161,30 @@ function CollectionMenu({ { type: "separator", }, + { + type: "submenu", + title: t("Sort in sidebar"), + visible: can.update, + icon: alphabeticalSort ? ( + + ) : ( + + ), + items: [ + { + type: "button", + title: t("Alphabetical sort"), + onClick: () => handleChangeSort("title"), + selected: alphabeticalSort, + }, + { + type: "button", + title: t("Manual sort"), + onClick: () => handleChangeSort("index"), + selected: !alphabeticalSort, + }, + ], + }, { type: "button", title: `${t("Edit")}…`, @@ -182,6 +222,8 @@ function CollectionMenu({ t, can.update, can.delete, + alphabeticalSort, + handleChangeSort, handleNewDocument, handleImportDocument, collection, diff --git a/app/menus/CollectionSortMenu.tsx b/app/menus/CollectionSortMenu.tsx deleted file mode 100644 index 151fdc4ef..000000000 --- a/app/menus/CollectionSortMenu.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { observer } from "mobx-react"; -import { AlphabeticalSortIcon, ManualSortIcon } from "outline-icons"; -import * as React from "react"; -import { useTranslation } from "react-i18next"; -import { useMenuState, MenuButton } from "reakit/Menu"; -import Collection from "~/models/Collection"; -import ContextMenu from "~/components/ContextMenu"; -import Template from "~/components/ContextMenu/Template"; -import NudeButton from "~/components/NudeButton"; - -type Props = { - collection: Collection; - onOpen?: () => void; - onClose?: () => void; -}; - -function CollectionSortMenu({ collection, onOpen, onClose }: Props) { - const { t } = useTranslation(); - const menu = useMenuState({ - modal: true, - }); - const handleChangeSort = React.useCallback( - (field: string) => { - menu.hide(); - return collection.save({ - sort: { - field, - direction: "asc", - }, - }); - }, - [collection, menu] - ); - const alphabeticalSort = collection.sort.field === "title"; - - return ( - <> - - {(props) => ( - - {alphabeticalSort ? : } - - )} - - -