Compare commits

..

1 Commits

Author SHA1 Message Date
Saumya Pandey c10e868626 fix: aria-checkbox selector isn't working in firefox 2022-02-12 18:41:37 +05:30
133 changed files with 649 additions and 1118 deletions
+2 -5
View File
@@ -1,4 +1,3 @@
import { transparentize } from "polished";
import styled from "styled-components";
const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>`
@@ -10,10 +9,8 @@ const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>`
primary ? theme.white : yellow ? theme.almostBlack : theme.textTertiary};
border: 1px solid
${({ primary, yellow, theme }) =>
primary || yellow
? "transparent"
: transparentize(0.4, theme.textTertiary)};
border-radius: 10px;
primary || yellow ? "transparent" : theme.textTertiary};
border-radius: 8px;
font-size: 12px;
font-weight: 500;
user-select: none;
+1 -8
View File
@@ -1,5 +1,5 @@
import { ExpandedIcon } from "outline-icons";
import { darken, lighten } from "polished";
import { darken } from "polished";
import * as React from "react";
import styled from "styled-components";
@@ -26,7 +26,6 @@ const RealButton = styled.button<{
flex-shrink: 0;
cursor: pointer;
user-select: none;
appearance: none !important;
${(props) =>
!props.borderOnHover &&
@@ -49,7 +48,6 @@ const RealButton = styled.button<{
cursor: default;
pointer-events: none;
color: ${(props) => props.theme.white50};
background: ${(props) => lighten(0.2, props.theme.buttonBackground)};
svg {
fill: ${(props) => props.theme.white50};
@@ -89,7 +87,6 @@ const RealButton = styled.button<{
&:disabled {
color: ${props.theme.textTertiary};
background: none;
svg {
fill: currentColor;
@@ -106,10 +103,6 @@ const RealButton = styled.button<{
&:hover:not(:disabled) {
background: ${darken(0.05, props.theme.danger)};
}
&:disabled {
background: none;
}
`};
`;
+3 -3
View File
@@ -32,7 +32,7 @@ function CommandBarItem(
return (
<Item active={active} ref={ref}>
<Content align="center" gap={8}>
<Text align="center" gap={8}>
<Icon>
{action.icon ? (
// @ts-expect-error no icon on ActionImpl
@@ -53,7 +53,7 @@ function CommandBarItem(
))}
{action.name}
{action.children?.length ? "…" : ""}
</Content>
</Text>
{action.shortcut?.length ? (
<div
style={{
@@ -84,7 +84,7 @@ const Ancestor = styled.span`
color: ${(props) => props.theme.textSecondary};
`;
const Content = styled(Flex)`
const Text = styled(Flex)`
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
+1 -1
View File
@@ -73,7 +73,7 @@ const ContentEditable = React.forwardRef(
if (autoFocus) {
ref.current?.focus();
}
}, [autoFocus, ref]);
});
React.useEffect(() => {
if (value !== ref.current?.innerText) {
+2 -7
View File
@@ -11,7 +11,6 @@ type Props = {
children?: React.ReactNode;
selected?: boolean;
disabled?: boolean;
dangerous?: boolean;
to?: string;
href?: string;
target?: "_blank";
@@ -93,11 +92,7 @@ const Spacer = styled.svg`
flex-shrink: 0;
`;
export const MenuAnchorCSS = css<{
level?: number;
disabled?: boolean;
dangerous?: boolean;
}>`
export const MenuAnchorCSS = css<{ level?: number; disabled?: boolean }>`
display: flex;
margin: 0;
border: 0;
@@ -133,7 +128,7 @@ export const MenuAnchorCSS = css<{
&:focus,
&.focus-visible {
color: ${props.theme.white};
background: ${props.dangerous ? props.theme.danger : props.theme.primary};
background: ${props.theme.primary};
box-shadow: none;
cursor: pointer;
-1
View File
@@ -163,7 +163,6 @@ function Template({ items, actions, context, ...menu }: Props) {
onClick={item.onClick}
disabled={item.disabled}
selected={item.selected}
dangerous={item.dangerous}
key={index}
icon={item.icon}
{...menu}
+6 -13
View File
@@ -2,7 +2,7 @@ import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { m } from "framer-motion";
import { observer } from "mobx-react";
import { CloseIcon, DocumentIcon, ClockIcon } from "outline-icons";
import { CloseIcon, DocumentIcon } from "outline-icons";
import { getLuminance, transparentize } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
@@ -10,12 +10,11 @@ import { Link } from "react-router-dom";
import styled, { css } from "styled-components";
import Document from "~/models/Document";
import Pin from "~/models/Pin";
import DocumentMeta from "~/components/DocumentMeta";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Time from "~/components/Time";
import useStores from "~/hooks/useStores";
import CollectionIcon from "./CollectionIcon";
import Text from "./Text";
import Tooltip from "./Tooltip";
type Props = {
@@ -97,10 +96,8 @@ function DocumentCard(props: Props) {
)}
<div>
<Heading dir={document.dir}>{document.titleWithDefault}</Heading>
<DocumentMeta size="xsmall">
<ClockIcon color="currentColor" size={18} />{" "}
<Time dateTime={document.updatedAt} addSuffix shorten />
</DocumentMeta>
<StyledDocumentMeta document={document} />
</div>
</Content>
</DocumentLink>
@@ -186,12 +183,8 @@ const Content = styled(Flex)`
z-index: 1;
`;
const DocumentMeta = styled(Text)`
display: flex;
align-items: center;
gap: 2px;
color: ${(props) => transparentize(0.25, props.theme.white)};
margin: 0;
const StyledDocumentMeta = styled(DocumentMeta)`
color: ${(props) => transparentize(0.25, props.theme.white)} !important;
`;
const DocumentLink = styled(Link)<{
-3
View File
@@ -125,9 +125,6 @@ function DocumentMeta({
}
if (!lastViewedAt) {
if (lastUpdatedByCurrentUser) {
return null;
}
return (
<Viewed>
&nbsp;<Modified highlight>{t("Never viewed")}</Modified>
+2 -3
View File
@@ -1,7 +1,6 @@
import * as React from "react";
import { Optional } from "utility-types";
import embeds from "@shared/editor/embeds";
import { isInternalUrl } from "@shared/utils/urls";
import ErrorBoundary from "~/components/ErrorBoundary";
import { Props as EditorProps } from "~/editor";
import useDictionary from "~/hooks/useDictionary";
@@ -9,7 +8,7 @@ import useToasts from "~/hooks/useToasts";
import history from "~/utils/history";
import { isModKey } from "~/utils/keyboard";
import { uploadFile } from "~/utils/uploadFile";
import { isHash } from "~/utils/urls";
import { isInternalUrl, isHash } from "~/utils/urls";
const SharedEditor = React.lazy(
() =>
@@ -103,4 +102,4 @@ function Editor(props: Props, ref: React.Ref<any>) {
);
}
export default React.forwardRef(Editor);
export default React.forwardRef<typeof Editor, Props>(Editor);
+5 -5
View File
@@ -7,8 +7,8 @@ import styled from "styled-components";
import { githubIssuesUrl } from "@shared/utils/urlHelpers";
import Button from "~/components/Button";
import CenteredContent from "~/components/CenteredContent";
import HelpText from "~/components/HelpText";
import PageTitle from "~/components/PageTitle";
import Text from "~/components/Text";
import env from "~/env";
type Props = WithTranslation & {
@@ -72,13 +72,13 @@ class ErrorBoundary extends React.Component<Props> {
<h1>
<Trans>Loading Failed</Trans>
</h1>
<Text type="secondary">
<HelpText>
<Trans>
Sorry, part of the application failed to load. This may be
because it was updated since you opened the tab or because of a
failed network request. Please try reloading.
</Trans>
</Text>
</HelpText>
<p>
<Button onClick={this.handleReload}>{t("Reload")}</Button>
</p>
@@ -92,7 +92,7 @@ class ErrorBoundary extends React.Component<Props> {
<h1>
<Trans>Something Unexpected Happened</Trans>
</h1>
<Text type="secondary">
<HelpText>
<Trans
defaults="Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch."
values={{
@@ -101,7 +101,7 @@ class ErrorBoundary extends React.Component<Props> {
: undefined,
}}
/>
</Text>
</HelpText>
{this.showDetails && <Pre>{error.toString()}</Pre>}
<p>
<Button onClick={this.handleReload}>{t("Reload")}</Button>{" "}
+8 -9
View File
@@ -8,7 +8,6 @@ import {
} from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import styled from "styled-components";
import Document from "~/models/Document";
import Event from "~/models/Event";
@@ -28,7 +27,6 @@ type Props = {
const EventListItem = ({ event, latest, document }: Props) => {
const { t } = useTranslation();
const { policies } = useStores();
const location = useLocation();
const can = policies.abilities(document.id);
const opts = {
userName: event.actor.name,
@@ -88,8 +86,6 @@ const EventListItem = ({ event, latest, document }: Props) => {
return null;
}
const isActive = location.pathname === to;
return (
<ListItem
small
@@ -98,8 +94,8 @@ const EventListItem = ({ event, latest, document }: Props) => {
title={
<Time
dateTime={event.createdAt}
tooltipDelay={500}
format="MMM do, h:mm a"
tooltipDelay={250}
format="MMMM do, h:mm a"
relative={false}
addSuffix
/>
@@ -112,7 +108,7 @@ const EventListItem = ({ event, latest, document }: Props) => {
</Subtitle>
}
actions={
isRevision && isActive && event.modelId && can.update ? (
isRevision && event.modelId && can.update ? (
<RevisionMenu document={document} revisionId={event.modelId} />
) : undefined
}
@@ -165,9 +161,12 @@ const ListItem = styled(Item)`
}
${Actions} {
opacity: 0.5;
opacity: 0.25;
transition: opacity 100ms ease-in-out;
}
&:hover {
&:hover {
${Actions} {
opacity: 1;
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ import styled from "styled-components";
import Button, { Inner } from "~/components/Button";
import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import Text from "~/components/Text";
import HelpText from "~/components/HelpText";
type TFilterOption = {
key: string;
@@ -72,7 +72,7 @@ const FilterOptions = ({
);
};
const Note = styled(Text)`
const Note = styled(HelpText)`
margin-top: 2px;
margin-bottom: 0;
line-height: 1.2em;
+1 -1
View File
@@ -131,7 +131,7 @@ const Wrapper = styled(Flex)<{ $passThrough?: boolean }>`
}
${breakpoint("tablet")`
padding: 16px;
padding: 16px 16px 0;
justify-content: center;
`};
`;
-1
View File
@@ -3,7 +3,6 @@ import styled from "styled-components";
const Heading = styled.h1<{ centered?: boolean }>`
display: flex;
align-items: center;
user-select: none;
${(props) => (props.centered ? "text-align: center;" : "")}
svg {
+10
View File
@@ -0,0 +1,10 @@
import styled from "styled-components";
const HelpText = styled.p<{ small?: boolean }>`
margin-top: 0;
color: ${(props) => props.theme.textSecondary};
font-size: ${(props) => (props.small ? "14px" : "inherit")};
white-space: normal;
`;
export default HelpText;
+1 -1
View File
@@ -3,10 +3,10 @@ import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import { isInternalUrl } from "@shared/utils/urls";
import HoverPreviewDocument from "~/components/HoverPreviewDocument";
import useStores from "~/hooks/useStores";
import { fadeAndSlideDown } from "~/styles/animations";
import { isInternalUrl } from "~/utils/urls";
const DELAY_OPEN = 300;
const DELAY_CLOSE = 300;
+2 -2
View File
@@ -42,9 +42,9 @@ import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import ContextMenu from "~/components/ContextMenu";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import { LabelText } from "~/components/Input";
import NudeButton from "~/components/NudeButton";
import Text from "~/components/Text";
const style = {
width: 30,
@@ -324,7 +324,7 @@ const IconButton = styled(NudeButton)`
height: 30px;
`;
const Loading = styled(Text)`
const Loading = styled(HelpText)`
padding: 16px;
`;
+3 -3
View File
@@ -3,8 +3,8 @@ import * as React from "react";
import { Trans } from "react-i18next";
import styled from "styled-components";
import Editor from "~/components/Editor";
import HelpText from "~/components/HelpText";
import { LabelText, Outline } from "~/components/Input";
import Text from "~/components/Text";
type Props = {
label: string;
@@ -32,9 +32,9 @@ function InputRich({ label, minHeight, maxHeight, ...rest }: Props) {
>
<React.Suspense
fallback={
<Text type="secondary">
<HelpText>
<Trans>Loading editor</Trans>
</Text>
</HelpText>
}
>
<Editor onBlur={handleBlur} onFocus={handleFocus} grow {...rest} />
+2 -6
View File
@@ -11,7 +11,7 @@ import { VisuallyHidden } from "reakit/VisuallyHidden";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import styled, { css } from "styled-components";
import Button, { Inner } from "~/components/Button";
import Text from "~/components/Text";
import HelpText from "~/components/HelpText";
import useMenuHeight from "~/hooks/useMenuHeight";
import { Position, Background, Backdrop, Placement } from "./ContextMenu";
import { MenuAnchorCSS } from "./ContextMenu/MenuItem";
@@ -203,11 +203,7 @@ const InputSelect = (props: Props) => {
}}
</SelectPopover>
</Wrapper>
{note && (
<Text type="secondary" size="small">
{note}
</Text>
)}
{note && <HelpText small>{note}</HelpText>}
{select.visible && <Backdrop />}
</>
);
+3 -9
View File
@@ -1,16 +1,10 @@
import styled from "styled-components";
type Props = {
/* Set to true if displaying a single symbol character to disable monospace */
symbol?: boolean;
};
const Key = styled.kbd<Props>`
const Key = styled.kbd`
display: inline-block;
padding: 4px 6px;
font-size: 11px;
font-family: ${(props) =>
props.symbol ? props.theme.fontFamily : props.theme.fontFamilyMono};
font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
line-height: 10px;
color: ${(props) => props.theme.almostBlack};
vertical-align: middle;
+3 -7
View File
@@ -69,21 +69,17 @@ function LocaleTime({
const tooltipContent = formatDate(
Date.parse(dateTime),
"MMMM do, yyyy h:mm a",
format || "MMMM do, yyyy h:mm a",
{
locale,
}
);
const content =
relative !== false
? relativeContent
: formatDate(Date.parse(dateTime), format || "MMMM do, yyyy h:mm a", {
locale,
});
children || relative !== false ? relativeContent : tooltipContent;
return (
<Tooltip tooltip={tooltipContent} delay={tooltipDelay} placement="bottom">
<time dateTime={dateTime}>{children || content}</time>
<time dateTime={dateTime}>{content}</time>
</Tooltip>
);
}
+11
View File
@@ -0,0 +1,11 @@
import styled from "styled-components";
const Notice = styled.p`
background: ${(props) => props.theme.sidebarBackground};
color: ${(props) => props.theme.sidebarText};
padding: 10px 12px;
border-radius: 4px;
position: relative;
`;
export default Notice;
-50
View File
@@ -1,50 +0,0 @@
import React from "react";
import styled from "styled-components";
import Flex from "./Flex";
import Text from "./Text";
type Props = {
children: React.ReactNode;
icon?: JSX.Element;
description?: JSX.Element;
};
const Notice = ({ children, icon, description }: Props) => {
return (
<Container>
<Flex as="span" gap={8}>
{icon}
<span>
<Title>{children}</Title>
{description && (
<>
<br />
{description}
</>
)}
</span>
</Flex>
</Container>
);
};
const Title = styled.span`
font-weight: 500;
font-size: 16px;
`;
const Container = styled(Text)`
background: ${(props) => props.theme.sidebarBackground};
color: ${(props) => props.theme.sidebarText};
padding: 10px 12px;
border-radius: 4px;
position: relative;
font-size: 14px;
margin: 1em 0 0;
svg {
flex-shrink: 0;
}
`;
export default Notice;
+1 -1
View File
@@ -41,7 +41,7 @@ function Star({ size, document, ...rest }: Props) {
{...rest}
>
{document.isStarred ? (
<AnimatedStar size={size} color={theme.yellow} />
<AnimatedStar size={size} color={theme.textSecondary} />
) : (
<AnimatedStar
size={size}
+2 -11
View File
@@ -1,8 +1,7 @@
import * as React from "react";
import styled from "styled-components";
import HelpText from "~/components/HelpText";
import { LabelText } from "~/components/Input";
import Text from "~/components/Text";
import Flex from "./Flex";
type Props = React.HTMLAttributes<HTMLInputElement> & {
width?: number;
@@ -49,14 +48,7 @@ function Switch({
{component}
<InlineLabelText>{label}</InlineLabelText>
</Label>
{note && (
<Flex>
<Input width={width} height={height} aria-hidden="true" />
<Text type="secondary" size="small">
{note}
</Text>
</Flex>
)}
{note && <HelpText small>{note}</HelpText>}
</Wrapper>
);
}
@@ -75,7 +67,6 @@ const InlineLabelText = styled(LabelText)`
const Label = styled.label<{ disabled?: boolean }>`
display: flex;
align-items: center;
user-select: none;
${(props) => (props.disabled ? `opacity: 0.75;` : "")}
`;
-30
View File
@@ -1,30 +0,0 @@
import styled from "styled-components";
type Props = {
type?: "secondary" | "tertiary";
size?: "small" | "xsmall";
};
/**
* Use this component for all interface text that should not be selectable
* by the user, this is the majority of UI text explainers, notes, headings.
*/
const Text = styled.p<Props>`
margin-top: 0;
color: ${(props) =>
props.type === "secondary"
? props.theme.textSecondary
: props.type === "tertiary"
? props.theme.textTertiary
: props.theme.text};
font-size: ${(props) =>
props.size === "small"
? "14px"
: props.size === "xsmall"
? "13px"
: "inherit"};
white-space: normal;
user-select: none;
`;
export default Text;
+1 -21
View File
@@ -29,27 +29,8 @@ function Toast({ closeAfterMs = 3000, onRequestClose, toast }: Props) {
}
}, [reoccurring]);
const handlePause = React.useCallback(() => {
if (timeout.current) {
clearTimeout(timeout.current);
}
}, []);
const handleResume = React.useCallback(() => {
if (timeout.current) {
timeout.current = setTimeout(
onRequestClose,
toast.timeout || closeAfterMs
);
}
}, [onRequestClose, toast, closeAfterMs]);
return (
<ListItem
$pulse={pulse}
onMouseEnter={handlePause}
onMouseLeave={handleResume}
>
<ListItem $pulse={pulse}>
<Container onClick={action ? undefined : onRequestClose}>
{type === "info" && <InfoIcon color="currentColor" />}
{type === "success" && <CheckboxIcon checked color="currentColor" />}
@@ -107,7 +88,6 @@ const Message = styled.div`
display: inline-block;
font-weight: 500;
padding: 10px 4px;
user-select: none;
`;
export default Toast;
+2 -11
View File
@@ -1,5 +1,4 @@
import {
ArrowIcon,
DocumentIcon,
CloseIcon,
PlusIcon,
@@ -12,7 +11,6 @@ import { EditorView } from "prosemirror-view";
import * as React from "react";
import styled from "styled-components";
import isUrl from "@shared/editor/lib/isUrl";
import { isInternalUrl } from "@shared/utils/urls";
import Flex from "~/components/Flex";
import { Dictionary } from "~/hooks/useDictionary";
import Input from "./Input";
@@ -301,7 +299,6 @@ class LinkEditor extends React.Component<Props, State> {
const looksLikeUrl = value.match(/^https?:\/\//i);
const suggestedLinkTitle = this.suggestedLinkTitle;
const isInternal = isInternalUrl(value);
const showCreateLink =
!!this.props.onCreateLink &&
@@ -327,15 +324,9 @@ class LinkEditor extends React.Component<Props, State> {
autoFocus={this.href === ""}
/>
<Tooltip
tooltip={isInternal ? dictionary.goToLink : dictionary.openLink}
>
<Tooltip tooltip={dictionary.openLink}>
<ToolbarButton onClick={this.handleOpenLink} disabled={!value}>
{isInternal ? (
<ArrowIcon color="currentColor" />
) : (
<OpenIcon color="currentColor" />
)}
<OpenIcon color="currentColor" />
</ToolbarButton>
</Tooltip>
<Tooltip tooltip={dictionary.removeLink}>
+8 -22
View File
@@ -529,16 +529,13 @@ const EditorStyles = styled.div<{
a {
color: ${(props) => props.theme.text};
text-decoration: underline;
text-decoration-color: ${(props) => lighten(0.5, props.theme.text)};
text-decoration-thickness: 1px;
text-underline-offset: .15em;
border-bottom: 1px solid ${(props) => lighten(0.5, props.theme.text)};
text-decoration: none !important;
font-weight: 500;
&:hover {
text-decoration: underline;
text-decoration-color: ${(props) => props.theme.text};
text-decoration-thickness: 1px;
border-bottom: 1px solid ${(props) => props.theme.text};
text-decoration: none;
}
}
}
@@ -548,12 +545,6 @@ const EditorStyles = styled.div<{
cursor: pointer;
}
.ProseMirror-focused {
a {
cursor: text;
}
}
a:hover {
text-decoration: ${(props) => (props.readOnly ? "underline" : "none")};
}
@@ -663,7 +654,7 @@ const EditorStyles = styled.div<{
"%23"
)}' /%3E%3C/svg%3E%0A");`}
&[aria-checked=true] {
&.checked {
opacity: 1;
background-image: ${(props) =>
`url(
@@ -727,11 +718,6 @@ const EditorStyles = styled.div<{
}
}
.external-link {
position: relative;
top: 2px;
}
.code-actions,
.notice-actions {
display: flex;
@@ -1118,19 +1104,19 @@ const EditorStyles = styled.div<{
background: none;
position: absolute;
transition: color 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275),
transform 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275),
opacity 150ms ease-in-out;
outline: none;
border: 0;
padding: 0;
margin-top: 1px;
margin-${(props) => (props.rtl ? "right" : "left")}: -28px;
border-radius: 4px;
margin-${(props) => (props.rtl ? "right" : "left")}: -24px;
&:hover,
&:focus {
cursor: pointer;
transform: scale(1.2);
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.secondaryBackground};
}
}
-1
View File
@@ -50,7 +50,6 @@ export default function useDictionary() {
newLineWithSlash: `${t("Keep typing to filter")}`,
noResults: t("No results"),
openLink: t("Open link"),
goToLink: t("Go to link"),
orderedList: t("Ordered list"),
pageBreak: t("Page break"),
pasteLink: `${t("Paste a link")}`,
-1
View File
@@ -34,7 +34,6 @@ function CollectionGroupMemberMenu({ onMembers, onRemove }: Props) {
{
type: "button",
title: t("Remove"),
dangerous: true,
onClick: onRemove,
},
]}
-1
View File
@@ -171,7 +171,6 @@ function CollectionMenu({
{
type: "button",
title: `${t("Delete")}`,
dangerous: true,
visible: !!(collection && can.delete),
onClick: () => setShowCollectionDelete(true),
icon: <TrashIcon />,
+7 -9
View File
@@ -385,17 +385,9 @@ function DocumentMenu({
visible: !!can.archive,
icon: <ArchiveIcon />,
},
{
type: "button",
title: `${t("Move")}`,
onClick: () => setShowMoveModal(true),
visible: !!can.move,
icon: <MoveIcon />,
},
{
type: "button",
title: `${t("Delete")}`,
dangerous: true,
onClick: () => setShowDeleteModal(true),
visible: !!can.delete,
icon: <TrashIcon />,
@@ -403,11 +395,17 @@ function DocumentMenu({
{
type: "button",
title: `${t("Permanently delete")}`,
dangerous: true,
onClick: () => setShowPermanentDeleteModal(true),
visible: can.permanentDelete,
icon: <CrossIcon />,
},
{
type: "button",
title: `${t("Move")}`,
onClick: () => setShowMoveModal(true),
visible: !!can.move,
icon: <MoveIcon />,
},
{
type: "button",
title: t("Enable embeds"),
-4
View File
@@ -1,4 +1,3 @@
import { DownloadIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
@@ -27,7 +26,6 @@ function FileOperationMenu({ id, onDelete }: Props) {
{
type: "link",
title: t("Download"),
icon: <DownloadIcon />,
href: "/api/fileOperations.redirect?id=" + id,
},
{
@@ -36,8 +34,6 @@ function FileOperationMenu({ id, onDelete }: Props) {
{
type: "button",
title: t("Delete"),
icon: <TrashIcon />,
dangerous: true,
onClick: onDelete,
},
]}
-1
View File
@@ -24,7 +24,6 @@ function GroupMemberMenu({ onRemove }: Props) {
items={[
{
type: "button",
dangerous: true,
title: t("Remove"),
onClick: onRemove,
},
-5
View File
@@ -1,5 +1,4 @@
import { observer } from "mobx-react";
import { EditIcon, GroupIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
@@ -51,7 +50,6 @@ function GroupMenu({ group, onMembers }: Props) {
{
type: "button",
title: `${t("Members")}`,
icon: <GroupIcon />,
onClick: onMembers,
visible: !!(group && can.read),
},
@@ -61,15 +59,12 @@ function GroupMenu({ group, onMembers }: Props) {
{
type: "button",
title: `${t("Edit")}`,
icon: <EditIcon />,
onClick: () => setEditModalOpen(true),
visible: !!(group && can.update),
},
{
type: "button",
title: `${t("Delete")}`,
icon: <TrashIcon />,
dangerous: true,
onClick: () => setDeleteModalOpen(true),
visible: !!(group && can.delete),
},
-1
View File
@@ -24,7 +24,6 @@ function MemberMenu({ onRemove }: Props) {
{
type: "button",
title: t("Remove"),
dangerous: true,
onClick: onRemove,
},
]}
+3 -11
View File
@@ -1,5 +1,4 @@
import { observer } from "mobx-react";
import { ArrowIcon, CopyIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
@@ -63,22 +62,15 @@ function ShareMenu({ share }: Props) {
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
<ContextMenu {...menu} aria-label={t("Share options")}>
<CopyToClipboard text={share.url} onCopy={handleCopy}>
<MenuItem {...menu} icon={<CopyIcon />}>
{t("Copy link")}
</MenuItem>
<MenuItem {...menu}>{t("Copy link")}</MenuItem>
</CopyToClipboard>
<MenuItem {...menu} onClick={handleGoToDocument} icon={<ArrowIcon />}>
<MenuItem {...menu} onClick={handleGoToDocument}>
{t("Go to document")}
</MenuItem>
{can.revoke && (
<>
<hr />
<MenuItem
{...menu}
onClick={handleRevoke}
icon={<TrashIcon />}
dangerous
>
<MenuItem {...menu} onClick={handleRevoke}>
{t("Revoke link")}
</MenuItem>
</>
-2
View File
@@ -157,7 +157,6 @@ function UserMenu({ user }: Props) {
{
type: "button",
title: `${t("Revoke invite")}`,
dangerous: true,
onClick: handleRevoke,
visible: user.isInvited,
},
@@ -170,7 +169,6 @@ function UserMenu({ user }: Props) {
{
type: "button",
title: `${t("Suspend account")}`,
dangerous: true,
onClick: handleSuspend,
visible: !user.isInvited && !user.isSuspended,
},
+1 -4
View File
@@ -66,10 +66,7 @@ export default class Collection extends BaseModel {
@computed
get isEmpty(): boolean {
return (
this.documents.length === 0 &&
this.store.rootStore.documents.inCollection(this.id).length === 0
);
return this.documents.length === 0;
}
@computed
+2 -2
View File
@@ -5,8 +5,8 @@ import Export from "~/scenes/Settings/Export";
import Features from "~/scenes/Settings/Features";
import Groups from "~/scenes/Settings/Groups";
import Import from "~/scenes/Settings/Import";
import Members from "~/scenes/Settings/Members";
import Notifications from "~/scenes/Settings/Notifications";
import People from "~/scenes/Settings/People";
import Profile from "~/scenes/Settings/Profile";
import Security from "~/scenes/Settings/Security";
import Shares from "~/scenes/Settings/Shares";
@@ -24,7 +24,7 @@ export default function SettingsRoutes() {
<Route exact path="/settings" component={Profile} />
<Route exact path="/settings/details" component={Details} />
<Route exact path="/settings/security" component={Security} />
<Route exact path="/settings/members" component={Members} />
<Route exact path="/settings/members" component={People} />
<Route exact path="/settings/features" component={Features} />
<Route exact path="/settings/groups" component={Groups} />
<Route exact path="/settings/shares" component={Shares} />
+3 -3
View File
@@ -2,8 +2,8 @@ import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -50,13 +50,13 @@ function APITokenNew({ onSubmit }: Props) {
return (
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans>
Name your token something that will help you to remember it's use in
the future, for example "local development", "production", or
"continuous integration".
</Trans>
</Text>
</HelpText>
<Flex>
<Input
type="text"
+2 -2
View File
@@ -3,8 +3,8 @@ import * as React from "react";
import Dropzone from "react-dropzone";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import HelpText from "~/components/HelpText";
import LoadingIndicator from "~/components/LoadingIndicator";
import Text from "~/components/Text";
import useImportDocument from "~/hooks/useImportDocument";
import useToasts from "~/hooks/useToasts";
@@ -55,7 +55,7 @@ function DropToImport({ children, disabled, accept, collectionId }: Props) {
);
}
const DropMessage = styled(Text)`
const DropMessage = styled(HelpText)`
opacity: 0;
pointer-events: none;
`;
+3 -3
View File
@@ -8,8 +8,8 @@ import Collection from "~/models/Collection";
import CollectionPermissions from "~/scenes/CollectionPermissions";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Modal from "~/components/Modal";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
@@ -34,7 +34,7 @@ function EmptyCollection({ collection }: Props) {
return (
<Centered column>
<Text type="secondary">
<HelpText>
<Trans
defaults="<em>{{ collectionName }}</em> doesnt contain any
documents yet."
@@ -49,7 +49,7 @@ function EmptyCollection({ collection }: Props) {
{can.createDocument && (
<Trans>Get started by creating a new one!</Trans>
)}
</Text>
</HelpText>
<Empty>
{can.createDocument && (
<Link to={newDocumentPath(collection.id)}>
+5 -5
View File
@@ -5,7 +5,7 @@ import { useHistory } from "react-router-dom";
import Collection from "~/models/Collection";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import HelpText from "~/components/HelpText";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useToasts from "~/hooks/useToasts";
import { homePath } from "~/utils/routeHelpers";
@@ -44,7 +44,7 @@ function CollectionDelete({ collection, onSubmit }: Props) {
return (
<Flex column>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans
defaults="Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash."
values={{
@@ -54,9 +54,9 @@ function CollectionDelete({ collection, onSubmit }: Props) {
em: <strong />,
}}
/>
</Text>
</HelpText>
{team.defaultCollectionId === collection.id ? (
<Text type="secondary">
<HelpText>
<Trans
defaults="Also, <em>{{collectionName}}</em> is being used as the start view deleting it will reset the start view to the Home page."
values={{
@@ -66,7 +66,7 @@ function CollectionDelete({ collection, onSubmit }: Props) {
em: <strong />,
}}
/>
</Text>
</HelpText>
) : null}
<Button type="submit" disabled={isDeleting} autoFocus danger>
{isDeleting ? `${t("Deleting")}` : t("Im sure Delete")}
+3 -3
View File
@@ -6,10 +6,10 @@ import { Trans, useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import IconPicker from "~/components/IconPicker";
import Input from "~/components/Input";
import InputSelect from "~/components/InputSelect";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -85,12 +85,12 @@ const CollectionEdit = ({ collectionId, onSubmit }: Props) => {
return (
<Flex column>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans>
You can edit the name and other details at any time, however doing
so often might confuse your team mates.
</Trans>
</Text>
</HelpText>
<Flex gap={8}>
<Input
type="text"
+3 -3
View File
@@ -4,7 +4,7 @@ import { useTranslation, Trans } from "react-i18next";
import Collection from "~/models/Collection";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import HelpText from "~/components/HelpText";
import useToasts from "~/hooks/useToasts";
type Props = {
@@ -35,7 +35,7 @@ function CollectionExport({ collection, onSubmit }: Props) {
return (
<Flex column>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans
defaults="Exporting the collection <em>{{collectionName}}</em> may take a few seconds. Your documents will be a zip of folders with files in Markdown format. Please visit the Export section on settings to get the zip."
values={{
@@ -45,7 +45,7 @@ function CollectionExport({ collection, onSubmit }: Props) {
em: <strong />,
}}
/>
</Text>
</HelpText>
<Button type="submit" disabled={isLoading} primary>
{isLoading ? `${t("Exporting")}` : t("Export Collection")}
</Button>
+3 -3
View File
@@ -7,11 +7,11 @@ import RootStore from "~/stores/RootStore";
import Collection from "~/models/Collection";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import IconPicker, { icons } from "~/components/IconPicker";
import Input from "~/components/Input";
import InputSelectPermission from "~/components/InputSelectPermission";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import withStores from "~/components/withStores";
import history from "~/utils/history";
@@ -115,13 +115,13 @@ class CollectionNew extends React.Component<Props> {
const teamSharingEnabled = !!auth.team && auth.team.sharing;
return (
<form onSubmit={this.handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans>
Collections are for grouping your documents. They work best when
organized around a topic or internal team Product or Engineering
for example.
</Trans>
</Text>
</HelpText>
<Flex gap={8}>
<Input
type="text"
@@ -13,10 +13,10 @@ import ButtonLink from "~/components/ButtonLink";
import Empty from "~/components/Empty";
import Flex from "~/components/Flex";
import GroupListItem from "~/components/GroupListItem";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Text from "~/components/Text";
import withStores from "~/components/withStores";
type Props = WithTranslation &
@@ -89,13 +89,13 @@ class AddGroupsToCollection extends React.Component<Props> {
return (
<Flex column>
{can.createGroup && (
<Text type="secondary">
<HelpText>
{t("Cant find the group youre looking for?")}{" "}
<ButtonLink onClick={this.handleNewGroupModalOpen}>
{t("Create a group")}
</ButtonLink>
.
</Text>
</HelpText>
)}
<Input
@@ -10,10 +10,10 @@ import Invite from "~/scenes/Invite";
import ButtonLink from "~/components/ButtonLink";
import Empty from "~/components/Empty";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Text from "~/components/Text";
import withStores from "~/components/withStores";
import MemberListItem from "./components/MemberListItem";
@@ -83,7 +83,7 @@ class AddPeopleToCollection extends React.Component<Props> {
return (
<Flex column>
<Text type="secondary">
<HelpText>
{t("Need to add someone whos not yet on the team yet?")}{" "}
<ButtonLink onClick={this.handleInviteModalOpen}>
{t("Invite people to {{ teamName }}", {
@@ -91,7 +91,7 @@ class AddPeopleToCollection extends React.Component<Props> {
})}
</ButtonLink>
.
</Text>
</HelpText>
<Input
type="search"
+4 -4
View File
@@ -7,12 +7,12 @@ import Collection from "~/models/Collection";
import Button from "~/components/Button";
import Divider from "~/components/Divider";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import InputSelectPermission from "~/components/InputSelectPermission";
import Labeled from "~/components/Labeled";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
@@ -205,7 +205,7 @@ function CollectionPermissions({ collection }: Props) {
value={collection.permission || ""}
nude
/>
<PermissionExplainer size="small">
<PermissionExplainer small>
{!collection.permission && (
<Trans
defaults="The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default."
@@ -349,11 +349,11 @@ function CollectionPermissions({ collection }: Props) {
);
}
const Empty = styled(Text)`
const Empty = styled(HelpText)`
margin-top: 8px;
`;
const PermissionExplainer = styled(Text)`
const PermissionExplainer = styled(HelpText)`
margin-top: -8px;
margin-bottom: 24px;
`;
+2 -2
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Text from "~/components/Text";
import HelpText from "~/components/HelpText";
import useWindowScrollPosition from "~/hooks/useWindowScrollPosition";
const HEADING_OFFSET = 20;
@@ -111,7 +111,7 @@ const Heading = styled.h3`
letter-spacing: 0.04em;
`;
const Empty = styled(Text)`
const Empty = styled(HelpText)`
margin: 1em 0 4em;
padding-right: 2em;
font-size: 14px;
@@ -6,7 +6,6 @@ import { observer } from "mobx-react";
import * as React from "react";
import { RouteComponentProps, StaticContext } from "react-router";
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import { isInternalUrl } from "@shared/utils/urls";
import RootStore from "~/stores/RootStore";
import Document from "~/models/Document";
import Revision from "~/models/Revision";
@@ -18,6 +17,7 @@ import { NavigationNode } from "~/types";
import { NotFoundError, OfflineError } from "~/utils/errors";
import history from "~/utils/history";
import { matchDocumentEdit } from "~/utils/routeHelpers";
import { isInternalUrl } from "~/utils/urls";
import HideSidebar from "./HideSidebar";
import Loading from "./Loading";
+59 -4
View File
@@ -1,9 +1,10 @@
import { debounce } from "lodash";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import { InputIcon } from "outline-icons";
import { AllSelection } from "prosemirror-state";
import * as React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { WithTranslation, Trans, withTranslation } from "react-i18next";
import {
Prompt,
Route,
@@ -25,9 +26,11 @@ import ErrorBoundary from "~/components/ErrorBoundary";
import Flex from "~/components/Flex";
import LoadingIndicator from "~/components/LoadingIndicator";
import Modal from "~/components/Modal";
import Notice from "~/components/Notice";
import PageTitle from "~/components/PageTitle";
import PlaceholderDocument from "~/components/PlaceholderDocument";
import RegisterKeyDown from "~/components/RegisterKeyDown";
import Time from "~/components/Time";
import withStores from "~/components/withStores";
import { NavigationNode } from "~/types";
import { client } from "~/utils/ApiClient";
@@ -47,7 +50,6 @@ import Editor from "./Editor";
import Header from "./Header";
import KeyboardShortcutsButton from "./KeyboardShortcutsButton";
import MarkAsViewed from "./MarkAsViewed";
import Notices from "./Notices";
import PublicReferences from "./PublicReferences";
import References from "./References";
@@ -532,7 +534,52 @@ class DocumentScene extends React.Component<Props> {
column
auto
>
<Notices document={document} readOnly={readOnly} />
{document.isTemplate && !readOnly && (
<Notice>
<Trans>
Youre editing a template. Highlight some text and use the{" "}
<PlaceholderIcon color="currentColor" /> control to add
placeholders that can be filled out when creating new
documents from this template.
</Trans>
</Notice>
)}
{document.archivedAt && !document.deletedAt && (
<Notice>
{t("Archived by {{userName}}", {
userName: document.updatedBy.name,
})}{" "}
<Time dateTime={document.updatedAt} addSuffix />
</Notice>
)}
{document.deletedAt && (
<Notice>
<strong>
{t("Deleted by {{userName}}", {
userName: document.updatedBy.name,
})}{" "}
<Time dateTime={document.deletedAt || ""} addSuffix />
</strong>
{document.permanentlyDeletedAt && (
<>
<br />
{document.template ? (
<Trans>
This template will be permanently deleted in{" "}
<Time dateTime={document.permanentlyDeletedAt} />{" "}
unless restored.
</Trans>
) : (
<Trans>
This document will be permanently deleted in{" "}
<Time dateTime={document.permanentlyDeletedAt} />{" "}
unless restored.
</Trans>
)}
</>
)}
</Notice>
)}
<React.Suspense fallback={<PlaceholderDocument />}>
<Flex auto={!readOnly}>
{showContents && (
@@ -544,7 +591,7 @@ class DocumentScene extends React.Component<Props> {
<Editor
id={document.id}
key={embedsDisabled ? "disabled" : "enabled"}
ref={this.editor}
innerRef={this.editor}
multiplayer={collaborativeEditing}
shareId={shareId}
isDraft={document.isDraft}
@@ -604,6 +651,11 @@ class DocumentScene extends React.Component<Props> {
}
}
const PlaceholderIcon = styled(InputIcon)`
position: relative;
top: 6px;
`;
const Background = styled(Container)`
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
@@ -625,6 +677,9 @@ type MaxWidthProps = {
};
const MaxWidth = styled(Flex)<MaxWidthProps>`
${(props) =>
props.archived && `* { color: ${props.theme.textSecondary} !important; } `};
// Adds space to the gutter to make room for heading annotations
padding: 0 32px;
transition: padding 100ms;
@@ -1,5 +1,6 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { MAX_TITLE_LENGTH } from "@shared/constants";
@@ -12,7 +13,6 @@ import { isModKey } from "~/utils/keyboard";
type Props = {
value: string;
placeholder: string;
document: Document;
/** Should the title be editable, policies will also be considered separately */
readOnly?: boolean;
@@ -39,11 +39,11 @@ const EditableTitle = React.forwardRef(
onSave,
onGoToNextInput,
starrable,
placeholder,
}: Props,
ref: React.RefObject<HTMLSpanElement>
) => {
const { policies } = useStores();
const { t } = useTranslation();
const can = policies.abilities(document.id);
const normalizedTitle =
!value && readOnly ? document.titleWithDefault : value;
@@ -131,7 +131,11 @@ const EditableTitle = React.forwardRef(
onChange={onChange}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder={placeholder}
placeholder={
document.isTemplate
? t("Start your template…")
: t("Start with a title…")
}
value={normalizedTitle}
$emojiWidth={emojiWidth}
$isStarred={document.isStarred}
+116 -118
View File
@@ -1,146 +1,144 @@
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useRouteMatch } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import Document from "~/models/Document";
import ClickablePadding from "~/components/ClickablePadding";
import DocumentMetaWithViews from "~/components/DocumentMetaWithViews";
import Editor, { Props as EditorProps } from "~/components/Editor";
import Flex from "~/components/Flex";
import HoverPreview from "~/components/HoverPreview";
import {
documentHistoryUrl,
documentUrl,
matchDocumentHistory,
} from "~/utils/routeHelpers";
import { documentHistoryUrl } from "~/utils/routeHelpers";
import MultiplayerEditor from "./AsyncMultiplayerEditor";
import EditableTitle from "./EditableTitle";
type Props = EditorProps & {
onChangeTitle: (text: string) => void;
title: string;
id: string;
document: Document;
isDraft: boolean;
multiplayer?: boolean;
onSave: (options: {
done?: boolean;
autosave?: boolean;
publish?: boolean;
}) => void;
children: React.ReactNode;
};
type Props = EditorProps &
WithTranslation & {
onChangeTitle: (text: string) => void;
title: string;
id: string;
document: Document;
isDraft: boolean;
multiplayer?: boolean;
onSave: (arg0: {
done?: boolean;
autosave?: boolean;
publish?: boolean;
}) => any;
innerRef: {
current: any;
};
children: React.ReactNode;
};
/**
* The main document editor includes an editable title with metadata below it,
* and support for hover previews of internal links.
*/
function DocumentEditor(props: Props, ref: React.RefObject<any>) {
const [
activeLinkEvent,
setActiveLinkEvent,
] = React.useState<MouseEvent | null>(null);
const titleRef = React.useRef<HTMLSpanElement>(null);
const { t } = useTranslation();
const match = useRouteMatch();
@observer
class DocumentEditor extends React.Component<Props> {
@observable
activeLinkEvent: MouseEvent | null | undefined;
const focusAtStart = React.useCallback(() => {
if (ref.current) {
ref.current.focusAtStart();
ref = React.createRef<HTMLDivElement | HTMLInputElement>();
titleRef = React.createRef<HTMLSpanElement>();
focusAtStart = () => {
if (this.props.innerRef.current) {
this.props.innerRef.current.focusAtStart();
}
}, [ref]);
};
const focusAtEnd = React.useCallback(() => {
if (ref.current) {
ref.current.focusAtEnd();
focusAtEnd = () => {
if (this.props.innerRef.current) {
this.props.innerRef.current.focusAtEnd();
}
}, [ref]);
};
const handleLinkActive = React.useCallback((event: MouseEvent) => {
setActiveLinkEvent(event);
insertParagraph = () => {
if (this.props.innerRef.current) {
const { view } = this.props.innerRef.current;
const { dispatch, state } = view;
dispatch(state.tr.insert(0, state.schema.nodes.paragraph.create()));
}
};
handleLinkActive = (event: MouseEvent) => {
this.activeLinkEvent = event;
return false;
}, []);
};
const handleLinkInactive = React.useCallback(() => {
setActiveLinkEvent(null);
}, []);
handleLinkInactive = () => {
this.activeLinkEvent = null;
};
const handleGoToNextInput = React.useCallback(
(insertParagraph: boolean) => {
if (insertParagraph && ref.current) {
const { view } = ref.current;
const { dispatch, state } = view;
dispatch(state.tr.insert(0, state.schema.nodes.paragraph.create()));
}
handleGoToNextInput = (insertParagraph: boolean) => {
if (insertParagraph) {
this.insertParagraph();
}
focusAtStart();
},
[focusAtStart, ref]
);
this.focusAtStart();
};
const {
document,
title,
onChangeTitle,
isDraft,
shareId,
readOnly,
children,
multiplayer,
...rest
} = props;
const EditorComponent = multiplayer ? MultiplayerEditor : Editor;
render() {
const {
document,
title,
onChangeTitle,
isDraft,
shareId,
readOnly,
innerRef,
children,
multiplayer,
t,
...rest
} = this.props;
const EditorComponent = multiplayer ? MultiplayerEditor : Editor;
return (
<Flex auto column>
<EditableTitle
ref={titleRef}
value={title}
readOnly={readOnly}
document={document}
onGoToNextInput={handleGoToNextInput}
onChange={onChangeTitle}
starrable={!shareId}
placeholder={t("Untitled")}
/>
{!shareId && (
<DocumentMetaWithViews
isDraft={isDraft}
return (
<Flex auto column>
<EditableTitle
ref={this.titleRef}
value={title}
readOnly={readOnly}
document={document}
to={
match.path === matchDocumentHistory
? documentUrl(document)
: documentHistoryUrl(document)
}
rtl={
titleRef.current
? window.getComputedStyle(titleRef.current).direction === "rtl"
: false
}
onGoToNextInput={this.handleGoToNextInput}
onChange={onChangeTitle}
starrable={!shareId}
/>
)}
<EditorComponent
ref={ref}
autoFocus={!!title && !props.defaultValue}
placeholder={t("Type '/' to insert, or start writing…")}
onHoverLink={handleLinkActive}
scrollTo={window.location.hash}
readOnly={readOnly}
shareId={shareId}
grow
{...rest}
/>
{!readOnly && <ClickablePadding onClick={focusAtEnd} grow />}
{activeLinkEvent && !shareId && (
<HoverPreview
node={activeLinkEvent.target as HTMLAnchorElement}
event={activeLinkEvent}
onClose={handleLinkInactive}
{!shareId && (
<DocumentMetaWithViews
isDraft={isDraft}
document={document}
to={documentHistoryUrl(document)}
rtl={
this.titleRef.current
? window.getComputedStyle(this.titleRef.current).direction ===
"rtl"
: false
}
/>
)}
<EditorComponent
ref={innerRef}
autoFocus={!!title && !this.props.defaultValue}
placeholder={t("…the rest is up to you")}
onHoverLink={this.handleLinkActive}
scrollTo={window.location.hash}
readOnly={readOnly}
shareId={shareId}
grow
{...rest}
/>
)}
{children}
</Flex>
);
{!readOnly && <ClickablePadding onClick={this.focusAtEnd} grow />}
{this.activeLinkEvent && !shareId && (
<HoverPreview
node={this.activeLinkEvent.target as HTMLAnchorElement}
event={this.activeLinkEvent}
onClose={this.handleLinkInactive}
/>
)}
{children}
</Flex>
);
}
}
export default observer(React.forwardRef(DocumentEditor));
export default withTranslation()(DocumentEditor);
@@ -1,80 +0,0 @@
import { TrashIcon, ArchiveIcon, ShapesIcon, InputIcon } from "outline-icons";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import Document from "~/models/Document";
import Notice from "~/components/Notice";
import Time from "~/components/Time";
type Props = {
document: Document;
readOnly: boolean;
};
export default function Notices({ document, readOnly }: Props) {
const { t } = useTranslation();
function permanentlyDeletedDescription() {
if (!document.permanentlyDeletedAt) {
return;
}
return document.template ? (
<Trans>
This template will be permanently deleted in{" "}
<Time dateTime={document.permanentlyDeletedAt} /> unless restored.
</Trans>
) : (
<Trans>
This document will be permanently deleted in{" "}
<Time dateTime={document.permanentlyDeletedAt} /> unless restored.
</Trans>
);
}
return (
<>
{document.isTemplate && !readOnly && (
<Notice
icon={<ShapesIcon />}
description={
<Trans>
Highlight some text and use the{" "}
<PlaceholderIcon color="currentColor" /> control to add
placeholders that can be filled out when creating new documents
</Trans>
}
>
{t("Youre editing a template")}
</Notice>
)}
{document.archivedAt && !document.deletedAt && (
<Notice icon={<ArchiveIcon />}>
{t("Archived by {{userName}}", {
userName: document.updatedBy.name,
})}
&nbsp;
<Time dateTime={document.updatedAt} addSuffix />
</Notice>
)}
{document.deletedAt && (
<Notice
icon={<TrashIcon />}
description={permanentlyDeletedDescription()}
>
{t("Deleted by {{userName}}", {
userName: document.updatedBy.name,
})}
&nbsp;
<Time dateTime={document.deletedAt} addSuffix />
</Notice>
)}
</>
);
}
const PlaceholderIcon = styled(InputIcon)`
position: relative;
top: 6px;
margin-top: -6px;
`;
@@ -26,7 +26,6 @@ function ShareButton({ document }: Props) {
const popover = usePopoverState({
gutter: 0,
placement: "bottom-end",
unstable_fixed: true,
});
return (
@@ -10,10 +10,10 @@ import Share from "~/models/Share";
import Button from "~/components/Button";
import CopyToClipboard from "~/components/CopyToClipboard";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import Notice from "~/components/Notice";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useKeyDown from "~/hooks/useKeyDown";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -164,9 +164,7 @@ function SharePopover({
</SwitchLabel>
</SwitchWrapper>
) : (
<Text type="secondary">
{t("Only team members with permission can view")}
</Text>
<HelpText>{t("Only team members with permission can view")}</HelpText>
)}
{canPublish && share?.published && !document.isDraft && (
@@ -233,7 +231,7 @@ const SwitchLabel = styled(Flex)`
}
`;
const SwitchText = styled(Text)`
const SwitchText = styled(HelpText)`
margin: 0;
font-size: 15px;
`;
+5 -5
View File
@@ -5,7 +5,7 @@ import { useHistory } from "react-router-dom";
import Document from "~/models/Document";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import HelpText from "~/components/HelpText";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import { collectionUrl, documentUrl } from "~/utils/routeHelpers";
@@ -83,7 +83,7 @@ function DocumentDelete({ document, onSubmit }: Props) {
return (
<Flex column>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
{document.isTemplate ? (
<Trans
defaults="Are you sure you want to delete the <em>{{ documentTitle }}</em> template?"
@@ -105,9 +105,9 @@ function DocumentDelete({ document, onSubmit }: Props) {
}}
/>
)}
</Text>
</HelpText>
{canArchive && (
<Text type="secondary">
<HelpText>
<Trans>
If youd like the option of referencing or restoring the{" "}
{{
@@ -115,7 +115,7 @@ function DocumentDelete({ document, onSubmit }: Props) {
}}{" "}
in the future, consider archiving it instead.
</Trans>
</Text>
</HelpText>
)}
<Button type="submit" danger>
{isDeleting ? `${t("Deleting")}` : t("Im sure Delete")}
+3 -3
View File
@@ -5,7 +5,7 @@ import { useHistory } from "react-router-dom";
import Document from "~/models/Document";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import HelpText from "~/components/HelpText";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -49,7 +49,7 @@ function DocumentPermanentDelete({ document, onSubmit }: Props) {
return (
<Flex column>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans
defaults="Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone."
values={{
@@ -59,7 +59,7 @@ function DocumentPermanentDelete({ document, onSubmit }: Props) {
em: <strong />,
}}
/>
</Text>
</HelpText>
<Button type="submit" danger>
{isDeleting ? `${t("Deleting")}` : t("Im sure Delete")}
</Button>
+3 -3
View File
@@ -5,7 +5,7 @@ import { Trans, useTranslation } from "react-i18next";
import Collection from "~/models/Collection";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import HelpText from "~/components/HelpText";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import { NavigationNode } from "~/types";
@@ -68,7 +68,7 @@ function DocumentReparent({ collection, item, onSubmit, onCancel }: Props) {
return (
<Flex column>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans
defaults="Heads up moving the document <em>{{ title }}</em> to the <em>{{ newCollectionName }}</em> collection will grant all team members <em>{{ newPermission }}</em>, they currently have {{ prevPermission }}."
values={{
@@ -83,7 +83,7 @@ function DocumentReparent({ collection, item, onSubmit, onCancel }: Props) {
em: <strong />,
}}
/>
</Text>
</HelpText>
<Button type="submit">
{isSaving ? `${t("Moving")}` : t("Move document")}
</Button>{" "}
+3 -3
View File
@@ -6,7 +6,7 @@ import { useTranslation, Trans } from "react-i18next";
import { useHistory } from "react-router-dom";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import HelpText from "~/components/HelpText";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import { documentUrl } from "~/utils/routeHelpers";
@@ -56,7 +56,7 @@ function DocumentTemplatize({ documentId, onSubmit }: Props) {
return (
<Flex column>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans
defaults="Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents."
values={{
@@ -66,7 +66,7 @@ function DocumentTemplatize({ documentId, onSubmit }: Props) {
em: <strong />,
}}
/>
</Text>
</HelpText>
<Button type="submit">
{isSaving ? `${t("Creating")}` : t("Create template")}
</Button>
+3 -3
View File
@@ -5,7 +5,7 @@ import { useHistory } from "react-router-dom";
import Group from "~/models/Group";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import HelpText from "~/components/HelpText";
import useToasts from "~/hooks/useToasts";
import { groupSettingsPath } from "~/utils/routeHelpers";
@@ -40,7 +40,7 @@ function GroupDelete({ group, onSubmit }: Props) {
return (
<Flex column>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans
defaults="Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with."
values={{
@@ -50,7 +50,7 @@ function GroupDelete({ group, onSubmit }: Props) {
em: <strong />,
}}
/>
</Text>
</HelpText>
<Button type="submit" danger>
{isDeleting ? `${t("Deleting")}` : t("Im sure Delete")}
</Button>
+3 -3
View File
@@ -4,8 +4,8 @@ import { useTranslation, Trans } from "react-i18next";
import Group from "~/models/Group";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import Text from "~/components/Text";
import useToasts from "~/hooks/useToasts";
type Props = {
@@ -48,12 +48,12 @@ function GroupEdit({ group, onSubmit }: Props) {
return (
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans>
You can edit the name of this group at any time, however doing so too
often might confuse your team mates.
</Trans>
</Text>
</HelpText>
<Flex>
<Input
type="text"
+3 -3
View File
@@ -10,10 +10,10 @@ import Invite from "~/scenes/Invite";
import ButtonLink from "~/components/ButtonLink";
import Empty from "~/components/Empty";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Text from "~/components/Text";
import withStores from "~/components/withStores";
import GroupMemberListItem from "./components/GroupMemberListItem";
@@ -82,7 +82,7 @@ class AddPeopleToGroup extends React.Component<Props> {
return (
<Flex column>
<Text type="secondary">
<HelpText>
{t(
"Add team members below to give them access to the group. Need to add someone whos not yet on the team yet?"
)}{" "}
@@ -92,7 +92,7 @@ class AddPeopleToGroup extends React.Component<Props> {
})}
</ButtonLink>
.
</Text>
</HelpText>
<Input
type="search"
placeholder={`${t("Search by name")}`}
+5 -5
View File
@@ -7,10 +7,10 @@ import User from "~/models/User";
import Button from "~/components/Button";
import Empty from "~/components/Empty";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Subheading from "~/components/Subheading";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import AddPeopleToGroup from "./AddPeopleToGroup";
@@ -56,7 +56,7 @@ function GroupMembers({ group }: Props) {
<Flex column>
{can.update ? (
<>
<Text type="secondary">
<HelpText>
<Trans
defaults="Add and remove team members in the <em>{{groupName}}</em> group. Adding people to the group will give them access to any collections this group has been added to."
values={{
@@ -66,7 +66,7 @@ function GroupMembers({ group }: Props) {
em: <strong />,
}}
/>
</Text>
</HelpText>
<span>
<Button
type="button"
@@ -79,7 +79,7 @@ function GroupMembers({ group }: Props) {
</span>
</>
) : (
<Text type="secondary">
<HelpText>
<Trans
defaults="Listing team members in the <em>{{groupName}}</em> group."
values={{
@@ -89,7 +89,7 @@ function GroupMembers({ group }: Props) {
em: <strong />,
}}
/>
</Text>
</HelpText>
)}
<Subheading>
+5 -5
View File
@@ -5,9 +5,9 @@ import Group from "~/models/Group";
import GroupMembers from "~/scenes/GroupMembers";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import Modal from "~/components/Modal";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -52,13 +52,13 @@ function GroupNew({ onSubmit }: Props) {
return (
<>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans>
Groups are for organizing your team. They work best when centered
around a function or a responsibility Support or Engineering for
example.
</Trans>
</Text>
</HelpText>
<Flex>
<Input
type="text"
@@ -70,9 +70,9 @@ function GroupNew({ onSubmit }: Props) {
flex
/>
</Flex>
<Text type="secondary">
<HelpText>
<Trans>Youll be able to add people to the group next.</Trans>
</Text>
</HelpText>
<Button type="submit" disabled={isSaving || !name}>
{isSaving ? `${t("Creating")}` : t("Continue")}
+5 -5
View File
@@ -8,10 +8,10 @@ import { Role } from "@shared/types";
import Button from "~/components/Button";
import CopyToClipboard from "~/components/CopyToClipboard";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import InputSelectRole from "~/components/InputSelectRole";
import NudeButton from "~/components/NudeButton";
import Text from "~/components/Text";
import Tooltip from "~/components/Tooltip";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
@@ -141,16 +141,16 @@ function Invite({ onSubmit }: Props) {
return (
<form onSubmit={handleSubmit}>
{team.guestSignin ? (
<Text type="secondary">
<HelpText>
<Trans
defaults="Invite team members or guests to join your knowledge base. Team members can sign in with {{signinMethods}} or use their email address."
values={{
signinMethods: team.signinMethods,
}}
/>
</Text>
</HelpText>
) : (
<Text type="secondary">
<HelpText>
<Trans
defaults="Invite team members to join your knowledge base. They will need to sign in with {{signinMethods}}."
values={{
@@ -163,7 +163,7 @@ function Invite({ onSubmit }: Props) {
<Link to="/settings/security">enable email sign-in</Link>.
</Trans>
)}
</Text>
</HelpText>
)}
{team.subdomain && (
<CopyBlock>
+26 -29
View File
@@ -4,8 +4,7 @@ import styled from "styled-components";
import Flex from "~/components/Flex";
import InputSearch from "~/components/InputSearch";
import Key from "~/components/Key";
import { isMac } from "~/utils/browser";
import { metaDisplay, altDisplay } from "~/utils/keyboard";
import { metaDisplay } from "~/utils/keyboard";
function KeyboardShortcuts() {
const { t } = useTranslation();
@@ -45,7 +44,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key symbol>{altDisplay}</Key> + <Key>h</Key>
<Key>Ctrl</Key> + <Key>Alt</Key> + <Key>h</Key>
</>
),
label: t("Table of contents"),
@@ -53,7 +52,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key>.</Key>
<Key>{metaDisplay}</Key> + <Key>.</Key>
</>
),
label: t("Toggle navigation"),
@@ -61,7 +60,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key>f</Key>
<Key>{metaDisplay}</Key> + <Key>f</Key>
</>
),
label: t("Focus search input"),
@@ -73,7 +72,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key>Enter</Key>
<Key>{metaDisplay}</Key> + <Key>Enter</Key>
</>
),
label: t("Save document and exit"),
@@ -81,8 +80,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key symbol></Key> +{" "}
<Key>p</Key>
<Key>{metaDisplay}</Key> + <Key></Key> + <Key>p</Key>
</>
),
label: t("Publish document and exit"),
@@ -90,7 +88,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key>s</Key>
<Key>{metaDisplay}</Key> + <Key>s</Key>
</>
),
label: t("Save document"),
@@ -98,7 +96,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{isMac() ? metaDisplay : "⇧"}</Key> + <Key>Esc</Key>
<Key>{metaDisplay}</Key> + <Key>Esc</Key>
</>
),
label: t("Cancel editing"),
@@ -111,7 +109,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key symbol></Key> + <Key>0</Key>
<Key>Ctrl</Key> + <Key></Key> + <Key>0</Key>
</>
),
label: t("Paragraph"),
@@ -119,7 +117,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key symbol></Key> + <Key>1</Key>
<Key>Ctrl</Key> + <Key></Key> + <Key>1</Key>
</>
),
label: t("Large header"),
@@ -127,7 +125,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key symbol></Key> + <Key>2</Key>
<Key>Ctrl</Key> + <Key></Key> + <Key>2</Key>
</>
),
label: t("Medium header"),
@@ -135,7 +133,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key symbol></Key> + <Key>3</Key>
<Key>Ctrl</Key> + <Key></Key> + <Key>3</Key>
</>
),
label: t("Small header"),
@@ -143,7 +141,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key symbol></Key> + <Key>\</Key>
<Key>Ctrl</Key> + <Key></Key> + <Key>\</Key>
</>
),
label: t("Code block"),
@@ -151,7 +149,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key>b</Key>
<Key>{metaDisplay}</Key> + <Key>b</Key>
</>
),
label: t("Bold"),
@@ -159,7 +157,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key>i</Key>
<Key>{metaDisplay}</Key> + <Key>i</Key>
</>
),
label: t("Italic"),
@@ -167,7 +165,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key>u</Key>
<Key>{metaDisplay}</Key> + <Key>u</Key>
</>
),
label: t("Underline"),
@@ -175,7 +173,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key>d</Key>
<Key>{metaDisplay}</Key> + <Key>d</Key>
</>
),
label: t("Strikethrough"),
@@ -183,7 +181,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key>k</Key>
<Key>{metaDisplay}</Key> + <Key>k</Key>
</>
),
label: t("Link"),
@@ -191,7 +189,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key>z</Key>
<Key>{metaDisplay}</Key> + <Key>z</Key>
</>
),
label: t("Undo"),
@@ -199,8 +197,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{metaDisplay}</Key> + <Key symbol></Key> +{" "}
<Key>z</Key>
<Key>{metaDisplay}</Key> + <Key></Key> + <Key>z</Key>
</>
),
label: t("Redo"),
@@ -213,7 +210,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key symbol></Key> + <Key>7</Key>
<Key>Ctrl</Key> + <Key></Key> + <Key>7</Key>
</>
),
label: t("Todo list"),
@@ -221,7 +218,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key symbol></Key> + <Key>8</Key>
<Key>Ctrl</Key> + <Key></Key> + <Key>8</Key>
</>
),
label: t("Bulleted list"),
@@ -229,7 +226,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key symbol></Key> + <Key>9</Key>
<Key>Ctrl</Key> + <Key></Key> + <Key>9</Key>
</>
),
label: t("Ordered list"),
@@ -241,7 +238,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol></Key> + <Key>Tab</Key>
<Key></Key> + <Key>Tab</Key>
</>
),
label: t("Outdent list item"),
@@ -249,7 +246,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{altDisplay}</Key> + <Key symbol></Key>
<Key>Alt</Key> + <Key></Key>
</>
),
label: t("Move list item up"),
@@ -257,7 +254,7 @@ function KeyboardShortcuts() {
{
shortcut: (
<>
<Key symbol>{altDisplay}</Key> + <Key symbol></Key>
<Key>Alt</Key> + <Key></Key>
</>
),
label: t("Move list item down"),
+3 -3
View File
@@ -11,10 +11,10 @@ import ButtonLarge from "~/components/ButtonLarge";
import Fade from "~/components/Fade";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import OutlineLogo from "~/components/OutlineLogo";
import PageTitle from "~/components/PageTitle";
import TeamLogo from "~/components/TeamLogo";
import Text from "~/components/Text";
import env from "~/env";
import useQuery from "~/hooks/useQuery";
import useStores from "~/hooks/useStores";
@@ -221,12 +221,12 @@ const Logo = styled.div`
height: 38px;
`;
const GetStarted = styled(Text)`
const GetStarted = styled(HelpText)`
text-align: center;
margin-top: -12px;
`;
const Note = styled(Text)`
const Note = styled(HelpText)`
text-align: center;
font-size: 14px;
+4 -4
View File
@@ -18,10 +18,10 @@ import DocumentListItem from "~/components/DocumentListItem";
import Empty from "~/components/Empty";
import Fade from "~/components/Fade";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import LoadingIndicator from "~/components/LoadingIndicator";
import RegisterKeyDown from "~/components/RegisterKeyDown";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import withStores from "~/components/withStores";
import { searchUrl } from "~/utils/routeHelpers";
import { decodeURIComponentSafe } from "~/utils/urls";
@@ -196,7 +196,7 @@ class Search extends React.Component<Props> {
@action
fetchResults = async () => {
if (this.query.trim()) {
if (this.query) {
const params = {
offset: this.offset,
limit: DEFAULT_PAGINATION_LIMIT,
@@ -321,9 +321,9 @@ class Search extends React.Component<Props> {
{showEmpty && (
<Fade>
<Centered column>
<Text type="secondary">
<HelpText>
<Trans>No documents found for your search filters.</Trans>
</Text>
</HelpText>
</Centered>
</Fade>
)}
+5 -5
View File
@@ -6,9 +6,9 @@ import { useTranslation, Trans } from "react-i18next";
import Button from "~/components/Button";
import DefaultCollectionInputSelect from "~/components/DefaultCollectionInputSelect";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
@@ -94,12 +94,12 @@ function Details() {
return (
<Scene title={t("Details")} icon={<TeamIcon color="currentColor" />}>
<Heading>{t("Details")}</Heading>
<Text type="secondary">
<HelpText>
<Trans>
These details affect the way that your Outline appears to everyone on
the team.
</Trans>
</Text>
</HelpText>
<ImageInput
label={t("Logo")}
@@ -132,10 +132,10 @@ function Details() {
short
/>
{subdomain && (
<Text type="secondary" size="small">
<HelpText small>
<Trans>Your knowledge base will be accessible at</Trans>{" "}
<strong>{subdomain}.getoutline.com</strong>
</Text>
</HelpText>
)}
</>
)}
+3 -3
View File
@@ -5,10 +5,10 @@ import { useTranslation, Trans } from "react-i18next";
import FileOperation from "~/models/FileOperation";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
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";
import useToasts from "~/hooks/useToasts";
@@ -55,7 +55,7 @@ function Export() {
return (
<Scene title={t("Export")} icon={<DownloadIcon color="currentColor" />}>
<Heading>{t("Export")}</Heading>
<Text type="secondary">
<HelpText>
<Trans
defaults="A full export might take some time, consider exporting a single document or collection. The exported data is a zip of your documents in Markdown format. You may leave this page once the export has started we will email a link to <em>{{ userEmail }}</em> when its complete."
values={{
@@ -65,7 +65,7 @@ function Export() {
em: <strong />,
}}
/>
</Text>
</HelpText>
<Button
type="submit"
onClick={handleExport}
+3 -3
View File
@@ -4,9 +4,9 @@ import { useState } from "react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Scene from "~/components/Scene";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -38,12 +38,12 @@ function Features() {
<Heading>
<Trans>Features</Trans>
</Heading>
<Text type="secondary">
<HelpText>
<Trans>
Manage optional and beta features. Changing these settings will affect
the experience for all team members.
</Trans>
</Text>
</HelpText>
<Switch
label={t("Collaborative editing")}
name="collaborativeEditing"
+3 -3
View File
@@ -8,11 +8,11 @@ import Button from "~/components/Button";
import Empty from "~/components/Empty";
import GroupListItem from "~/components/GroupListItem";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
@@ -50,11 +50,11 @@ function Groups() {
}
>
<Heading>{t("Groups")}</Heading>
<Text type="secondary">
<HelpText>
<Trans>
Groups can be used to organize and manage the people on your team.
</Trans>
</Text>
</HelpText>
<Subheading>{t("All groups")}</Subheading>
<PaginatedList
items={groups.orderedData}
+3 -3
View File
@@ -8,12 +8,12 @@ import getDataTransferFiles from "@shared/utils/getDataTransferFiles";
import { cdnPath } from "@shared/utils/urls";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
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";
import { uploadFile } from "~/utils/uploadFile";
@@ -73,13 +73,13 @@ function Import() {
return (
<Scene title={t("Import")} icon={<NewDocumentIcon color="currentColor" />}>
<Heading>{t("Import")}</Heading>
<Text type="secondary">
<HelpText>
<Trans>
Quickly transfer your existing documents, pages, and files from other
tools and services into Outline. You can also drag and drop any HTML,
Markdown, and text documents directly into Collections in the app.
</Trans>
</Text>
</HelpText>
<VisuallyHidden>
<input
type="file"
+3 -3
View File
@@ -5,11 +5,11 @@ import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import Notice from "~/components/Notice";
import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import Text from "~/components/Text";
import env from "~/env";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
@@ -100,12 +100,12 @@ function Notifications() {
</Trans>
</Notice>
)}
<Text type="secondary">
<HelpText>
<Trans>
Manage when and where you receive email notifications from Outline.
Your email address can be updated in your SSO provider.
</Trans>
</Text>
</HelpText>
{env.EMAIL_ENABLED ? (
<>
@@ -5,7 +5,6 @@ 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";
import Invite from "~/scenes/Invite";
@@ -13,10 +12,10 @@ import { Action } from "~/components/Actions";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import InputSearch from "~/components/InputSearch";
import Modal from "~/components/Modal";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useQuery from "~/hooks/useQuery";
@@ -24,7 +23,7 @@ import useStores from "~/hooks/useStores";
import PeopleTable from "./components/PeopleTable";
import UserStatusFilter from "./components/UserStatusFilter";
function Members() {
function People() {
const topRef = React.useRef();
const location = useLocation();
const history = useHistory();
@@ -210,13 +209,13 @@ function Members() {
}
>
<Heading>{t("Members")}</Heading>
<Text type="secondary">
<HelpText>
<Trans>
Everyone that has signed into Outline appears here. Its possible that
there are other users who have access through {team.signinMethods} but
havent signed in yet.
</Trans>
</Text>
</HelpText>
<Flex gap={8}>
<InputSearch
short
@@ -224,7 +223,7 @@ function Members() {
placeholder={`${t("Filter")}`}
onChange={handleSearch}
/>
<LargeUserStatusFilter activeKey={filter} onSelect={handleFilter} />
<UserStatusFilter activeKey={filter} onSelect={handleFilter} />
</Flex>
<PeopleTable
topRef={topRef}
@@ -250,8 +249,4 @@ function Members() {
);
}
const LargeUserStatusFilter = styled(UserStatusFilter)`
height: 32px;
`;
export default observer(Members);
export default observer(People);
+3 -3
View File
@@ -7,10 +7,10 @@ import { languageOptions } from "@shared/i18n";
import UserDelete from "~/scenes/UserDelete";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import InputSelect from "~/components/InputSelect";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -126,12 +126,12 @@ const Profile = () => {
<DangerZone>
<h2>{t("Delete Account")}</h2>
<Text type="secondary" size="small">
<HelpText small>
<Trans>
You may delete your account at any time, note that this is
unrecoverable
</Trans>
</Text>
</HelpText>
<Button onClick={toggleDeleteAccount} neutral>
{t("Delete account")}
</Button>
+3 -3
View File
@@ -5,10 +5,10 @@ import { useState } from "react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import InputSelect from "~/components/InputSelect";
import Scene from "~/components/Scene";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
@@ -62,12 +62,12 @@ function Security() {
<Heading>
<Trans>Security</Trans>
</Heading>
<Text type="secondary">
<HelpText>
<Trans>
Settings that impact the access, security, and content of your
knowledge base.
</Trans>
</Text>
</HelpText>
<Switch
label={t("Allow email authentication")}
+5 -5
View File
@@ -5,10 +5,10 @@ import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom";
import Empty from "~/components/Empty";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import ShareListItem from "./components/ShareListItem";
@@ -23,15 +23,15 @@ function Shares() {
return (
<Scene title={t("Share Links")} icon={<LinkIcon color="currentColor" />}>
<Heading>{t("Share Links")}</Heading>
<Text type="secondary">
<HelpText>
<Trans>
Documents that have been shared are listed below. Anyone that has the
public link can access a read-only version of the document until the
link has been revoked.
</Trans>
</Text>
</HelpText>
{can.manage && (
<Text type="secondary">
<HelpText>
{!canShareDocuments && (
<strong>{t("Sharing is currently disabled.")}</strong>
)}{" "}
@@ -41,7 +41,7 @@ function Shares() {
em: <Link to="/settings/security" />,
}}
/>
</Text>
</HelpText>
)}
<Subheading>{t("Shared documents")}</Subheading>
<PaginatedList
+5 -5
View File
@@ -8,12 +8,12 @@ import Integration from "~/models/Integration";
import Button from "~/components/Button";
import CollectionIcon from "~/components/CollectionIcon";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import List from "~/components/List";
import ListItem from "~/components/List/Item";
import Notice from "~/components/Notice";
import Scene from "~/components/Scene";
import SlackIcon from "~/components/SlackIcon";
import Text from "~/components/Text";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useQuery from "~/hooks/useQuery";
@@ -72,7 +72,7 @@ function Slack() {
</Trans>
</Notice>
)}
<Text type="secondary">
<HelpText>
<Trans
defaults="Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat."
values={{
@@ -82,7 +82,7 @@ function Slack() {
em: <Code />,
}}
/>
</Text>
</HelpText>
{env.SLACK_KEY ? (
<>
<p>
@@ -102,13 +102,13 @@ function Slack() {
<p>&nbsp;</p>
<h2>{t("Collections")}</h2>
<Text type="secondary">
<HelpText>
<Trans>
Connect Outline collections to Slack channels and messages will be
automatically posted to Slack when documents are published or
updated.
</Trans>
</Text>
</HelpText>
<List>
{groupedCollections.map(([collection, integration]) => {
+3 -3
View File
@@ -6,11 +6,11 @@ import APITokenNew from "~/scenes/APITokenNew";
import { Action } from "~/components/Actions";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
@@ -42,7 +42,7 @@ function Tokens() {
}
>
<Heading>{t("API Tokens")}</Heading>
<Text type="secondary">
<HelpText>
<Trans
defaults="You can create an unlimited amount of personal tokens to authenticate
with the API. Tokens have the same permissions as your user account.
@@ -53,7 +53,7 @@ function Tokens() {
),
}}
/>
</Text>
</HelpText>
<PaginatedList
fetch={apiKeys.fetchPage}
items={apiKeys.orderedData}
+3 -3
View File
@@ -2,8 +2,8 @@ import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import ZapierIcon from "~/components/ZapierIcon";
function Zapier() {
@@ -11,13 +11,13 @@ function Zapier() {
return (
<Scene title={t("Zapier")} icon={<ZapierIcon color="currentColor" />}>
<Heading>{t("Zapier")}</Heading>
<Text type="secondary">
<HelpText>
<Trans>
Zapier is a platform that allows Outline to easily integrate with
thousands of other business tools. Head over to Zapier to setup a
"Zap" and start programmatically interacting with Outline.'
</Trans>
</Text>
</HelpText>
<p>
<Button
onClick={() =>
@@ -10,10 +10,10 @@ import Button from "~/components/Button";
import ButtonLink from "~/components/ButtonLink";
import CollectionIcon from "~/components/CollectionIcon";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import ListItem from "~/components/List/Item";
import Popover from "~/components/Popover";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useToasts from "~/hooks/useToasts";
type Props = {
@@ -81,9 +81,7 @@ function SlackListItem({ integration, collection }: Props) {
<Popover {...popover} aria-label={t("Settings")}>
<Events>
<h3>{t("Notifications")}</h3>
<Text type="secondary">
{t("These events should be posted to Slack")}
</Text>
<HelpText>{t("These events should be posted to Slack")}</HelpText>
<Switch
label={t("Document published")}
name="documents.publish"
@@ -7,7 +7,7 @@ type Props = {
onSelect: (key: string | null | undefined) => void;
};
const UserStatusFilter = ({ activeKey, onSelect, ...rest }: Props) => {
const UserStatusFilter = ({ activeKey, onSelect }: Props) => {
const { t } = useTranslation();
const options = React.useMemo(
() => [
@@ -45,7 +45,6 @@ const UserStatusFilter = ({ activeKey, onSelect, ...rest }: Props) => {
activeKey={activeKey}
onSelect={onSelect}
defaultLabel={t("Active")}
{...rest}
/>
);
};
+5 -5
View File
@@ -3,8 +3,8 @@ import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Modal from "~/components/Modal";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -41,22 +41,22 @@ function UserDelete({ onRequestClose }: Props) {
<Modal isOpen title={t("Delete Account")} onRequestClose={onRequestClose}>
<Flex column>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<HelpText>
<Trans>
Are you sure? Deleting your account will destroy identifying data
associated with your user and cannot be undone. You will be
immediately logged out of Outline and all your API tokens will be
revoked.
</Trans>
</Text>
<Text type="secondary">
</HelpText>
<HelpText>
<Trans
defaults="<em>Note:</em> Signing back in will cause a new account to be automatically reprovisioned."
components={{
em: <strong />,
}}
/>
</Text>
</HelpText>
<Button type="submit" danger>
{isDeleting ? `${t("Deleting")}` : t("Delete My Account")}
</Button>
+4 -4
View File
@@ -10,10 +10,10 @@ import Avatar from "~/components/Avatar";
import Badge from "~/components/Badge";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText";
import Modal from "~/components/Modal";
import PaginatedDocumentList from "~/components/PaginatedDocumentList";
import Subheading from "~/components/Subheading";
import Text from "~/components/Text";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { settingsPath } from "~/utils/routeHelpers";
@@ -79,11 +79,11 @@ function UserProfile(props: Props) {
}}
heading={<Subheading>{t("Recently updated")}</Subheading>}
empty={
<Text type="secondary">
<HelpText>
{t("{{ userName }} hasnt updated any documents yet.", {
userName: user.name,
})}
</Text>
</HelpText>
}
showCollection
/>
@@ -103,7 +103,7 @@ const StyledBadge = styled(Badge)`
top: -2px;
`;
const Meta = styled(Text)`
const Meta = styled(HelpText)`
margin-top: -12px;
`;
+1 -6
View File
@@ -142,12 +142,7 @@ export default class DocumentsStore extends BaseStore<Document> {
return [];
}
const drafts = this.drafts({ collectionId });
return compact([
...drafts,
...collection.documents.map((node) => this.get(node.id)),
]);
return compact(collection.documents.map((node) => this.get(node.id)));
}
leastRecentlyUpdatedInCollection(collectionId: string): Document[] {
-1
View File
@@ -7,7 +7,6 @@ export type MenuItemButton = {
type: "button";
title: React.ReactNode;
onClick: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
dangerous?: boolean;
visible?: boolean;
selected?: boolean;
disabled?: boolean;
-2
View File
@@ -1,7 +1,5 @@
import { isMac } from "~/utils/browser";
export const altDisplay = isMac() ? "⌥" : "Alt";
export const metaDisplay = isMac() ? "⌘" : "Ctrl";
export const meta = isMac() ? "cmd" : "ctrl";
-2
View File
@@ -117,5 +117,3 @@ export const matchDocumentSlug =
":documentSlug([0-9a-zA-Z-_~]*-[a-zA-z0-9]{10,15})";
export const matchDocumentEdit = `/doc/${matchDocumentSlug}/edit`;
export const matchDocumentHistory = `/doc/${matchDocumentSlug}/history/:revisionId?`;
+22
View File
@@ -1,3 +1,25 @@
import { parseDomain } from "@shared/utils/domains";
export function isInternalUrl(href: string) {
if (href[0] === "/") {
return true;
}
const outline = parseDomain(window.location.href);
const parsed = parseDomain(href);
if (
parsed &&
outline &&
parsed.subdomain === outline.subdomain &&
parsed.domain === outline.domain &&
parsed.tld === outline.tld
) {
return true;
}
return false;
}
export function isHash(href: string) {
if (href[0] === "#") {
return true;
+1 -1
View File
@@ -122,7 +122,7 @@
"mobx-react": "^6.3.1",
"natural-sort": "^1.0.0",
"nodemailer": "^6.6.1",
"outline-icons": "^1.41.0",
"outline-icons": "^1.39.0",
"oy-vey": "^0.10.0",
"passport": "^0.4.1",
"passport-google-oauth2": "^0.2.0",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

-11
View File
@@ -1263,17 +1263,6 @@ describe("#documents.search", () => {
expect(body.data.length).toEqual(0);
});
it("should expect a query", async () => {
const { user } = await seed();
const res = await server.post("/api/documents.search", {
body: {
token: user.getJwtToken(),
query: " ",
},
});
expect(res.status).toEqual(400);
});
it("should not allow unknown dateFilter values", async () => {
const { user } = await seed();
const res = await server.post("/api/documents.search", {
+3 -3
View File
@@ -37,7 +37,6 @@ import {
assertIn,
assertPresent,
assertPositiveInteger,
assertNotEmpty,
} from "@server/validation";
import env from "../../env";
import pagination from "./middlewares/pagination";
@@ -813,7 +812,7 @@ router.post("documents.search", auth(), pagination(), async (ctx) => {
const { offset, limit } = ctx.state.pagination;
const { user } = ctx.state;
assertNotEmpty(query, "query is required");
assertPresent(query, "query is required");
if (collectionId) {
assertUuid(collectionId, "collectionId must be a UUID");
@@ -1009,6 +1008,7 @@ router.post("documents.update", auth(), async (ctx) => {
} = ctx.body;
const editorVersion = ctx.headers["x-editor-version"] as string | undefined;
assertPresent(id, "id is required");
assertPresent(title || text, "title or text is required");
if (append) {
assertPresent(text, "Text is required while appending");
}
@@ -1026,7 +1026,7 @@ router.post("documents.update", auth(), async (ctx) => {
const previousTitle = document.title;
// Update document
if (title !== undefined) {
if (title) {
document.title = title;
}
if (editorVersion) {
+10
View File
@@ -92,6 +92,16 @@ export async function getUserForEmailSigninToken(token: string): Promise<User> {
});
invariant(user, "User not found");
// if user has signed in at all since the token was created then
// it's no longer valid, they'll need a new one.
if (
user.lastSignedInAt &&
payload.createdAt &&
user.lastSignedInAt > new Date(payload.createdAt)
) {
throw AuthenticationError("Token has already been used");
}
try {
JWT.verify(token, user.jwtSecret);
} catch (err) {
+6 -62
View File
@@ -23,10 +23,7 @@ export type Item = {
item: JSZipObject;
};
async function addDocumentTreeToArchive(
zip: JSZip,
documents: NavigationNode[]
) {
async function addToArchive(zip: JSZip, documents: NavigationNode[]) {
for (const doc of documents) {
const document = await Document.findByPk(doc.id);
@@ -47,9 +44,8 @@ async function addDocumentTreeToArchive(
text = text.replace(attachment.redirectUrl, encodeURI(attachment.key));
}
let title = serializeFilename(document.title) || "Untitled";
title = safeAddFileToArchive(zip, `${title}.md`, text, {
const title = serializeFilename(document.title) || "Untitled";
zip.file(`${title}.md`, text, {
date: document.updatedAt,
comment: JSON.stringify({
createdAt: document.createdAt,
@@ -58,21 +54,15 @@ async function addDocumentTreeToArchive(
});
if (doc.children && doc.children.length) {
const folder = zip.folder(path.parse(title).name);
const folder = zip.folder(title);
if (folder) {
await addDocumentTreeToArchive(folder, doc.children);
await addToArchive(folder, doc.children);
}
}
}
}
/**
* Adds the content of a file in remote storage to the given zip file.
*
* @param zip JSZip object to add to
* @param key path to file in S3 storage
*/
async function addImageToArchive(zip: JSZip, key: string) {
try {
const img = await getFileByKey(key);
@@ -88,52 +78,6 @@ async function addImageToArchive(zip: JSZip, key: string) {
}
}
/**
* Adds content to a zip file, if the given filename already exists in the zip
* then it will automatically increment numbers at the end of the filename.
*
* @param zip JSZip object to add to
* @param key filename with extension
* @param content the content to add
* @param options options for added content
* @returns The new title
*/
function safeAddFileToArchive(
zip: JSZip,
key: string,
content: string | Uint8Array | ArrayBuffer | Blob,
options: JSZip.JSZipFileOptions
) {
// @ts-expect-error root exists
const root = zip.root;
// Filenames in the directory already
const keysInDirectory = Object.keys(zip.files)
.filter((k) => k.includes(root))
.filter((k) => !k.endsWith("/"))
.map((k) => path.basename(k).replace(/\s\((\d+)\)\./, "."));
// The number of duplicate filenames
const existingKeysCount = keysInDirectory.filter((t) => t === key).length;
const filename = path.parse(key).name;
const extension = path.extname(key);
// Construct the new de-duplicated filename (if any)
const safeKey =
existingKeysCount > 0
? `${filename} (${existingKeysCount})${extension}`
: key;
zip.file(safeKey, content, options);
return safeKey;
}
/**
* Write a zip file to a temporary disk location
*
* @param zip JSZip object
* @returns pathname of the temporary file where the zip was written to disk
*/
async function archiveToPath(zip: JSZip) {
return new Promise((resolve, reject) => {
tmp.file(
@@ -166,7 +110,7 @@ export async function archiveCollections(collections: Collection[]) {
const folder = zip.folder(collection.name);
if (folder) {
await addDocumentTreeToArchive(folder, collection.documentStructure);
await addToArchive(folder, collection.documentStructure);
}
}
}

Some files were not shown because too many files have changed in this diff Show More