From d63326066f9e8625693b3077d460e374613b6129 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 14 Mar 2022 17:44:56 -0700 Subject: [PATCH] feat: Improve settings layout (#3234) * Setup, and security settings * Settings -> Details * Settings -> Notifications * Profile * lint * fix: Flash of loading on members screen * align language input * feat: Move share links management to sortable table * Add account menu to sidebar on settings page * Aesthetic tweaks, light borders between settings and slight column offset --- .../DefaultCollectionInputSelect.tsx | 11 +- app/components/Input.tsx | 2 +- app/components/InputSelect.tsx | 6 +- app/components/Scrollable.tsx | 1 + app/components/Sidebar/App.tsx | 24 ---- app/components/Sidebar/Settings.tsx | 8 +- app/components/Sidebar/Sidebar.tsx | 27 +++++ app/components/Table.tsx | 47 +++++--- app/components/TableFromParams.tsx | 75 +++++++++++++ app/components/Text.ts | 6 +- app/scenes/Settings/Details.tsx | 104 +++++++++++------- app/scenes/Settings/Export.tsx | 5 +- app/scenes/Settings/Features.tsx | 29 ++--- app/scenes/Settings/Import.tsx | 5 +- app/scenes/Settings/Members.tsx | 58 +--------- app/scenes/Settings/Notifications.tsx | 64 ++++++----- app/scenes/Settings/Profile.tsx | 93 ++++++++++------ app/scenes/Settings/Security.tsx | 95 +++++++++------- app/scenes/Settings/Shares.tsx | 96 ++++++++++++---- app/scenes/Settings/components/ImageInput.tsx | 31 ++---- .../components/NotificationListItem.tsx | 34 ------ .../Settings/components/PeopleTable.tsx | 20 +--- app/scenes/Settings/components/SettingRow.tsx | 72 ++++++++++++ .../Settings/components/SharesTable.tsx | 82 ++++++++++++++ server/routes/api/shares.ts | 74 +++++++------ shared/i18n/locales/en_US/translation.json | 24 ++-- 26 files changed, 682 insertions(+), 411 deletions(-) create mode 100644 app/components/TableFromParams.tsx delete mode 100644 app/scenes/Settings/components/NotificationListItem.tsx create mode 100644 app/scenes/Settings/components/SettingRow.tsx create mode 100644 app/scenes/Settings/components/SharesTable.tsx diff --git a/app/components/DefaultCollectionInputSelect.tsx b/app/components/DefaultCollectionInputSelect.tsx index 99f8222f2..c6e363334 100644 --- a/app/components/DefaultCollectionInputSelect.tsx +++ b/app/components/DefaultCollectionInputSelect.tsx @@ -1,6 +1,7 @@ import { HomeIcon } from "outline-icons"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; +import { Optional } from "utility-types"; import CollectionIcon from "~/components/CollectionIcon"; import Flex from "~/components/Flex"; import InputSelect from "~/components/InputSelect"; @@ -8,7 +9,9 @@ import { IconWrapper } from "~/components/Sidebar/components/SidebarLink"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; -type DefaultCollectionInputSelectProps = { +type DefaultCollectionInputSelectProps = Optional< + React.ComponentProps +> & { onSelectCollection: (collection: string) => void; defaultCollectionId: string | null; }; @@ -16,6 +19,7 @@ type DefaultCollectionInputSelectProps = { const DefaultCollectionInputSelect = ({ onSelectCollection, defaultCollectionId, + ...rest }: DefaultCollectionInputSelectProps) => { const { t } = useTranslation(); const { collections } = useStores(); @@ -88,14 +92,11 @@ const DefaultCollectionInputSelect = ({ return ( ); }; diff --git a/app/components/Input.tsx b/app/components/Input.tsx index e8135f6cd..8d8257d08 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -97,7 +97,7 @@ export const LabelText = styled.div` display: inline-block; `; -export type Props = { +export type Props = React.HTMLAttributes & { type?: "text" | "email" | "checkbox" | "search" | "textarea"; value?: string; label?: string; diff --git a/app/components/InputSelect.tsx b/app/components/InputSelect.tsx index 946860b11..6770bbca8 100644 --- a/app/components/InputSelect.tsx +++ b/app/components/InputSelect.tsx @@ -23,6 +23,8 @@ export type Option = { }; export type Props = { + id?: string; + name?: string; value?: string | null; label?: string; nude?: boolean; @@ -54,6 +56,7 @@ const InputSelect = (props: Props) => { disabled, note, icon, + ...rest } = props; const select = useSelectState({ @@ -128,7 +131,7 @@ const InputSelect = (props: Props) => { wrappedLabel ))} - {(props) => ( ` margin-bottom: 16px; display: block; width: 100%; + cursor: default; &:hover:not(:disabled) { background: ${(props) => props.theme.buttonNeutralBackground}; diff --git a/app/components/Scrollable.tsx b/app/components/Scrollable.tsx index 58ced4d09..5ef28f984 100644 --- a/app/components/Scrollable.tsx +++ b/app/components/Scrollable.tsx @@ -49,6 +49,7 @@ function Scrollable( React.useEffect(() => { updateShadows(); }, [height, updateShadows]); + return ( { @@ -128,22 +124,6 @@ function AppSidebar() { - - {(props) => ( - - } - /> - )} - )} @@ -154,10 +134,6 @@ const StyledTeamLogo = styled(TeamLogo)` margin-right: 4px; `; -const StyledAvatar = styled(Avatar)` - margin-left: 4px; -`; - const Drafts = styled(Text)` margin: 0 4px; `; diff --git a/app/components/Sidebar/Settings.tsx b/app/components/Sidebar/Settings.tsx index fb8fe6c5e..286c709ce 100644 --- a/app/components/Sidebar/Settings.tsx +++ b/app/components/Sidebar/Settings.tsx @@ -9,8 +9,6 @@ import Flex from "~/components/Flex"; import Scrollable from "~/components/Scrollable"; import env from "~/env"; import useAuthorizedSettingsConfig from "~/hooks/useAuthorizedSettingsConfig"; -import useCurrentTeam from "~/hooks/useCurrentTeam"; -import usePolicy from "~/hooks/usePolicy"; import Sidebar from "./Sidebar"; import Header from "./components/Header"; import Section from "./components/Section"; @@ -23,8 +21,6 @@ const isHosted = env.DEPLOYMENT === "hosted"; function SettingsSidebar() { const { t } = useTranslation(); const history = useHistory(); - const team = useCurrentTeam(); - const can = usePolicy(team.id); const configs = useAuthorizedSettingsConfig(); const groupedConfig = groupBy(configs, "group"); @@ -42,7 +38,7 @@ function SettingsSidebar() { /> - + {Object.keys(groupedConfig).map((header) => (
{header}
@@ -56,7 +52,7 @@ function SettingsSidebar() { ))}
))} - {can.update && !isHosted && ( + {!isHosted && (
{t("Installation")}
diff --git a/app/components/Sidebar/Sidebar.tsx b/app/components/Sidebar/Sidebar.tsx index 3060f84ad..192d58876 100644 --- a/app/components/Sidebar/Sidebar.tsx +++ b/app/components/Sidebar/Sidebar.tsx @@ -6,11 +6,15 @@ import { useLocation } from "react-router-dom"; import styled, { useTheme } from "styled-components"; import breakpoint from "styled-components-breakpoint"; import Flex from "~/components/Flex"; +import useCurrentUser from "~/hooks/useCurrentUser"; import useMenuContext from "~/hooks/useMenuContext"; import usePrevious from "~/hooks/usePrevious"; import useStores from "~/hooks/useStores"; +import AccountMenu from "~/menus/AccountMenu"; import { fadeIn } from "~/styles/animations"; +import Avatar from "../Avatar"; import ResizeBorder from "./components/ResizeBorder"; +import SidebarButton from "./components/SidebarButton"; import Toggle, { ToggleButton, Positioner } from "./components/Toggle"; const ANIMATION_MS = 250; @@ -28,6 +32,8 @@ const Sidebar = React.forwardRef( const location = useLocation(); const previousLocation = usePrevious(location); const { isMenuOpen } = useMenuContext(); + const user = useCurrentUser(); + const width = ui.sidebarWidth; const collapsed = (ui.isEditing || ui.sidebarCollapsed) && !isMenuOpen; const maxWidth = theme.sidebarMaxWidth; @@ -162,6 +168,23 @@ const Sidebar = React.forwardRef( )} {children} + + + {(props) => ( + + } + /> + )} + ( } ); +const StyledAvatar = styled(Avatar)` + margin-left: 4px; +`; + const Backdrop = styled.a` animation: ${fadeIn} 250ms ease-in-out; position: fixed; diff --git a/app/components/Table.tsx b/app/components/Table.tsx index db6dd3364..5af1eb2a6 100644 --- a/app/components/Table.tsx +++ b/app/components/Table.tsx @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import { useTable, useSortBy, usePagination } from "react-table"; import styled from "styled-components"; import Button from "~/components/Button"; +import DelayedMount from "~/components/DelayedMount"; import Empty from "~/components/Empty"; import Flex from "~/components/Flex"; import PlaceholderText from "~/components/PlaceholderText"; @@ -121,7 +122,11 @@ function Table({ {headerGroup.headers.map((column) => ( - + {column.render("Header")} {column.isSorted && (column.isSortedDesc ? ( @@ -190,17 +195,19 @@ export const Placeholder = ({ rows?: number; }) => { return ( - - {new Array(rows).fill(1).map((_, row) => ( - - {new Array(columns).fill(1).map((_, col) => ( - - - - ))} - - ))} - + + + {new Array(rows).fill(1).map((_, row) => ( + + {new Array(columns).fill(1).map((_, col) => ( + + + + ))} + + ))} + + ); }; @@ -214,6 +221,8 @@ const Pagination = styled(Flex)` `; const DescSortIcon = styled(CollapsedIcon)` + margin-left: -2px; + &:hover { fill: ${(props) => props.theme.text}; } @@ -229,12 +238,22 @@ const InnerTable = styled.table` width: 100%; `; -const SortWrapper = styled(Flex)` +const SortWrapper = styled(Flex)<{ $sortable: boolean }>` + display: inline-flex; height: 24px; + user-select: none; + border-radius: 4px; + margin: 0 -4px; + padding: 0 4px; + + &:hover { + background: ${(props) => + props.$sortable ? props.theme.secondaryBackground : "none"}; + } `; const Cell = styled.td` - padding: 6px; + padding: 8px 6px; border-bottom: 1px solid ${(props) => props.theme.divider}; font-size: 14px; diff --git a/app/components/TableFromParams.tsx b/app/components/TableFromParams.tsx new file mode 100644 index 000000000..b5b692d13 --- /dev/null +++ b/app/components/TableFromParams.tsx @@ -0,0 +1,75 @@ +import * as React from "react"; +import { useHistory, useLocation } from "react-router-dom"; +import scrollIntoView from "smooth-scroll-into-view-if-needed"; +import useQuery from "~/hooks/useQuery"; +import type { Props } from "./Table"; + +const Table = React.lazy( + () => + import( + /* webpackChunkName: "table" */ + "~/components/Table" + ) +); + +const TableFromParams = ( + props: Omit +) => { + const topRef = React.useRef(); + const location = useLocation(); + const history = useHistory(); + const params = useQuery(); + + const handleChangeSort = React.useCallback( + (sort, direction) => { + if (sort) { + params.set("sort", sort); + } else { + params.delete("sort"); + } + + params.set("direction", direction.toLowerCase()); + + history.replace({ + pathname: location.pathname, + search: params.toString(), + }); + }, + [params, history, location.pathname] + ); + + const handleChangePage = React.useCallback( + (page) => { + if (page) { + params.set("page", page.toString()); + } else { + params.delete("page"); + } + + history.replace({ + pathname: location.pathname, + search: params.toString(), + }); + + if (topRef.current) { + scrollIntoView(topRef.current, { + scrollMode: "if-needed", + behavior: "auto", + block: "start", + }); + } + }, + [params, history, location.pathname] + ); + + return ( + + ); +}; + +export default TableFromParams; diff --git a/app/components/Text.ts b/app/components/Text.ts index 91a2d2b07..2e8d73eba 100644 --- a/app/components/Text.ts +++ b/app/components/Text.ts @@ -2,7 +2,7 @@ import styled from "styled-components"; type Props = { type?: "secondary" | "tertiary"; - size?: "small" | "xsmall"; + size?: "large" | "small" | "xsmall"; }; /** @@ -18,7 +18,9 @@ const Text = styled.p` ? props.theme.textTertiary : props.theme.text}; font-size: ${(props) => - props.size === "small" + props.size === "large" + ? "18px" + : props.size === "small" ? "14px" : props.size === "xsmall" ? "13px" diff --git a/app/scenes/Settings/Details.tsx b/app/scenes/Settings/Details.tsx index bbaacb796..fdb668849 100644 --- a/app/scenes/Settings/Details.tsx +++ b/app/scenes/Settings/Details.tsx @@ -14,6 +14,7 @@ import useCurrentTeam from "~/hooks/useCurrentTeam"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; import ImageInput from "./components/ImageInput"; +import SettingRow from "./components/SettingRow"; function Details() { const { auth } = useStores(); @@ -89,60 +90,87 @@ function Details() { setDefaultCollectionId(defaultCollectionId); }, []); - const isValid = form.current && form.current.checkValidity(); + const isValid = form.current?.checkValidity(); return ( }> {t("Details")} - These details affect the way that your Outline appears to everyone on - the team. + These settings affect the way that your knowledge base appears to + everyone on the team. - -
- + + + - {env.SUBDOMAINS_ENABLED && ( - <> - - {subdomain && ( + description={t( + "The team name, usually the same as your company name." + )} + > + + + Your knowledge base will be accessible at{" "} {subdomain}.getoutline.com - )} - - )} - + ) : ( + t("Choose a subdomain to enable a login page just for your team.") + ) + } + > + + + + + + diff --git a/app/scenes/Settings/Export.tsx b/app/scenes/Settings/Export.tsx index 11e918e08..63052374a 100644 --- a/app/scenes/Settings/Export.tsx +++ b/app/scenes/Settings/Export.tsx @@ -7,7 +7,6 @@ import Button from "~/components/Button"; import Heading from "~/components/Heading"; import PaginatedList from "~/components/PaginatedList"; import Scene from "~/components/Scene"; -import Subheading from "~/components/Subheading"; import Text from "~/components/Text"; import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; @@ -86,9 +85,9 @@ function Export() { type: "export", }} heading={ - +

Recent exports - +

} renderItem={(item) => ( }> - - Features - + {t("Features")} Manage optional and beta features. Changing these settings will affect the experience for all team members. - - When enabled multiple people can edit documents at the same time - with shared presence and live cursors. - - } - /> + label={t("Collaborative editing")} + description={t( + "When enabled multiple people can edit documents at the same time with shared presence and live cursors." + )} + > + +
); } diff --git a/app/scenes/Settings/Import.tsx b/app/scenes/Settings/Import.tsx index 30f2f8ea5..df8b3915a 100644 --- a/app/scenes/Settings/Import.tsx +++ b/app/scenes/Settings/Import.tsx @@ -12,7 +12,6 @@ import Item from "~/components/List/Item"; import OutlineLogo from "~/components/OutlineLogo"; import PaginatedList from "~/components/PaginatedList"; import Scene from "~/components/Scene"; -import Subheading from "~/components/Subheading"; import Text from "~/components/Text"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; @@ -139,9 +138,9 @@ function Import() { type: "import", }} heading={ - +

Recent imports - +

} renderItem={(item) => ( diff --git a/app/scenes/Settings/Members.tsx b/app/scenes/Settings/Members.tsx index 9325cc976..e34fc0d64 100644 --- a/app/scenes/Settings/Members.tsx +++ b/app/scenes/Settings/Members.tsx @@ -4,7 +4,6 @@ import { PlusIcon, UserIcon } from "outline-icons"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; import { useHistory, useLocation } from "react-router-dom"; -import scrollIntoView from "smooth-scroll-into-view-if-needed"; import styled from "styled-components"; import { PAGINATION_SYMBOL } from "~/stores/BaseStore"; import User from "~/models/User"; @@ -26,7 +25,6 @@ import PeopleTable from "./components/PeopleTable"; import UserStatusFilter from "./components/UserStatusFilter"; function Members() { - const topRef = React.useRef(); const location = useLocation(); const history = useHistory(); const [ @@ -46,9 +44,9 @@ function Members() { const query = params.get("query") || ""; const filter = params.get("filter") || ""; const sort = params.get("sort") || "name"; - const direction = - params.get("direction")?.toUpperCase() === "ASC" ? "ASC" : "DESC"; - + const direction = (params.get("direction") || "asc").toUpperCase() as + | "ASC" + | "DESC"; const page = parseInt(params.get("page") || "0", 10); const limit = 25; @@ -141,52 +139,6 @@ function Members() { [params, history, location.pathname] ); - const handleChangeSort = React.useCallback( - (sort, direction) => { - if (sort) { - params.set("sort", sort); - } else { - params.delete("sort"); - } - - if (direction === "DESC") { - params.set("direction", direction.toLowerCase()); - } else { - params.delete("direction"); - } - - history.replace({ - pathname: location.pathname, - search: params.toString(), - }); - }, - [params, history, location.pathname] - ); - - const handleChangePage = React.useCallback( - (page) => { - if (page) { - params.set("page", page.toString()); - } else { - params.delete("page"); - } - - history.replace({ - pathname: location.pathname, - search: params.toString(), - }); - - if (topRef.current) { - scrollIntoView(topRef.current, { - scrollMode: "if-needed", - behavior: "auto", - block: "start", - }); - } - }, - [params, history, location.pathname] - ); - return ( diff --git a/app/scenes/Settings/Notifications.tsx b/app/scenes/Settings/Notifications.tsx index b436c7fda..42792ae09 100644 --- a/app/scenes/Settings/Notifications.tsx +++ b/app/scenes/Settings/Notifications.tsx @@ -3,18 +3,17 @@ import { observer } from "mobx-react"; import { EmailIcon } from "outline-icons"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; -import styled from "styled-components"; import Heading from "~/components/Heading"; import Input from "~/components/Input"; import Notice from "~/components/Notice"; import Scene from "~/components/Scene"; -import Subheading from "~/components/Subheading"; +import Switch from "~/components/Switch"; import Text from "~/components/Text"; import env from "~/env"; import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; -import NotificationListItem from "./components/NotificationListItem"; +import SettingRow from "./components/SettingRow"; function Notifications() { const { notificationSettings } = useStores(); @@ -48,6 +47,7 @@ function Notifications() { separator: true, }, { + visible: env.DEPLOYMENT === "hosted", event: "emails.onboarding", title: t("Getting started"), description: t( @@ -55,6 +55,7 @@ function Notifications() { ), }, { + visible: env.DEPLOYMENT === "hosted", event: "emails.features", title: t("New features"), description: t("Receive an email when new features of note are added"), @@ -101,42 +102,49 @@ function Notifications() { )} - - Manage when and where you receive email notifications from Outline. - Your email address can be updated in your SSO provider. - + Manage when and where you receive email notifications. {env.EMAIL_ENABLED ? ( <> - + name="email" + description={t( + "Your email address should be updated in your SSO provider." + )} + > + + - {t("Notifications")} +

{t("Notifications")}

- {options.map((option, index) => { + {options.map((option) => { if (option.separator || !option.event) { - return ; + return
; } const setting = notificationSettings.getByEvent(option.event); return ( - + + + ); })} @@ -153,8 +161,4 @@ function Notifications() { ); } -const Separator = styled.hr` - padding-bottom: 12px; -`; - export default observer(Notifications); diff --git a/app/scenes/Settings/Profile.tsx b/app/scenes/Settings/Profile.tsx index 952da03cc..5fc88b0eb 100644 --- a/app/scenes/Settings/Profile.tsx +++ b/app/scenes/Settings/Profile.tsx @@ -15,6 +15,7 @@ import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; import ImageInput from "./components/ImageInput"; +import SettingRow from "./components/SettingRow"; const Profile = () => { const { auth } = useStores(); @@ -74,51 +75,71 @@ const Profile = () => { setShowDeleteModal((prev) => !prev); }; - const isValid = form.current && form.current.checkValidity(); + const isValid = form.current?.checkValidity(); const { isSaving } = auth; return ( }> {t("Profile")} - - - + + + - + + + + - Please note that translations are currently in early access. -
- Community contributions are accepted though our{" "} - - translation portal - - + name="language" + description={ + <> + + Please note that translations are currently in early access. + Community contributions are accepted though our{" "} + + translation portal + + . + + } - short - /> + > + +
+ @@ -126,7 +147,7 @@ const Profile = () => {

{t("Delete Account")}

- + You may delete your account at any time, note that this is unrecoverable diff --git a/app/scenes/Settings/Security.tsx b/app/scenes/Settings/Security.tsx index 4a86a5d0e..6176ff7e9 100644 --- a/app/scenes/Settings/Security.tsx +++ b/app/scenes/Settings/Security.tsx @@ -13,6 +13,7 @@ import env from "~/env"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; +import SettingRow from "./components/SettingRow"; function Security() { const { auth } = useStores(); @@ -25,10 +26,6 @@ function Security() { guestSignin: team.guestSignin, defaultUserRole: team.defaultUserRole, }); - const notes = { - member: t("New user accounts will be given member permissions by default"), - viewer: t("New user accounts will be given viewer permissions by default"), - }; const showSuccessMessage = React.useMemo( () => @@ -42,7 +39,7 @@ function Security() { const handleChange = React.useCallback( async (ev: React.ChangeEvent) => { - const newData = { ...data, [ev.target.name]: ev.target.checked }; + const newData = { ...data, [ev.target.id]: ev.target.checked }; setData(newData); await auth.updateTeam(newData); showSuccessMessage(); @@ -59,9 +56,7 @@ function Security() { return ( }> - - Security - + {t("Security")} Settings that impact the access, security, and content of your @@ -69,54 +64,70 @@ function Security() { - - + + + - + + + - + > + + + + + + ); } diff --git a/app/scenes/Settings/Shares.tsx b/app/scenes/Settings/Shares.tsx index e76e86a4e..99c03a38f 100644 --- a/app/scenes/Settings/Shares.tsx +++ b/app/scenes/Settings/Shares.tsx @@ -1,18 +1,20 @@ +import { sortBy } from "lodash"; import { observer } from "mobx-react"; -import { LinkIcon } from "outline-icons"; +import { LinkIcon, WarningIcon } from "outline-icons"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; import { Link } from "react-router-dom"; -import Empty from "~/components/Empty"; +import { PAGINATION_SYMBOL } from "~/stores/BaseStore"; +import Share from "~/models/Share"; import Heading from "~/components/Heading"; -import PaginatedList from "~/components/PaginatedList"; +import Notice from "~/components/Notice"; import Scene from "~/components/Scene"; -import Subheading from "~/components/Subheading"; import Text from "~/components/Text"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import usePolicy from "~/hooks/usePolicy"; +import useQuery from "~/hooks/useQuery"; import useStores from "~/hooks/useStores"; -import ShareListItem from "./components/ShareListItem"; +import SharesTable from "./components/SharesTable"; function Shares() { const team = useCurrentTeam(); @@ -20,10 +22,64 @@ function Shares() { const { shares, auth } = useStores(); const canShareDocuments = auth.team && auth.team.sharing; const can = usePolicy(team.id); + const [isLoading, setIsLoading] = React.useState(false); + const [data, setData] = React.useState([]); + const [totalPages, setTotalPages] = React.useState(0); + const [shareIds, setShareIds] = React.useState([]); + const params = useQuery(); + const query = params.get("query") || ""; + const sort = params.get("sort") || "createdAt"; + const direction = (params.get("direction") || "desc").toUpperCase() as + | "ASC" + | "DESC"; + const page = parseInt(params.get("page") || "0", 10); + const limit = 25; + + React.useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + + try { + const response = await shares.fetchPage({ + offset: page * limit, + limit, + sort, + direction, + }); + setTotalPages(Math.ceil(response[PAGINATION_SYMBOL].total / limit)); + setShareIds(response.map((u: Share) => u.id)); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, [query, sort, page, direction, shares]); + + React.useEffect(() => { + // sort the resulting data by the original order from the server + setData(sortBy(shares.orderedData, (item) => shareIds.indexOf(item.id))); + }, [shares.orderedData, shareIds]); return ( }> {t("Share Links")} + + {can.manage && !canShareDocuments && ( + <> + }> + {t("Sharing is currently disabled.")}{" "} + , + }} + /> + +
+ + )} + Documents that have been shared are listed below. Anyone that has the @@ -31,25 +87,17 @@ function Shares() { link has been revoked. - {can.manage && ( - - {!canShareDocuments && ( - {t("Sharing is currently disabled.")} - )}{" "} - , - }} - /> - - )} - {t("Shared documents")} - {t("No share links, yet.")}} - fetch={shares.fetchPage} - renderItem={(item) => } + +

{t("Shared documents")}

+ +
); diff --git a/app/scenes/Settings/components/ImageInput.tsx b/app/scenes/Settings/components/ImageInput.tsx index 4ce1c07fa..d875eb3a3 100644 --- a/app/scenes/Settings/components/ImageInput.tsx +++ b/app/scenes/Settings/components/ImageInput.tsx @@ -2,39 +2,30 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; import Flex from "~/components/Flex"; -import { LabelText } from "~/components/Input"; import ImageUpload, { Props as ImageUploadProps } from "./ImageUpload"; type Props = ImageUploadProps & { - label: string; src?: string; }; -export default function ImageInput({ label, src, ...rest }: Props) { +export default function ImageInput({ src, ...rest }: Props) { const { t } = useTranslation(); return ( - - {label} - - - - - {t("Upload")} - - - - + + + + + {t("Upload")} + + + ); } -const InputWrapper = styled(Flex)` - margin-bottom: 24px; -`; - const avatarStyles = ` - width: 80px; - height: 80px; + width: 64px; + height: 64px; `; const Avatar = styled.img` diff --git a/app/scenes/Settings/components/NotificationListItem.tsx b/app/scenes/Settings/components/NotificationListItem.tsx deleted file mode 100644 index c69052202..000000000 --- a/app/scenes/Settings/components/NotificationListItem.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from "react"; -import NotificationSetting from "~/models/NotificationSetting"; -import Switch from "~/components/Switch"; - -type Props = { - setting?: NotificationSetting; - title: string; - event: string; - description: string; - disabled: boolean; - onChange: (ev: React.SyntheticEvent) => void | Promise; -}; - -const NotificationListItem = ({ - setting, - title, - event, - onChange, - disabled, - description, -}: Props) => { - return ( - - ); -}; - -export default NotificationListItem; diff --git a/app/scenes/Settings/components/PeopleTable.tsx b/app/scenes/Settings/components/PeopleTable.tsx index df467400c..205b3fc1e 100644 --- a/app/scenes/Settings/components/PeopleTable.tsx +++ b/app/scenes/Settings/components/PeopleTable.tsx @@ -2,29 +2,16 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; -import { $Diff } from "utility-types"; import User from "~/models/User"; import Avatar from "~/components/Avatar"; import Badge from "~/components/Badge"; import Flex from "~/components/Flex"; -import { Props as TableProps } from "~/components/Table"; +import TableFromParams from "~/components/TableFromParams"; import Time from "~/components/Time"; import useCurrentUser from "~/hooks/useCurrentUser"; import UserMenu from "~/menus/UserMenu"; -const Table = React.lazy( - () => - import( - /* webpackChunkName: "table" */ - "~/components/Table" - ) -); -type Props = $Diff< - TableProps, - { - columns: any; - } -> & { +type Props = Omit, "columns"> & { data: User[]; canManage: boolean; }; @@ -82,6 +69,7 @@ function PeopleTable({ canManage, ...rest }: Props) { Header: " ", accessor: "id", className: "actions", + disableSortBy: true, Cell: observer( ({ row, value }: { value: string; row: { original: User } }) => currentUser.id !== value ? ( @@ -94,7 +82,7 @@ function PeopleTable({ canManage, ...rest }: Props) { [t, canManage, currentUser] ); - return
; + return ; } const Badges = styled.div` diff --git a/app/scenes/Settings/components/SettingRow.tsx b/app/scenes/Settings/components/SettingRow.tsx new file mode 100644 index 000000000..79d29584e --- /dev/null +++ b/app/scenes/Settings/components/SettingRow.tsx @@ -0,0 +1,72 @@ +import { transparentize } from "polished"; +import * as React from "react"; +import styled from "styled-components"; +import breakpoint from "styled-components-breakpoint"; +import Flex from "~/components/Flex"; +import Text from "~/components/Text"; + +type Props = { + label: React.ReactNode; + description: React.ReactNode; + name: string; + children: React.ReactNode; + visible?: boolean; + border?: boolean; +}; + +const Row = styled(Flex)<{ $border?: boolean }>` + display: block; + padding: 24px 0; + border-bottom: 1px solid + ${(props) => + props.$border === false + ? "transparent" + : transparentize(0.5, props.theme.divider)}; + + ${breakpoint("tablet")` + display: flex; + `}; + + &:last-child { + border-bottom: 0; + } +`; + +const Column = styled.div` + display: flex; + flex-direction: column; + flex-basis: 100%; + flex: 1; + + &:first-child { + min-width: 60%; + } + + ${breakpoint("tablet")` + p { + margin-bottom: 0; + } + `}; +`; + +const Label = styled(Text)` + margin-bottom: 4px; +`; + +export default function SettingRow(props: Props) { + if (props.visible === false) { + return null; + } + + return ( + + + + {props.description} + + {props.children} + + ); +} diff --git a/app/scenes/Settings/components/SharesTable.tsx b/app/scenes/Settings/components/SharesTable.tsx new file mode 100644 index 000000000..54c864438 --- /dev/null +++ b/app/scenes/Settings/components/SharesTable.tsx @@ -0,0 +1,82 @@ +import { observer } from "mobx-react"; +import { CheckmarkIcon } from "outline-icons"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "styled-components"; +import Share from "~/models/Share"; +import TableFromParams from "~/components/TableFromParams"; +import Time from "~/components/Time"; +import ShareMenu from "~/menus/ShareMenu"; + +type Props = Omit, "columns"> & { + data: Share[]; + canManage: boolean; +}; + +function SharesTable({ canManage, ...rest }: Props) { + const { t } = useTranslation(); + const theme = useTheme(); + + const columns = React.useMemo( + () => + [ + { + id: "documentTitle", + Header: t("Document"), + accessor: "documentTitle", + disableSortBy: true, + Cell: observer(({ value }: { value: string }) => <>{value}), + }, + { + id: "createdAt", + Header: t("Date shared"), + accessor: "createdAt", + Cell: observer( + ({ value, row }: { value: string; row: { original: Share } }) => + value ? ( + <> +