feat: Inline collection editing (#1865)
This commit is contained in:
23
app/components/Arrow.js
Normal file
23
app/components/Arrow.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export default function Arrow() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="13"
|
||||||
|
height="30"
|
||||||
|
viewBox="0 0 13 30"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M7.40242 1.48635C8.23085 0.0650039 10.0656 -0.421985 11.5005 0.39863C12.9354 1.21924 13.427 3.03671 12.5986 4.45806L5.59858 16.4681C4.77015 17.8894 2.93538 18.3764 1.5005 17.5558C0.065623 16.7352 -0.426002 14.9177 0.402425 13.4964L7.40242 1.48635Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12.5986 25.5419C13.427 26.9633 12.9354 28.7808 11.5005 29.6014C10.0656 30.422 8.23087 29.935 7.40244 28.5136L0.402438 16.5036C-0.425989 15.0823 0.0656365 13.2648 1.50051 12.4442C2.93539 11.6236 4.77016 12.1106 5.59859 13.5319L12.5986 25.5419Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
212
app/components/CollectionDescription.js
Normal file
212
app/components/CollectionDescription.js
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// @flow
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { transparentize } from "polished";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Collection from "models/Collection";
|
||||||
|
import Arrow from "components/Arrow";
|
||||||
|
import ButtonLink from "components/ButtonLink";
|
||||||
|
import Editor from "components/Editor";
|
||||||
|
import LoadingIndicator from "components/LoadingIndicator";
|
||||||
|
import NudeButton from "components/NudeButton";
|
||||||
|
import useDebouncedCallback from "hooks/useDebouncedCallback";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
|
|
||||||
|
type Props = {|
|
||||||
|
collection: Collection,
|
||||||
|
|};
|
||||||
|
|
||||||
|
function CollectionDescription({ collection }: Props) {
|
||||||
|
const { collections, ui, policies } = useStores();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isExpanded, setExpanded] = React.useState(false);
|
||||||
|
const [isEditing, setEditing] = React.useState(false);
|
||||||
|
const [isDirty, setDirty] = React.useState(false);
|
||||||
|
const can = policies.abilities(collection.id);
|
||||||
|
|
||||||
|
const handleStartEditing = React.useCallback(() => {
|
||||||
|
setEditing(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleStopEditing = React.useCallback(() => {
|
||||||
|
setEditing(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClickDisclosure = React.useCallback(
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (isExpanded && document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
setExpanded(!isExpanded);
|
||||||
|
},
|
||||||
|
[isExpanded]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSave = useDebouncedCallback(async (getValue) => {
|
||||||
|
try {
|
||||||
|
await collection.save({
|
||||||
|
description: getValue(),
|
||||||
|
});
|
||||||
|
setDirty(false);
|
||||||
|
} catch (err) {
|
||||||
|
ui.showToast(
|
||||||
|
t("Sorry, an error occurred saving the collection", {
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const handleChange = React.useCallback(
|
||||||
|
(getValue) => {
|
||||||
|
setDirty(true);
|
||||||
|
handleSave(getValue);
|
||||||
|
},
|
||||||
|
[handleSave]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setEditing(false);
|
||||||
|
}, [collection.id]);
|
||||||
|
|
||||||
|
const placeholder = `${t("Add a description")}…`;
|
||||||
|
const key = isEditing || isDirty ? "draft" : collection.updatedAt;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MaxHeight data-editing={isEditing} data-expanded={isExpanded}>
|
||||||
|
<Input
|
||||||
|
$isEditable={can.update}
|
||||||
|
data-editing={isEditing}
|
||||||
|
data-expanded={isExpanded}
|
||||||
|
>
|
||||||
|
<span onClick={can.update ? handleStartEditing : undefined}>
|
||||||
|
{collections.isSaving && <LoadingIndicator />}
|
||||||
|
{collection.hasDescription || isEditing || isDirty ? (
|
||||||
|
<React.Suspense fallback={<Placeholder>Loading…</Placeholder>}>
|
||||||
|
<Editor
|
||||||
|
id={collection.id}
|
||||||
|
key={key}
|
||||||
|
defaultValue={collection.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
readOnly={!isEditing}
|
||||||
|
autoFocus={isEditing}
|
||||||
|
onBlur={handleStopEditing}
|
||||||
|
maxLength={1000}
|
||||||
|
disableEmbeds
|
||||||
|
readOnlyWriteCheckboxes
|
||||||
|
grow
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
|
) : (
|
||||||
|
can.update && <Placeholder>{placeholder}</Placeholder>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Input>
|
||||||
|
{!isEditing && (
|
||||||
|
<Disclosure
|
||||||
|
onClick={handleClickDisclosure}
|
||||||
|
aria-label={isExpanded ? t("Collapse") : t("Expand")}
|
||||||
|
size={30}
|
||||||
|
>
|
||||||
|
<Arrow />
|
||||||
|
</Disclosure>
|
||||||
|
)}
|
||||||
|
</MaxHeight>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Disclosure = styled(NudeButton)`
|
||||||
|
opacity: 0;
|
||||||
|
color: ${(props) => props.theme.divider};
|
||||||
|
position: absolute;
|
||||||
|
top: calc(25vh - 50px);
|
||||||
|
left: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
transform: rotate(-90deg) translateX(-50%);
|
||||||
|
transition: opacity 100ms ease-in-out;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: ${(props) => props.theme.sidebarText};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Placeholder = styled(ButtonLink)`
|
||||||
|
color: ${(props) => props.theme.placeholder};
|
||||||
|
cursor: text;
|
||||||
|
min-height: 27px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MaxHeight = styled.div`
|
||||||
|
position: relative;
|
||||||
|
max-height: 25vh;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: -8px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
&[data-editing="true"],
|
||||||
|
&[data-expanded="true"] {
|
||||||
|
max-height: initial;
|
||||||
|
overflow: initial;
|
||||||
|
|
||||||
|
${Disclosure} {
|
||||||
|
top: initial;
|
||||||
|
bottom: 0;
|
||||||
|
transform: rotate(90deg) translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover ${Disclosure} {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Input = styled.div`
|
||||||
|
margin: -8px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: ${(props) => props.theme.backgroundTransition};
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: calc(25vh - 50px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 50px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
${(props) => transparentize(1, props.theme.background)} 0%,
|
||||||
|
${(props) => props.theme.background} 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-editing="true"],
|
||||||
|
&[data-expanded="true"] {
|
||||||
|
&:after {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-editing="true"] {
|
||||||
|
background: ${(props) => props.theme.secondaryBackground};
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-menu-trigger,
|
||||||
|
.heading-anchor {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default observer(CollectionDescription);
|
@ -27,13 +27,16 @@ export type Props = {|
|
|||||||
autoFocus?: boolean,
|
autoFocus?: boolean,
|
||||||
template?: boolean,
|
template?: boolean,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
|
maxLength?: number,
|
||||||
scrollTo?: string,
|
scrollTo?: string,
|
||||||
|
handleDOMEvents?: Object,
|
||||||
readOnlyWriteCheckboxes?: boolean,
|
readOnlyWriteCheckboxes?: boolean,
|
||||||
onBlur?: (event: SyntheticEvent<>) => any,
|
onBlur?: (event: SyntheticEvent<>) => any,
|
||||||
onFocus?: (event: SyntheticEvent<>) => any,
|
onFocus?: (event: SyntheticEvent<>) => any,
|
||||||
onPublish?: (event: SyntheticEvent<>) => any,
|
onPublish?: (event: SyntheticEvent<>) => any,
|
||||||
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
|
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
|
||||||
onCancel?: () => any,
|
onCancel?: () => any,
|
||||||
|
onDoubleClick?: () => any,
|
||||||
onChange?: (getValue: () => string) => any,
|
onChange?: (getValue: () => string) => any,
|
||||||
onSearchLink?: (title: string) => any,
|
onSearchLink?: (title: string) => any,
|
||||||
onHoverLink?: (event: MouseEvent) => any,
|
onHoverLink?: (event: MouseEvent) => any,
|
||||||
@ -177,7 +180,7 @@ const StyledEditor = styled(RichMarkdownEditor)`
|
|||||||
justify-content: start;
|
justify-content: start;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
transition: ${(props) => props.theme.backgroundTransition};
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
& * {
|
& * {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
|
import Arrow from "components/Arrow";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
direction: "left" | "right",
|
direction: "left" | "right",
|
||||||
@ -14,22 +15,7 @@ const Toggle = React.forwardRef<Props, HTMLButtonElement>(
|
|||||||
return (
|
return (
|
||||||
<Positioner style={style}>
|
<Positioner style={style}>
|
||||||
<ToggleButton ref={ref} $direction={direction} onClick={onClick}>
|
<ToggleButton ref={ref} $direction={direction} onClick={onClick}>
|
||||||
<svg
|
<Arrow />
|
||||||
width="13"
|
|
||||||
height="30"
|
|
||||||
viewBox="0 0 13 30"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M7.40242 1.48635C8.23085 0.0650039 10.0656 -0.421985 11.5005 0.39863C12.9354 1.21924 13.427 3.03671 12.5986 4.45806L5.59858 16.4681C4.77015 17.8894 2.93538 18.3764 1.5005 17.5558C0.065623 16.7352 -0.426002 14.9177 0.402425 13.4964L7.40242 1.48635Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M12.5986 25.5419C13.427 26.9633 12.9354 28.7808 11.5005 29.6014C10.0656 30.422 8.23087 29.935 7.40244 28.5136L0.402438 16.5036C-0.425989 15.0823 0.0656365 13.2648 1.50051 12.4442C2.93539 11.6236 4.77016 12.1106 5.59859 13.5319L12.5986 25.5419Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</Positioner>
|
</Positioner>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Toast from "./components/Toast";
|
import Toast from "components/Toast";
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
|
|
||||||
function Toasts() {
|
function Toasts() {
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import Toasts from "./Toasts";
|
|
||||||
export default Toasts;
|
|
31
app/hooks/useDebouncedCallback.js
Normal file
31
app/hooks/useDebouncedCallback.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export default function useDebouncedCallback(
|
||||||
|
callback: (any) => mixed,
|
||||||
|
wait: number
|
||||||
|
) {
|
||||||
|
// track args & timeout handle between calls
|
||||||
|
const argsRef = React.useRef();
|
||||||
|
const timeout = React.useRef();
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (timeout.current) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure our timeout gets cleared if consuming component gets unmounted
|
||||||
|
React.useEffect(() => cleanup, []);
|
||||||
|
|
||||||
|
return function (...args: any) {
|
||||||
|
argsRef.current = args;
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
timeout.current = setTimeout(() => {
|
||||||
|
if (argsRef.current) {
|
||||||
|
callback(...argsRef.current);
|
||||||
|
}
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
}
|
@ -1,12 +1,11 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer, inject } from "mobx-react";
|
import { observer, inject } from "mobx-react";
|
||||||
|
|
||||||
import { NewDocumentIcon, PlusIcon, PinIcon, MoreIcon } from "outline-icons";
|
import { NewDocumentIcon, PlusIcon, PinIcon, MoreIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
||||||
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
|
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
|
||||||
import styled, { withTheme } from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import CollectionsStore from "stores/CollectionsStore";
|
import CollectionsStore from "stores/CollectionsStore";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
@ -20,9 +19,9 @@ import Search from "scenes/Search";
|
|||||||
import Actions, { Action, Separator } from "components/Actions";
|
import Actions, { Action, Separator } from "components/Actions";
|
||||||
import Button from "components/Button";
|
import Button from "components/Button";
|
||||||
import CenteredContent from "components/CenteredContent";
|
import CenteredContent from "components/CenteredContent";
|
||||||
|
import CollectionDescription from "components/CollectionDescription";
|
||||||
import CollectionIcon from "components/CollectionIcon";
|
import CollectionIcon from "components/CollectionIcon";
|
||||||
import DocumentList from "components/DocumentList";
|
import DocumentList from "components/DocumentList";
|
||||||
import Editor from "components/Editor";
|
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import Heading from "components/Heading";
|
import Heading from "components/Heading";
|
||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
@ -37,7 +36,6 @@ import Tab from "components/Tab";
|
|||||||
import Tabs from "components/Tabs";
|
import Tabs from "components/Tabs";
|
||||||
import Tooltip from "components/Tooltip";
|
import Tooltip from "components/Tooltip";
|
||||||
import CollectionMenu from "menus/CollectionMenu";
|
import CollectionMenu from "menus/CollectionMenu";
|
||||||
import { type Theme } from "types";
|
|
||||||
import { AuthorizationError } from "utils/errors";
|
import { AuthorizationError } from "utils/errors";
|
||||||
import { newDocumentUrl, collectionUrl } from "utils/routeHelpers";
|
import { newDocumentUrl, collectionUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
@ -47,7 +45,6 @@ type Props = {
|
|||||||
collections: CollectionsStore,
|
collections: CollectionsStore,
|
||||||
policies: PoliciesStore,
|
policies: PoliciesStore,
|
||||||
match: Match,
|
match: Match,
|
||||||
theme: Theme,
|
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,7 +54,6 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
@observable isFetching: boolean = true;
|
@observable isFetching: boolean = true;
|
||||||
@observable permissionsModalOpen: boolean = false;
|
@observable permissionsModalOpen: boolean = false;
|
||||||
@observable editModalOpen: boolean = false;
|
@observable editModalOpen: boolean = false;
|
||||||
@observable redirectTo: ?string;
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { id } = this.props.match.params;
|
const { id } = this.props.match.params;
|
||||||
@ -108,14 +104,6 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onNewDocument = (ev: SyntheticEvent<>) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
if (this.collection) {
|
|
||||||
this.redirectTo = newDocumentUrl(this.collection.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPermissions = (ev: SyntheticEvent<>) => {
|
onPermissions = (ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.permissionsModalOpen = true;
|
this.permissionsModalOpen = true;
|
||||||
@ -157,7 +145,12 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
delay={500}
|
delay={500}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
<Button onClick={this.onNewDocument} icon={<PlusIcon />}>
|
<Button
|
||||||
|
as={Link}
|
||||||
|
to={this.collection ? newDocumentUrl(this.collection.id) : ""}
|
||||||
|
disabled={!this.collection}
|
||||||
|
icon={<PlusIcon />}
|
||||||
|
>
|
||||||
{t("New doc")}
|
{t("New doc")}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -186,9 +179,8 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { documents, theme, t } = this.props;
|
const { documents, t } = this.props;
|
||||||
|
|
||||||
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
|
||||||
if (!this.isFetching && !this.collection) return <Search notFound />;
|
if (!this.isFetching && !this.collection) return <Search notFound />;
|
||||||
|
|
||||||
const pinnedDocuments = this.collection
|
const pinnedDocuments = this.collection
|
||||||
@ -197,7 +189,6 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
const collection = this.collection;
|
const collection = this.collection;
|
||||||
const collectionName = collection ? collection.name : "";
|
const collectionName = collection ? collection.name : "";
|
||||||
const hasPinnedDocuments = !!pinnedDocuments.length;
|
const hasPinnedDocuments = !!pinnedDocuments.length;
|
||||||
const hasDescription = collection ? collection.hasDescription : false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent>
|
<CenteredContent>
|
||||||
@ -218,7 +209,7 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
</HelpText>
|
</HelpText>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Link to={newDocumentUrl(collection.id)}>
|
<Link to={newDocumentUrl(collection.id)}>
|
||||||
<Button icon={<NewDocumentIcon color={theme.buttonText} />}>
|
<Button icon={<NewDocumentIcon color="currentColor" />}>
|
||||||
{t("Create a document")}
|
{t("Create a document")}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
@ -257,17 +248,7 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
<CollectionIcon collection={collection} size={40} expanded />{" "}
|
<CollectionIcon collection={collection} size={40} expanded />{" "}
|
||||||
{collection.name}
|
{collection.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
<CollectionDescription collection={collection} />
|
||||||
{hasDescription && (
|
|
||||||
<React.Suspense fallback={<p>Loading…</p>}>
|
|
||||||
<Editor
|
|
||||||
id={collection.id}
|
|
||||||
key={collection.description}
|
|
||||||
defaultValue={collection.description}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</React.Suspense>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasPinnedDocuments && (
|
{hasPinnedDocuments && (
|
||||||
<>
|
<>
|
||||||
@ -396,10 +377,5 @@ const Wrapper = styled(Flex)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default withTranslation()<CollectionScene>(
|
export default withTranslation()<CollectionScene>(
|
||||||
inject(
|
inject("collections", "policies", "documents", "ui")(CollectionScene)
|
||||||
"collections",
|
|
||||||
"policies",
|
|
||||||
"documents",
|
|
||||||
"ui"
|
|
||||||
)(withTheme(CollectionScene))
|
|
||||||
);
|
);
|
||||||
|
@ -11,7 +11,6 @@ import Flex from "components/Flex";
|
|||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
import IconPicker from "components/IconPicker";
|
import IconPicker from "components/IconPicker";
|
||||||
import Input from "components/Input";
|
import Input from "components/Input";
|
||||||
import InputRich from "components/InputRich";
|
|
||||||
import InputSelect from "components/InputSelect";
|
import InputSelect from "components/InputSelect";
|
||||||
import Switch from "components/Switch";
|
import Switch from "components/Switch";
|
||||||
|
|
||||||
@ -27,7 +26,6 @@ type Props = {
|
|||||||
class CollectionEdit extends React.Component<Props> {
|
class CollectionEdit extends React.Component<Props> {
|
||||||
@observable name: string = this.props.collection.name;
|
@observable name: string = this.props.collection.name;
|
||||||
@observable sharing: boolean = this.props.collection.sharing;
|
@observable sharing: boolean = this.props.collection.sharing;
|
||||||
@observable description: string = this.props.collection.description;
|
|
||||||
@observable icon: string = this.props.collection.icon;
|
@observable icon: string = this.props.collection.icon;
|
||||||
@observable color: string = this.props.collection.color || "#4E5C6E";
|
@observable color: string = this.props.collection.color || "#4E5C6E";
|
||||||
@observable private: boolean = this.props.collection.private;
|
@observable private: boolean = this.props.collection.private;
|
||||||
@ -43,7 +41,6 @@ class CollectionEdit extends React.Component<Props> {
|
|||||||
try {
|
try {
|
||||||
await this.props.collection.save({
|
await this.props.collection.save({
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
|
||||||
icon: this.icon,
|
icon: this.icon,
|
||||||
color: this.color,
|
color: this.color,
|
||||||
private: this.private,
|
private: this.private,
|
||||||
@ -69,10 +66,6 @@ class CollectionEdit extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDescriptionChange = (getValue: () => string) => {
|
|
||||||
this.description = getValue();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
||||||
this.name = ev.target.value;
|
this.name = ev.target.value;
|
||||||
};
|
};
|
||||||
@ -120,15 +113,6 @@ class CollectionEdit extends React.Component<Props> {
|
|||||||
icon={this.icon}
|
icon={this.icon}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<InputRich
|
|
||||||
id={this.props.collection.id}
|
|
||||||
label={t("Description")}
|
|
||||||
onChange={this.handleDescriptionChange}
|
|
||||||
defaultValue={this.description || ""}
|
|
||||||
placeholder={t("More details about this collection…")}
|
|
||||||
minHeight={68}
|
|
||||||
maxHeight={200}
|
|
||||||
/>
|
|
||||||
<InputSelect
|
<InputSelect
|
||||||
label={t("Sort in sidebar")}
|
label={t("Sort in sidebar")}
|
||||||
options={[
|
options={[
|
||||||
|
@ -14,7 +14,6 @@ import Flex from "components/Flex";
|
|||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
import IconPicker, { icons } from "components/IconPicker";
|
import IconPicker, { icons } from "components/IconPicker";
|
||||||
import Input from "components/Input";
|
import Input from "components/Input";
|
||||||
import InputRich from "components/InputRich";
|
|
||||||
import Switch from "components/Switch";
|
import Switch from "components/Switch";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -29,7 +28,6 @@ type Props = {
|
|||||||
@observer
|
@observer
|
||||||
class CollectionNew extends React.Component<Props> {
|
class CollectionNew extends React.Component<Props> {
|
||||||
@observable name: string = "";
|
@observable name: string = "";
|
||||||
@observable description: string = "";
|
|
||||||
@observable icon: string = "";
|
@observable icon: string = "";
|
||||||
@observable color: string = "#4E5C6E";
|
@observable color: string = "#4E5C6E";
|
||||||
@observable sharing: boolean = true;
|
@observable sharing: boolean = true;
|
||||||
@ -43,7 +41,6 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
const collection = new Collection(
|
const collection = new Collection(
|
||||||
{
|
{
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
|
||||||
sharing: this.sharing,
|
sharing: this.sharing,
|
||||||
icon: this.icon,
|
icon: this.icon,
|
||||||
color: this.color,
|
color: this.color,
|
||||||
@ -90,10 +87,6 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
this.hasOpenedIconPicker = true;
|
this.hasOpenedIconPicker = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDescriptionChange = (getValue: () => string) => {
|
|
||||||
this.description = getValue();
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePrivateChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
|
handlePrivateChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
this.private = ev.target.checked;
|
this.private = ev.target.checked;
|
||||||
};
|
};
|
||||||
@ -115,9 +108,9 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<HelpText>
|
<HelpText>
|
||||||
<Trans>
|
<Trans>
|
||||||
Collections are for grouping your knowledge base. They work best
|
Collections are for grouping your documents. They work best when
|
||||||
when organized around a topic or internal team — Product or
|
organized around a topic or internal team — Product or Engineering
|
||||||
Engineering for example.
|
for example.
|
||||||
</Trans>
|
</Trans>
|
||||||
</HelpText>
|
</HelpText>
|
||||||
<Flex>
|
<Flex>
|
||||||
@ -138,14 +131,6 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
icon={this.icon}
|
icon={this.icon}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<InputRich
|
|
||||||
label={t("Description")}
|
|
||||||
onChange={this.handleDescriptionChange}
|
|
||||||
defaultValue={this.description || ""}
|
|
||||||
placeholder={t("More details about this collection…")}
|
|
||||||
minHeight={68}
|
|
||||||
maxHeight={200}
|
|
||||||
/>
|
|
||||||
<Switch
|
<Switch
|
||||||
id="private"
|
id="private"
|
||||||
label={t("Private collection")}
|
label={t("Private collection")}
|
||||||
|
@ -154,7 +154,7 @@
|
|||||||
"react-waypoint": "^9.0.2",
|
"react-waypoint": "^9.0.2",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"reakit": "^1.3.4",
|
"reakit": "^1.3.4",
|
||||||
"rich-markdown-editor": "^11.2.0-0",
|
"rich-markdown-editor": "^11.3.0-0",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"sequelize": "^6.3.4",
|
"sequelize": "^6.3.4",
|
||||||
"sequelize-cli": "^6.2.0",
|
"sequelize-cli": "^6.2.0",
|
||||||
|
@ -8,6 +8,10 @@
|
|||||||
"Drafts": "Drafts",
|
"Drafts": "Drafts",
|
||||||
"Templates": "Templates",
|
"Templates": "Templates",
|
||||||
"Deleted Collection": "Deleted Collection",
|
"Deleted Collection": "Deleted Collection",
|
||||||
|
"Sorry, an error occurred saving the collection": "Sorry, an error occurred saving the collection",
|
||||||
|
"Add description": "Add description",
|
||||||
|
"Collapse": "Collapse",
|
||||||
|
"Expand": "Expand",
|
||||||
"Submenu": "Submenu",
|
"Submenu": "Submenu",
|
||||||
"New": "New",
|
"New": "New",
|
||||||
"Only visible to you": "Only visible to you",
|
"Only visible to you": "Only visible to you",
|
||||||
@ -108,8 +112,6 @@
|
|||||||
"Export Data": "Export Data",
|
"Export Data": "Export Data",
|
||||||
"Integrations": "Integrations",
|
"Integrations": "Integrations",
|
||||||
"Installation": "Installation",
|
"Installation": "Installation",
|
||||||
"Expand": "Expand",
|
|
||||||
"Collapse": "Collapse",
|
|
||||||
"Unstar": "Unstar",
|
"Unstar": "Unstar",
|
||||||
"Star": "Star",
|
"Star": "Star",
|
||||||
"Appearance": "Appearance",
|
"Appearance": "Appearance",
|
||||||
@ -204,8 +206,6 @@
|
|||||||
"The collection was updated": "The collection was updated",
|
"The collection was updated": "The collection was updated",
|
||||||
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "You can edit the name and other details at any time, however doing so often might confuse your team mates.",
|
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "You can edit the name and other details at any time, however doing so often might confuse your team mates.",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Description": "Description",
|
|
||||||
"More details about this collection…": "More details about this collection…",
|
|
||||||
"Alphabetical": "Alphabetical",
|
"Alphabetical": "Alphabetical",
|
||||||
"Private collection": "Private collection",
|
"Private collection": "Private collection",
|
||||||
"A private collection will only be visible to invited team members.": "A private collection will only be visible to invited team members.",
|
"A private collection will only be visible to invited team members.": "A private collection will only be visible to invited team members.",
|
||||||
@ -237,7 +237,7 @@
|
|||||||
"Never signed in": "Never signed in",
|
"Never signed in": "Never signed in",
|
||||||
"Invited": "Invited",
|
"Invited": "Invited",
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
"Collections are for grouping your knowledge base. They work best when organized around a topic or internal team — Product or Engineering for example.": "Collections are for grouping your knowledge base. They work best when organized around a topic or internal team — Product or Engineering for example.",
|
"Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.": "Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.",
|
||||||
"Creating": "Creating",
|
"Creating": "Creating",
|
||||||
"Create": "Create",
|
"Create": "Create",
|
||||||
"Recently viewed": "Recently viewed",
|
"Recently viewed": "Recently viewed",
|
||||||
|
@ -10833,10 +10833,10 @@ retry-as-promised@^3.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
any-promise "^1.3.0"
|
any-promise "^1.3.0"
|
||||||
|
|
||||||
rich-markdown-editor@^11.2.0-0:
|
rich-markdown-editor@^11.3.0-0:
|
||||||
version "11.2.0-0"
|
version "11.3.0-0"
|
||||||
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.2.0-0.tgz#8f031e2367133f3aac22cb47d150e347460d4987"
|
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.3.0-0.tgz#0034da293928e4211c5c39544038fd28a7d78a67"
|
||||||
integrity sha512-qqL44VDToMEmTQZ68r+rv9ZjgtN6s5WiXtZFIOYRGq9pUO7brvd/+WWpXY0z4dNqlAMV5nLGXGIshwKRAMbg/g==
|
integrity sha512-6iNmmiYYOXSoifkIemRG1GIp6gkEo6yAnw4FjkFTslOP0B/ZoHFworU7I3qqJ84efEhYPzEGfrGBclRPBaggAg==
|
||||||
dependencies:
|
dependencies:
|
||||||
copy-to-clipboard "^3.0.8"
|
copy-to-clipboard "^3.0.8"
|
||||||
lodash "^4.17.11"
|
lodash "^4.17.11"
|
||||||
|
Reference in New Issue
Block a user