mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c10e868626 |
@@ -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,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;
|
||||
}
|
||||
`};
|
||||
`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -73,7 +73,7 @@ const ContentEditable = React.forwardRef(
|
||||
if (autoFocus) {
|
||||
ref.current?.focus();
|
||||
}
|
||||
}, [autoFocus, ref]);
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (value !== ref.current?.innerText) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)<{
|
||||
|
||||
@@ -125,9 +125,6 @@ function DocumentMeta({
|
||||
}
|
||||
|
||||
if (!lastViewedAt) {
|
||||
if (lastUpdatedByCurrentUser) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Viewed>
|
||||
• <Modified highlight>{t("Never viewed")}</Modified>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -131,7 +131,7 @@ const Wrapper = styled(Flex)<{ $passThrough?: boolean }>`
|
||||
}
|
||||
|
||||
${breakpoint("tablet")`
|
||||
padding: 16px;
|
||||
padding: 16px 16px 0;
|
||||
justify-content: center;
|
||||
`};
|
||||
`;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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} />
|
||||
|
||||
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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;` : "")}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")}…`,
|
||||
|
||||
@@ -34,7 +34,6 @@ function CollectionGroupMemberMenu({ onMembers, onRemove }: Props) {
|
||||
{
|
||||
type: "button",
|
||||
title: t("Remove"),
|
||||
dangerous: true,
|
||||
onClick: onRemove,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -171,7 +171,6 @@ function CollectionMenu({
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Delete")}…`,
|
||||
dangerous: true,
|
||||
visible: !!(collection && can.delete),
|
||||
onClick: () => setShowCollectionDelete(true),
|
||||
icon: <TrashIcon />,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -24,7 +24,6 @@ function GroupMemberMenu({ onRemove }: Props) {
|
||||
items={[
|
||||
{
|
||||
type: "button",
|
||||
dangerous: true,
|
||||
title: t("Remove"),
|
||||
onClick: onRemove,
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -24,7 +24,6 @@ function MemberMenu({ onRemove }: Props) {
|
||||
{
|
||||
type: "button",
|
||||
title: t("Remove"),
|
||||
dangerous: true,
|
||||
onClick: onRemove,
|
||||
},
|
||||
]}
|
||||
|
||||
+3
-11
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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> doesn’t 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,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("I’m sure – Delete")}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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("Can’t find the group you’re 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 who’s 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"
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
You’re 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}
|
||||
|
||||
@@ -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("You’re editing a template")}
|
||||
</Notice>
|
||||
)}
|
||||
{document.archivedAt && !document.deletedAt && (
|
||||
<Notice icon={<ArchiveIcon />}>
|
||||
{t("Archived by {{userName}}", {
|
||||
userName: document.updatedBy.name,
|
||||
})}
|
||||
|
||||
<Time dateTime={document.updatedAt} addSuffix />
|
||||
</Notice>
|
||||
)}
|
||||
{document.deletedAt && (
|
||||
<Notice
|
||||
icon={<TrashIcon />}
|
||||
description={permanentlyDeletedDescription()}
|
||||
>
|
||||
{t("Deleted by {{userName}}", {
|
||||
userName: document.updatedBy.name,
|
||||
})}
|
||||
|
||||
<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,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 you’d 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("I’m sure – Delete")}
|
||||
|
||||
@@ -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("I’m sure – Delete")}
|
||||
</Button>
|
||||
|
||||
@@ -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>{" "}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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("I’m sure – Delete")}
|
||||
</Button>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 who’s 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")}…`}
|
||||
|
||||
@@ -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,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>You’ll be able to add people to the group next.</Trans>
|
||||
</Text>
|
||||
</HelpText>
|
||||
|
||||
<Button type="submit" disabled={isSaving || !name}>
|
||||
{isSaving ? `${t("Creating")}…` : t("Continue")}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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 it’s complete."
|
||||
values={{
|
||||
@@ -65,7 +65,7 @@ function Export() {
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
</HelpText>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={handleExport}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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. It’s possible that
|
||||
there are other users who have access through {team.signinMethods} but
|
||||
haven’t 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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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> </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]) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }} hasn’t 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;
|
||||
`;
|
||||
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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?`;
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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", {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user