feat: Optional full-width toggle for document display (#2869)
* Migration, model, presenter * Working implementation * fix: Account for table of contents * Checkbox -> Toggle * Checkbox -> Toggle
This commit is contained in:
@@ -1,62 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import HelpText from "~/components/HelpText";
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
checked?: boolean;
|
|
||||||
label?: React.ReactNode;
|
|
||||||
labelHidden?: boolean;
|
|
||||||
className?: string;
|
|
||||||
name?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
|
|
||||||
note?: React.ReactNode;
|
|
||||||
small?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LabelText = styled.span<{ small?: boolean }>`
|
|
||||||
font-weight: 500;
|
|
||||||
margin-left: ${(props) => (props.small ? "6px" : "10px")};
|
|
||||||
${(props) => (props.small ? `color: ${props.theme.textSecondary}` : "")};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Wrapper = styled.div<{ small?: boolean }>`
|
|
||||||
padding-bottom: 8px;
|
|
||||||
${(props) => (props.small ? "font-size: 14px" : "")};
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Label = styled.label`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
user-select: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function Checkbox({
|
|
||||||
label,
|
|
||||||
labelHidden,
|
|
||||||
note,
|
|
||||||
className,
|
|
||||||
small,
|
|
||||||
...rest
|
|
||||||
}: Props) {
|
|
||||||
const wrappedLabel = <LabelText small={small}>{label}</LabelText>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Wrapper small={small} className={className}>
|
|
||||||
<Label>
|
|
||||||
<input type="checkbox" {...rest} />
|
|
||||||
{label &&
|
|
||||||
(labelHidden ? (
|
|
||||||
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
|
|
||||||
) : (
|
|
||||||
wrappedLabel
|
|
||||||
))}
|
|
||||||
</Label>
|
|
||||||
{note && <HelpText small>{note}</HelpText>}
|
|
||||||
</Wrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import Arrow from "~/components/Arrow";
|
|||||||
type Props = {
|
type Props = {
|
||||||
direction: "left" | "right";
|
direction: "left" | "right";
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
onClick?: () => any;
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Toggle = React.forwardRef<HTMLButtonElement, Props>(
|
const Toggle = React.forwardRef<HTMLButtonElement, Props>(
|
||||||
|
|||||||
104
app/components/Toggle.tsx
Normal file
104
app/components/Toggle.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import HelpText from "~/components/HelpText";
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
checked?: boolean;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
labelHidden?: boolean;
|
||||||
|
className?: string;
|
||||||
|
name?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
|
||||||
|
note?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LabelText = styled.span`
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
padding-bottom: 8px;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Label = styled.label`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SlideToggle = styled.label`
|
||||||
|
cursor: pointer;
|
||||||
|
text-indent: -9999px;
|
||||||
|
width: 26px;
|
||||||
|
height: 14px;
|
||||||
|
background: ${(props) => props.theme.slate};
|
||||||
|
display: block;
|
||||||
|
border-radius: 10px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: ${(props) => props.theme.white};
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: width 100ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:after {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HiddenInput = styled.input`
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
&:checked + ${SlideToggle} {
|
||||||
|
background: ${(props) => props.theme.primary};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + ${SlideToggle}:after {
|
||||||
|
left: calc(100% - 2px);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let inputId = 0;
|
||||||
|
|
||||||
|
export default function Toggle({
|
||||||
|
label,
|
||||||
|
labelHidden,
|
||||||
|
note,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: Props) {
|
||||||
|
const wrappedLabel = <LabelText>{label}</LabelText>;
|
||||||
|
const [id] = React.useState(`checkbox-input-${inputId++}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Wrapper className={className}>
|
||||||
|
<Label>
|
||||||
|
<HiddenInput type="checkbox" id={id} {...rest} />
|
||||||
|
<SlideToggle htmlFor={id} />
|
||||||
|
{label &&
|
||||||
|
(labelHidden ? (
|
||||||
|
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
|
||||||
|
) : (
|
||||||
|
wrappedLabel
|
||||||
|
))}
|
||||||
|
</Label>
|
||||||
|
{note && <HelpText small>{note}</HelpText>}
|
||||||
|
</Wrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import { useHistory } from "react-router-dom";
|
|||||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import DocumentDelete from "~/scenes/DocumentDelete";
|
import DocumentDelete from "~/scenes/DocumentDelete";
|
||||||
import DocumentMove from "~/scenes/DocumentMove";
|
import DocumentMove from "~/scenes/DocumentMove";
|
||||||
@@ -33,9 +34,11 @@ import DocumentTemplatize from "~/scenes/DocumentTemplatize";
|
|||||||
import CollectionIcon from "~/components/CollectionIcon";
|
import CollectionIcon from "~/components/CollectionIcon";
|
||||||
import ContextMenu from "~/components/ContextMenu";
|
import ContextMenu from "~/components/ContextMenu";
|
||||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||||
|
import Separator from "~/components/ContextMenu/Separator";
|
||||||
import Template from "~/components/ContextMenu/Template";
|
import Template from "~/components/ContextMenu/Template";
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import Modal from "~/components/Modal";
|
import Modal from "~/components/Modal";
|
||||||
|
import Toggle from "~/components/Toggle";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import useToasts from "~/hooks/useToasts";
|
import useToasts from "~/hooks/useToasts";
|
||||||
@@ -52,7 +55,8 @@ type Props = {
|
|||||||
document: Document;
|
document: Document;
|
||||||
className?: string;
|
className?: string;
|
||||||
isRevision?: boolean;
|
isRevision?: boolean;
|
||||||
showPrint?: boolean;
|
/** Pass true if the document is currently being displayed */
|
||||||
|
showDisplayOptions?: boolean;
|
||||||
modal?: boolean;
|
modal?: boolean;
|
||||||
showToggleEmbeds?: boolean;
|
showToggleEmbeds?: boolean;
|
||||||
showPin?: boolean;
|
showPin?: boolean;
|
||||||
@@ -67,7 +71,7 @@ function DocumentMenu({
|
|||||||
className,
|
className,
|
||||||
modal = true,
|
modal = true,
|
||||||
showToggleEmbeds,
|
showToggleEmbeds,
|
||||||
showPrint,
|
showDisplayOptions,
|
||||||
showPin,
|
showPin,
|
||||||
label,
|
label,
|
||||||
onOpen,
|
onOpen,
|
||||||
@@ -448,11 +452,26 @@ function DocumentMenu({
|
|||||||
type: "button",
|
type: "button",
|
||||||
title: t("Print"),
|
title: t("Print"),
|
||||||
onClick: handlePrint,
|
onClick: handlePrint,
|
||||||
visible: !!showPrint,
|
visible: !!showDisplayOptions,
|
||||||
icon: <PrintIcon />,
|
icon: <PrintIcon />,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{showDisplayOptions && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<Style>
|
||||||
|
<ToggleMenuItem
|
||||||
|
label={t("Full width")}
|
||||||
|
checked={document.fullWidth}
|
||||||
|
onChange={(ev) => {
|
||||||
|
document.fullWidth = ev.target.checked;
|
||||||
|
document.save();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Style>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
{renderModals && (
|
{renderModals && (
|
||||||
<>
|
<>
|
||||||
@@ -516,6 +535,21 @@ function DocumentMenu({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ToggleMenuItem = styled(Toggle)`
|
||||||
|
span {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Style = styled.div`
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
`};
|
||||||
|
`;
|
||||||
|
|
||||||
const CollectionName = styled.div`
|
const CollectionName = styled.div`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ export default class Document extends BaseModel {
|
|||||||
@observable
|
@observable
|
||||||
template: boolean;
|
template: boolean;
|
||||||
|
|
||||||
|
@Field
|
||||||
|
@observable
|
||||||
|
fullWidth: boolean;
|
||||||
|
|
||||||
@Field
|
@Field
|
||||||
@observable
|
@observable
|
||||||
templateId: string | undefined;
|
templateId: string | undefined;
|
||||||
@@ -311,6 +315,7 @@ export default class Document extends BaseModel {
|
|||||||
{
|
{
|
||||||
id: this.id,
|
id: this.id,
|
||||||
title: options.title || this.title,
|
title: options.title || this.title,
|
||||||
|
fullWidth: this.fullWidth,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
lastRevision: options.lastRevision,
|
lastRevision: options.lastRevision,
|
||||||
@@ -327,7 +332,7 @@ export default class Document extends BaseModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
save = async (options: SaveOptions | undefined) => {
|
save = async (options?: SaveOptions | undefined) => {
|
||||||
if (this.isSaving) return this;
|
if (this.isSaving) return this;
|
||||||
const isCreating = !this.id;
|
const isCreating = !this.id;
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
@@ -349,24 +354,21 @@ export default class Document extends BaseModel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.lastRevision) {
|
|
||||||
return await this.store.update(
|
return await this.store.update(
|
||||||
{
|
{
|
||||||
id: this.id,
|
id: this.id,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
|
fullWidth: this.fullWidth,
|
||||||
templateId: this.templateId,
|
templateId: this.templateId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
lastRevision: options?.lastRevision,
|
lastRevision: options?.lastRevision || this.revision,
|
||||||
publish: options?.publish,
|
publish: options?.publish,
|
||||||
done: options?.done,
|
done: options?.done,
|
||||||
autosave: options?.autosave,
|
autosave: options?.autosave,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Attempting to update without a lastRevision");
|
|
||||||
} finally {
|
} finally {
|
||||||
this.isSaving = false;
|
this.isSaving = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import useWindowScrollPosition from "~/hooks/useWindowScrollPosition";
|
|||||||
const HEADING_OFFSET = 20;
|
const HEADING_OFFSET = 20;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
isFullWidth: boolean;
|
||||||
headings: {
|
headings: {
|
||||||
title: string;
|
title: string;
|
||||||
level: number;
|
level: number;
|
||||||
@@ -14,7 +15,7 @@ type Props = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Contents({ headings }: Props) {
|
export default function Contents({ headings, isFullWidth }: Props) {
|
||||||
const [activeSlug, setActiveSlug] = React.useState<string>();
|
const [activeSlug, setActiveSlug] = React.useState<string>();
|
||||||
const position = useWindowScrollPosition({
|
const position = useWindowScrollPosition({
|
||||||
throttle: 100,
|
throttle: 100,
|
||||||
@@ -49,8 +50,8 @@ export default function Contents({ headings }: Props) {
|
|||||||
const headingAdjustment = minHeading - 1;
|
const headingAdjustment = minHeading - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Wrapper isFullWidth={isFullWidth}>
|
||||||
<Wrapper>
|
<Sticky>
|
||||||
<Heading>Contents</Heading>
|
<Heading>Contents</Heading>
|
||||||
{headings.length ? (
|
{headings.length ? (
|
||||||
<List>
|
<List>
|
||||||
@@ -67,30 +68,38 @@ export default function Contents({ headings }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<Empty>Headings you add to the document will appear here</Empty>
|
<Empty>Headings you add to the document will appear here</Empty>
|
||||||
)}
|
)}
|
||||||
|
</Sticky>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div<{ isFullWidth: boolean }>`
|
||||||
|
width: 256px;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
display: block;
|
||||||
|
`};
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
!props.isFullWidth &&
|
||||||
|
breakpoint("desktopLarge")`
|
||||||
|
transform: translateX(-256px);
|
||||||
|
width: 0;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Sticky = styled.div`
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 80px;
|
top: 80px;
|
||||||
max-height: calc(100vh - 80px);
|
max-height: calc(100vh - 80px);
|
||||||
|
|
||||||
box-shadow: 1px 0 0 ${(props) => props.theme.divider};
|
box-shadow: 1px 0 0 ${(props) => props.theme.divider};
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
margin-right: 3.2em;
|
margin-right: 32px;
|
||||||
|
width: 224px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
${breakpoint("desktopLarge")`
|
|
||||||
margin-left: -16em;
|
|
||||||
`};
|
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
|
||||||
display: block;
|
|
||||||
`};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Heading = styled.h3`
|
const Heading = styled.h3`
|
||||||
|
|||||||
@@ -493,6 +493,7 @@ class DocumentScene extends React.Component<Props> {
|
|||||||
archived={document.isArchived}
|
archived={document.isArchived}
|
||||||
showContents={showContents}
|
showContents={showContents}
|
||||||
isEditing={!readOnly}
|
isEditing={!readOnly}
|
||||||
|
isFullWidth={document.fullWidth}
|
||||||
column
|
column
|
||||||
auto
|
auto
|
||||||
>
|
>
|
||||||
@@ -544,7 +545,12 @@ class DocumentScene extends React.Component<Props> {
|
|||||||
)}
|
)}
|
||||||
<React.Suspense fallback={<PlaceholderDocument />}>
|
<React.Suspense fallback={<PlaceholderDocument />}>
|
||||||
<Flex auto={!readOnly}>
|
<Flex auto={!readOnly}>
|
||||||
{showContents && <Contents headings={headings} />}
|
{showContents && (
|
||||||
|
<Contents
|
||||||
|
headings={headings}
|
||||||
|
isFullWidth={document.fullWidth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Editor
|
<Editor
|
||||||
id={document.id}
|
id={document.id}
|
||||||
key={disableEmbeds ? "disabled" : "enabled"}
|
key={disableEmbeds ? "disabled" : "enabled"}
|
||||||
@@ -628,6 +634,7 @@ const ReferencesWrapper = styled.div<{ isOnlyTitle?: boolean }>`
|
|||||||
|
|
||||||
type MaxWidthProps = {
|
type MaxWidthProps = {
|
||||||
isEditing?: boolean;
|
isEditing?: boolean;
|
||||||
|
isFullWidth?: boolean;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
showContents?: boolean;
|
showContents?: boolean;
|
||||||
};
|
};
|
||||||
@@ -636,22 +643,23 @@ const MaxWidth = styled(Flex)<MaxWidthProps>`
|
|||||||
${(props) =>
|
${(props) =>
|
||||||
props.archived && `* { color: ${props.theme.textSecondary} !important; } `};
|
props.archived && `* { color: ${props.theme.textSecondary} !important; } `};
|
||||||
|
|
||||||
// Adds space to the gutter to make room for heading annotations on mobile
|
// Adds space to the gutter to make room for heading annotations
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
transition: padding 100ms;
|
transition: padding 100ms;
|
||||||
|
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
${breakpoint("tablet")`
|
||||||
padding: 0 24px;
|
|
||||||
margin: 4px auto 12px;
|
margin: 4px auto 12px;
|
||||||
max-width: calc(48px + ${(props: MaxWidthProps) =>
|
max-width: ${(props: MaxWidthProps) =>
|
||||||
props.showContents ? "64em" : "46em"});
|
props.isFullWidth
|
||||||
|
? "100vw"
|
||||||
|
: `calc(64px + 46em + ${props.showContents ? "256px" : "0px"});`}
|
||||||
`};
|
`};
|
||||||
|
|
||||||
${breakpoint("desktopLarge")`
|
${breakpoint("desktopLarge")`
|
||||||
max-width: calc(48px + 52em);
|
max-width: ${(props: MaxWidthProps) =>
|
||||||
|
props.isFullWidth ? "100vw" : `calc(64px + 52em);`}
|
||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ function DocumentHeader({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
showToggleEmbeds={canToggleEmbeds}
|
showToggleEmbeds={canToggleEmbeds}
|
||||||
showPrint
|
showDisplayOptions
|
||||||
/>
|
/>
|
||||||
</Action>
|
</Action>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { BeakerIcon } from "outline-icons";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation, Trans } from "react-i18next";
|
import { useTranslation, Trans } from "react-i18next";
|
||||||
import Checkbox from "~/components/Checkbox";
|
|
||||||
import Heading from "~/components/Heading";
|
import Heading from "~/components/Heading";
|
||||||
import HelpText from "~/components/HelpText";
|
import HelpText from "~/components/HelpText";
|
||||||
import Scene from "~/components/Scene";
|
import Scene from "~/components/Scene";
|
||||||
|
import Toggle from "~/components/Toggle";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import useToasts from "~/hooks/useToasts";
|
import useToasts from "~/hooks/useToasts";
|
||||||
@@ -52,7 +52,7 @@ function Features() {
|
|||||||
the experience for all team members.
|
the experience for all team members.
|
||||||
</Trans>
|
</Trans>
|
||||||
</HelpText>
|
</HelpText>
|
||||||
<Checkbox
|
<Toggle
|
||||||
label={t("Collaborative editing")}
|
label={t("Collaborative editing")}
|
||||||
name="collaborativeEditing"
|
name="collaborativeEditing"
|
||||||
checked={data.collaborativeEditing}
|
checked={data.collaborativeEditing}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { PadlockIcon } from "outline-icons";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation, Trans } from "react-i18next";
|
import { useTranslation, Trans } from "react-i18next";
|
||||||
import Checkbox from "~/components/Checkbox";
|
|
||||||
import Heading from "~/components/Heading";
|
import Heading from "~/components/Heading";
|
||||||
import HelpText from "~/components/HelpText";
|
import HelpText from "~/components/HelpText";
|
||||||
import InputSelect from "~/components/InputSelect";
|
import InputSelect from "~/components/InputSelect";
|
||||||
import Scene from "~/components/Scene";
|
import Scene from "~/components/Scene";
|
||||||
|
import Toggle from "~/components/Toggle";
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
@@ -69,7 +69,7 @@ function Security() {
|
|||||||
</Trans>
|
</Trans>
|
||||||
</HelpText>
|
</HelpText>
|
||||||
|
|
||||||
<Checkbox
|
<Toggle
|
||||||
label={t("Allow email authentication")}
|
label={t("Allow email authentication")}
|
||||||
name="guestSignin"
|
name="guestSignin"
|
||||||
checked={data.guestSignin}
|
checked={data.guestSignin}
|
||||||
@@ -81,7 +81,7 @@ function Security() {
|
|||||||
}
|
}
|
||||||
disabled={!env.EMAIL_ENABLED}
|
disabled={!env.EMAIL_ENABLED}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Toggle
|
||||||
label={t("Public document sharing")}
|
label={t("Public document sharing")}
|
||||||
name="sharing"
|
name="sharing"
|
||||||
checked={data.sharing}
|
checked={data.sharing}
|
||||||
@@ -90,7 +90,7 @@ function Security() {
|
|||||||
"When enabled, documents can be shared publicly on the internet by any team member"
|
"When enabled, documents can be shared publicly on the internet by any team member"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Toggle
|
||||||
label={t("Rich service embeds")}
|
label={t("Rich service embeds")}
|
||||||
name="documentEmbeds"
|
name="documentEmbeds"
|
||||||
checked={data.documentEmbeds}
|
checked={data.documentEmbeds}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import NotificationSetting from "~/models/NotificationSetting";
|
import NotificationSetting from "~/models/NotificationSetting";
|
||||||
import Checkbox from "~/components/Checkbox";
|
import Toggle from "~/components/Toggle";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setting?: NotificationSetting;
|
setting?: NotificationSetting;
|
||||||
@@ -20,7 +20,7 @@ const NotificationListItem = ({
|
|||||||
description,
|
description,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Toggle
|
||||||
label={title}
|
label={title}
|
||||||
name={event}
|
name={event}
|
||||||
checked={!!setting}
|
checked={!!setting}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import Collection from "~/models/Collection";
|
|||||||
import Integration from "~/models/Integration";
|
import Integration from "~/models/Integration";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import ButtonLink from "~/components/ButtonLink";
|
import ButtonLink from "~/components/ButtonLink";
|
||||||
import Checkbox from "~/components/Checkbox";
|
|
||||||
import CollectionIcon from "~/components/CollectionIcon";
|
import CollectionIcon from "~/components/CollectionIcon";
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import HelpText from "~/components/HelpText";
|
import HelpText from "~/components/HelpText";
|
||||||
import ListItem from "~/components/List/Item";
|
import ListItem from "~/components/List/Item";
|
||||||
import Popover from "~/components/Popover";
|
import Popover from "~/components/Popover";
|
||||||
|
import Toggle from "~/components/Toggle";
|
||||||
import useToasts from "~/hooks/useToasts";
|
import useToasts from "~/hooks/useToasts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -82,13 +82,13 @@ function SlackListItem({ integration, collection }: Props) {
|
|||||||
<Events>
|
<Events>
|
||||||
<h3>{t("Notifications")}</h3>
|
<h3>{t("Notifications")}</h3>
|
||||||
<HelpText>{t("These events should be posted to Slack")}</HelpText>
|
<HelpText>{t("These events should be posted to Slack")}</HelpText>
|
||||||
<Checkbox
|
<Toggle
|
||||||
label={t("Document published")}
|
label={t("Document published")}
|
||||||
name="documents.publish"
|
name="documents.publish"
|
||||||
checked={integration.events.includes("documents.publish")}
|
checked={integration.events.includes("documents.publish")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Toggle
|
||||||
label={t("Document updated")}
|
label={t("Document updated")}
|
||||||
name="documents.update"
|
name="documents.update"
|
||||||
checked={integration.events.includes("documents.update")}
|
checked={integration.events.includes("documents.update")}
|
||||||
|
|||||||
@@ -643,6 +643,7 @@ export default class DocumentsStore extends BaseStore<Document> {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
templateId?: string;
|
templateId?: string;
|
||||||
},
|
},
|
||||||
options?: {
|
options?: {
|
||||||
|
|||||||
14
server/migrations/20211218193004-documents-full-width.js
Normal file
14
server/migrations/20211218193004-documents-full-width.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn("documents", "fullWidth", {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
defaultValue: false,
|
||||||
|
allowNull: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn("documents", "fullWidth");
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -81,6 +81,7 @@ const Document = sequelize.define(
|
|||||||
previousTitles: DataTypes.ARRAY(DataTypes.STRING),
|
previousTitles: DataTypes.ARRAY(DataTypes.STRING),
|
||||||
version: DataTypes.SMALLINT,
|
version: DataTypes.SMALLINT,
|
||||||
template: DataTypes.BOOLEAN,
|
template: DataTypes.BOOLEAN,
|
||||||
|
fullWidth: DataTypes.BOOLEAN,
|
||||||
editorVersion: DataTypes.STRING,
|
editorVersion: DataTypes.STRING,
|
||||||
text: DataTypes.TEXT,
|
text: DataTypes.TEXT,
|
||||||
state: DataTypes.BLOB,
|
state: DataTypes.BLOB,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Attachment, Document } from "@server/models";
|
import { Attachment } from "@server/models";
|
||||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||||
import { getSignedUrl } from "@server/utils/s3";
|
import { getSignedUrl } from "@server/utils/s3";
|
||||||
import presentUser from "./user";
|
import presentUser from "./user";
|
||||||
@@ -25,80 +25,58 @@ async function replaceImageAttachments(text: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function present(
|
export default async function present(
|
||||||
document: Document,
|
document: any,
|
||||||
options: Options | null | undefined
|
options: Options | null | undefined
|
||||||
) {
|
) {
|
||||||
options = {
|
options = {
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'migrateVersion' does not exist on type '... Remove this comment to see the full error message
|
|
||||||
await document.migrateVersion();
|
await document.migrateVersion();
|
||||||
const text = options.isPublic
|
const text = options.isPublic
|
||||||
? // @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
? await replaceImageAttachments(document.text)
|
||||||
await replaceImageAttachments(document.text)
|
: document.text;
|
||||||
: // @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
|
||||||
document.text;
|
|
||||||
const data = {
|
const data = {
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
|
||||||
id: document.id,
|
id: document.id,
|
||||||
// @ts-expect-error ts-migrate(2551) FIXME: Property 'url' does not exist on type 'Document'. ... Remove this comment to see the full error message
|
|
||||||
url: document.url,
|
url: document.url,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'urlId' does not exist on type 'Document'... Remove this comment to see the full error message
|
|
||||||
urlId: document.urlId,
|
urlId: document.urlId,
|
||||||
title: document.title,
|
title: document.title,
|
||||||
text,
|
text,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'emoji' does not exist on type 'Document'... Remove this comment to see the full error message
|
|
||||||
emoji: document.emoji,
|
emoji: document.emoji,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'tasks' does not exist on type 'Document'... Remove this comment to see the full error message
|
|
||||||
tasks: document.tasks,
|
tasks: document.tasks,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'createdAt' does not exist on type 'Docum... Remove this comment to see the full error message
|
|
||||||
createdAt: document.createdAt,
|
createdAt: document.createdAt,
|
||||||
createdBy: undefined,
|
createdBy: undefined,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedAt' does not exist on type 'Docum... Remove this comment to see the full error message
|
|
||||||
updatedAt: document.updatedAt,
|
updatedAt: document.updatedAt,
|
||||||
updatedBy: undefined,
|
updatedBy: undefined,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'publishedAt' does not exist on type 'Doc... Remove this comment to see the full error message
|
|
||||||
publishedAt: document.publishedAt,
|
publishedAt: document.publishedAt,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'archivedAt' does not exist on type 'Docu... Remove this comment to see the full error message
|
|
||||||
archivedAt: document.archivedAt,
|
archivedAt: document.archivedAt,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'deletedAt' does not exist on type 'Docum... Remove this comment to see the full error message
|
|
||||||
deletedAt: document.deletedAt,
|
deletedAt: document.deletedAt,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
|
||||||
teamId: document.teamId,
|
teamId: document.teamId,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'template' does not exist on type 'Docume... Remove this comment to see the full error message
|
|
||||||
template: document.template,
|
template: document.template,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'templateId' does not exist on type 'Docu... Remove this comment to see the full error message
|
|
||||||
templateId: document.templateId,
|
templateId: document.templateId,
|
||||||
collaboratorIds: [],
|
collaboratorIds: [],
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'starred' does not exist on type 'Documen... Remove this comment to see the full error message
|
|
||||||
starred: document.starred ? !!document.starred.length : undefined,
|
starred: document.starred ? !!document.starred.length : undefined,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'revisionCount' does not exist on type 'D... Remove this comment to see the full error message
|
|
||||||
revision: document.revisionCount,
|
revision: document.revisionCount,
|
||||||
|
fullWidth: document.fullWidth,
|
||||||
pinned: undefined,
|
pinned: undefined,
|
||||||
collectionId: undefined,
|
collectionId: undefined,
|
||||||
parentDocumentId: undefined,
|
parentDocumentId: undefined,
|
||||||
lastViewedAt: undefined,
|
lastViewedAt: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'views' does not exist on type 'Document'... Remove this comment to see the full error message
|
|
||||||
if (!!document.views && document.views.length > 0) {
|
if (!!document.views && document.views.length > 0) {
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'views' does not exist on type 'Document'... Remove this comment to see the full error message
|
|
||||||
data.lastViewedAt = document.views[0].updatedAt;
|
data.lastViewedAt = document.views[0].updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.isPublic) {
|
if (!options.isPublic) {
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'boolean' is not assignable to type 'undefine... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2322) FIXME: Type 'boolean' is not assignable to type 'undefine... Remove this comment to see the full error message
|
||||||
data.pinned = !!document.pinnedById;
|
data.pinned = !!document.pinnedById;
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
|
||||||
data.collectionId = document.collectionId;
|
data.collectionId = document.collectionId;
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
|
|
||||||
data.parentDocumentId = document.parentDocumentId;
|
data.parentDocumentId = document.parentDocumentId;
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'UserPresentation | null | undefined' is not ... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2322) FIXME: Type 'UserPresentation | null | undefined' is not ... Remove this comment to see the full error message
|
||||||
data.createdBy = presentUser(document.createdBy);
|
data.createdBy = presentUser(document.createdBy);
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'UserPresentation | null | undefined' is not ... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2322) FIXME: Type 'UserPresentation | null | undefined' is not ... Remove this comment to see the full error message
|
||||||
data.updatedBy = presentUser(document.updatedBy);
|
data.updatedBy = presentUser(document.updatedBy);
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collaboratorIds' does not exist on type ... Remove this comment to see the full error message
|
|
||||||
data.collaboratorIds = document.collaboratorIds;
|
data.collaboratorIds = document.collaboratorIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1106,6 +1106,7 @@ router.post("documents.update", auth(), async (ctx) => {
|
|||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
|
fullWidth,
|
||||||
publish,
|
publish,
|
||||||
autosave,
|
autosave,
|
||||||
done,
|
done,
|
||||||
@@ -1128,10 +1129,12 @@ router.post("documents.update", auth(), async (ctx) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previousTitle = document.title;
|
const previousTitle = document.title;
|
||||||
|
|
||||||
// Update document
|
// Update document
|
||||||
if (title) document.title = title;
|
if (title) document.title = title;
|
||||||
if (editorVersion) document.editorVersion = editorVersion;
|
if (editorVersion) document.editorVersion = editorVersion;
|
||||||
if (templateId) document.templateId = templateId;
|
if (templateId) document.templateId = templateId;
|
||||||
|
if (fullWidth !== undefined) document.fullWidth = fullWidth;
|
||||||
|
|
||||||
if (!user.team?.collaborativeEditing) {
|
if (!user.team?.collaborativeEditing) {
|
||||||
if (append) {
|
if (append) {
|
||||||
|
|||||||
@@ -241,6 +241,7 @@
|
|||||||
"Move": "Move",
|
"Move": "Move",
|
||||||
"Enable embeds": "Enable embeds",
|
"Enable embeds": "Enable embeds",
|
||||||
"Disable embeds": "Disable embeds",
|
"Disable embeds": "Disable embeds",
|
||||||
|
"Full width": "Full width",
|
||||||
"Move {{ documentName }}": "Move {{ documentName }}",
|
"Move {{ documentName }}": "Move {{ documentName }}",
|
||||||
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
|
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
|
||||||
"Export options": "Export options",
|
"Export options": "Export options",
|
||||||
|
|||||||
Reference in New Issue
Block a user