mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6ba98b262 | |||
| 4fc52b9f3d | |||
| 619b369295 | |||
| 6f4cdc9359 | |||
| 4fb5022324 | |||
| 70e79a5264 | |||
| da3d08566e | |||
| 44a484e456 | |||
| 75e6734388 | |||
| a6ee913211 | |||
| 525d34a1f7 | |||
| 6dde45cee9 | |||
| 3b07a53093 | |||
| b25a98c114 | |||
| 2ae30a36cd | |||
| b2e6a1a4f6 | |||
| d48a1eed35 | |||
| a55d95c7b2 | |||
| 5d2be0a199 | |||
| 8911020f34 | |||
| 3864443eae | |||
| 660e9682c5 | |||
| ca36db1ac3 | |||
| ff87da523f | |||
| 633ccc0800 | |||
| 755ceab7dd | |||
| e55290f118 |
@@ -91,6 +91,7 @@ class DropToImport extends React.Component<Props> {
|
||||
match,
|
||||
history,
|
||||
staticContext,
|
||||
ui,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { observer, Observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
@@ -9,7 +9,10 @@ import Document from "models/Document";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import DropToImport from "components/DropToImport";
|
||||
import Flex from "components/Flex";
|
||||
import { SidebarDnDContext } from "./Collections";
|
||||
import DocumentLink from "./DocumentLink";
|
||||
import Draggable from "./Draggable";
|
||||
import Droppable from "./Droppable";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import CollectionMenu from "menus/CollectionMenu";
|
||||
@@ -48,46 +51,66 @@ class CollectionLink extends React.Component<Props> {
|
||||
collectionId={collection.id}
|
||||
activeClassName="activeDropZone"
|
||||
>
|
||||
<SidebarLink
|
||||
key={collection.id}
|
||||
to={collection.url}
|
||||
icon={<CollectionIcon collection={collection} expanded={expanded} />}
|
||||
iconColor={collection.color}
|
||||
expanded={expanded}
|
||||
hideDisclosure
|
||||
menuOpen={this.menuOpen}
|
||||
label={
|
||||
<EditableTitle
|
||||
title={collection.name}
|
||||
onSubmit={this.handleTitleChange}
|
||||
canUpdate={canUpdate}
|
||||
/>
|
||||
}
|
||||
exact={false}
|
||||
menu={
|
||||
<CollectionMenu
|
||||
position="right"
|
||||
collection={collection}
|
||||
onOpen={() => (this.menuOpen = true)}
|
||||
onClose={() => (this.menuOpen = false)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex column>
|
||||
{collection.documents.map((node) => (
|
||||
<DocumentLink
|
||||
key={node.id}
|
||||
node={node}
|
||||
documents={documents}
|
||||
collection={collection}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
canUpdate={canUpdate}
|
||||
depth={1.5}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</SidebarLink>
|
||||
<SidebarDnDContext.Consumer>
|
||||
{({ draggingDocumentId, isDragging }) => (
|
||||
<Droppable collectionId={collection.id}>
|
||||
{(provided, snapshot) => (
|
||||
<SidebarLink
|
||||
key={collection.id}
|
||||
to={collection.url}
|
||||
icon={
|
||||
<CollectionIcon
|
||||
collection={collection}
|
||||
expanded={expanded}
|
||||
/>
|
||||
}
|
||||
iconColor={collection.color}
|
||||
expanded={
|
||||
isDragging ? expanded || snapshot.isDraggingOver : expanded
|
||||
}
|
||||
hideDisclosure
|
||||
menuOpen={this.menuOpen}
|
||||
label={
|
||||
<EditableTitle
|
||||
title={collection.name}
|
||||
onSubmit={this.handleTitleChange}
|
||||
canUpdate={canUpdate}
|
||||
/>
|
||||
}
|
||||
exact={false}
|
||||
menu={
|
||||
<CollectionMenu
|
||||
position="right"
|
||||
collection={collection}
|
||||
onOpen={() => (this.menuOpen = true)}
|
||||
onClose={() => (this.menuOpen = false)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex column>
|
||||
<Observer>
|
||||
{() =>
|
||||
collection.documents.map((node, index) => (
|
||||
<DocumentLink
|
||||
key={node.id}
|
||||
node={node}
|
||||
index={index}
|
||||
documents={documents}
|
||||
collection={collection}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
canUpdate={canUpdate}
|
||||
depth={1.5}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Observer>
|
||||
</Flex>
|
||||
</SidebarLink>
|
||||
)}
|
||||
</Droppable>
|
||||
)}
|
||||
</SidebarDnDContext.Consumer>
|
||||
</DropToImport>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import type { DropResult, BeforeCapture } from "react-beautiful-dnd";
|
||||
import { DragDropContext } from "react-beautiful-dnd";
|
||||
import keydown from "react-keydown";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
|
||||
@@ -15,8 +17,28 @@ import CollectionLink from "./CollectionLink";
|
||||
import CollectionsLoading from "./CollectionsLoading";
|
||||
import Header from "./Header";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import {
|
||||
DROPPABLE_COLLECTION_SUFFIX,
|
||||
DROPPABLE_DOCUMENT_SUFFIX,
|
||||
DROPPABLE_DOCUMENT_SEPARATOR,
|
||||
} from "utils/dnd";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
|
||||
type SidebarDnDContextObject = {
|
||||
isDragging: boolean,
|
||||
draggingDocumentId?: string,
|
||||
};
|
||||
|
||||
const initialSidebarDnDContextValue: SidebarDnDContextObject = {
|
||||
isDragging: false,
|
||||
draggingDocumentId: undefined,
|
||||
};
|
||||
|
||||
//$FlowFixMe
|
||||
export const SidebarDnDContext = React.createContext(
|
||||
initialSidebarDnDContextValue
|
||||
);
|
||||
|
||||
type Props = {
|
||||
history: RouterHistory,
|
||||
policies: PoliciesStore,
|
||||
@@ -26,8 +48,16 @@ type Props = {
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
type State = {
|
||||
draggingDocumentId?: string,
|
||||
isDragging: boolean,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Collections extends React.Component<Props> {
|
||||
class Collections extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
isDragging: false,
|
||||
};
|
||||
isPreloaded: boolean = !!this.props.collections.orderedData.length;
|
||||
|
||||
componentDidMount() {
|
||||
@@ -51,22 +81,139 @@ class Collections extends React.Component<Props> {
|
||||
this.props.history.push(newDocumentUrl(activeCollectionId));
|
||||
}
|
||||
|
||||
getDroppableIdParts(droppableId: string) {
|
||||
let collection, parentDocumentId;
|
||||
const { collections } = this.props;
|
||||
|
||||
if (droppableId.indexOf(DROPPABLE_COLLECTION_SUFFIX) === 0) {
|
||||
collection = collections.get(
|
||||
droppableId.substring(DROPPABLE_COLLECTION_SUFFIX.length)
|
||||
);
|
||||
} else if (
|
||||
droppableId.indexOf(DROPPABLE_DOCUMENT_SUFFIX) === 0 &&
|
||||
droppableId.indexOf(DROPPABLE_DOCUMENT_SEPARATOR)
|
||||
) {
|
||||
const [documentId, collectionId] = droppableId
|
||||
.substring(DROPPABLE_DOCUMENT_SUFFIX.length)
|
||||
.split(DROPPABLE_DOCUMENT_SEPARATOR);
|
||||
|
||||
parentDocumentId = documentId;
|
||||
|
||||
collection = collections.get(collectionId);
|
||||
}
|
||||
|
||||
return {
|
||||
collection,
|
||||
parentDocumentId,
|
||||
};
|
||||
}
|
||||
|
||||
handleBeforeCapture = (before: BeforeCapture) => {
|
||||
this.setState({
|
||||
isDragging: true,
|
||||
draggingDocumentId: before.draggableId,
|
||||
});
|
||||
};
|
||||
|
||||
reorder = (result: DropResult) => {
|
||||
this.setState({
|
||||
isDragging: false,
|
||||
draggingDocumentId: undefined,
|
||||
});
|
||||
|
||||
// Bail out early if result doesn't have a destination or combine data
|
||||
if (!result.destination && !result.combine) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail out early if no changes
|
||||
if (
|
||||
(result.destination &&
|
||||
result.destination.droppableId === result.source.droppableId &&
|
||||
result.destination.index === result.source.index) ||
|
||||
(result.combine && result.combine.draggableId === result.draggableId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { documents } = this.props;
|
||||
const document = documents.get(result.draggableId);
|
||||
let collection,
|
||||
parentDocumentId,
|
||||
index = 0;
|
||||
|
||||
// Bail out if document doesn't exist
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.destination) {
|
||||
index = result.destination.index;
|
||||
const droppableId = result.destination.droppableId;
|
||||
const parts = this.getDroppableIdParts(droppableId);
|
||||
|
||||
collection = parts.collection;
|
||||
parentDocumentId = parts.parentDocumentId;
|
||||
} else if (result.combine) {
|
||||
const { draggableId, droppableId } = result.combine;
|
||||
const parts = this.getDroppableIdParts(droppableId);
|
||||
|
||||
collection = parts.collection;
|
||||
parentDocumentId = draggableId;
|
||||
}
|
||||
|
||||
// Bail out if collection doesn't exist
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentDocumentId) {
|
||||
// Bail out if moving document to itself
|
||||
if (parentDocumentId === document.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentDocument = documents.get(parentDocumentId);
|
||||
|
||||
// Bail out if parent document doesn't exist
|
||||
if (!parentDocument) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
documents.move(document, collection.id, parentDocumentId, index);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collections, ui, policies, documents } = this.props;
|
||||
const { draggingDocumentId, isDragging } = this.state;
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{collections.orderedData.map((collection) => (
|
||||
<CollectionLink
|
||||
key={collection.id}
|
||||
documents={documents}
|
||||
collection={collection}
|
||||
activeDocument={documents.active}
|
||||
prefetchDocument={documents.prefetchDocument}
|
||||
canUpdate={policies.abilities(collection.id).update}
|
||||
ui={ui}
|
||||
/>
|
||||
))}
|
||||
<React.Fragment>
|
||||
<DragDropContext
|
||||
onBeforeCapture={this.handleBeforeCapture}
|
||||
onDragEnd={this.reorder}
|
||||
>
|
||||
<SidebarDnDContext.Provider
|
||||
value={{
|
||||
draggingDocumentId,
|
||||
isDragging,
|
||||
}}
|
||||
>
|
||||
{collections.orderedData.map((collection) => (
|
||||
<CollectionLink
|
||||
key={collection.id}
|
||||
documents={documents}
|
||||
collection={collection}
|
||||
activeDocument={documents.active}
|
||||
prefetchDocument={documents.prefetchDocument}
|
||||
canUpdate={policies.abilities(collection.id).update}
|
||||
ui={ui}
|
||||
/>
|
||||
))}
|
||||
<div id="sidebar-collections-portal" />
|
||||
</SidebarDnDContext.Provider>
|
||||
</DragDropContext>
|
||||
<SidebarLink
|
||||
to="/collections"
|
||||
onClick={this.props.onCreateCollection}
|
||||
@@ -74,7 +221,7 @@ class Collections extends React.Component<Props> {
|
||||
label="New collection…"
|
||||
exact
|
||||
/>
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { observer, Observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
@@ -9,6 +9,8 @@ import Document from "models/Document";
|
||||
import DropToImport from "components/DropToImport";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import { SidebarDnDContext } from "./Collections";
|
||||
import Draggable from "./Draggable";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import DocumentMenu from "menus/DocumentMenu";
|
||||
@@ -17,12 +19,15 @@ import { type NavigationNode } from "types";
|
||||
type Props = {|
|
||||
node: NavigationNode,
|
||||
documents: DocumentsStore,
|
||||
collection: Collection,
|
||||
canUpdate: boolean,
|
||||
collection?: Collection,
|
||||
collection: Collection,
|
||||
index?: number,
|
||||
activeDocument: ?Document,
|
||||
activeDocumentRef?: (?HTMLElement) => void,
|
||||
prefetchDocument: (documentId: string) => Promise<void>,
|
||||
depth: number,
|
||||
isDropDisabled?: boolean,
|
||||
|};
|
||||
|
||||
@observer
|
||||
@@ -83,6 +88,8 @@ class DocumentLink extends React.Component<Props> {
|
||||
activeDocumentRef,
|
||||
prefetchDocument,
|
||||
depth,
|
||||
index,
|
||||
isDropDisabled,
|
||||
canUpdate,
|
||||
} = this.props;
|
||||
|
||||
@@ -98,62 +105,78 @@ class DocumentLink extends React.Component<Props> {
|
||||
const document = documents.get(node.id);
|
||||
const title = node.title || "Untitled";
|
||||
|
||||
let hideDisclosure;
|
||||
if (!this.hasChildDocuments()) {
|
||||
hideDisclosure = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
column
|
||||
key={node.id}
|
||||
ref={this.isActiveDocument() ? activeDocumentRef : undefined}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
>
|
||||
<DropToImport documentId={node.id} activeClassName="activeDropZone">
|
||||
<SidebarLink
|
||||
to={{
|
||||
pathname: node.url,
|
||||
state: { title: node.title },
|
||||
}}
|
||||
expanded={showChildren ? true : undefined}
|
||||
label={
|
||||
<EditableTitle
|
||||
title={title}
|
||||
onSubmit={this.handleTitleChange}
|
||||
canUpdate={canUpdate}
|
||||
/>
|
||||
}
|
||||
depth={depth}
|
||||
exact={false}
|
||||
menuOpen={this.menuOpen}
|
||||
menu={
|
||||
document ? (
|
||||
<Fade>
|
||||
<DocumentMenu
|
||||
position="right"
|
||||
document={document}
|
||||
onOpen={() => (this.menuOpen = true)}
|
||||
onClose={() => (this.menuOpen = false)}
|
||||
/>
|
||||
</Fade>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{this.hasChildDocuments() && (
|
||||
<DocumentChildren column>
|
||||
{node.children.map((childNode) => (
|
||||
<DocumentLink
|
||||
key={childNode.id}
|
||||
collection={collection}
|
||||
node={childNode}
|
||||
documents={documents}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
depth={depth + 1}
|
||||
<>
|
||||
{/* <DropToImport documentId={node.id} activeClassName="activeDropZone"> */}
|
||||
<SidebarDnDContext.Consumer>
|
||||
{({ draggingDocumentId, isDragging }) => {
|
||||
const disableChildDrops =
|
||||
isDropDisabled || draggingDocumentId === node.id;
|
||||
|
||||
return (
|
||||
<SidebarLink
|
||||
to={{
|
||||
pathname: node.url,
|
||||
state: { title: node.title },
|
||||
}}
|
||||
expanded={showChildren ? true : undefined}
|
||||
hideDisclosure={hideDisclosure}
|
||||
label={
|
||||
<EditableTitle
|
||||
title={title}
|
||||
onSubmit={this.handleTitleChange}
|
||||
canUpdate={canUpdate}
|
||||
/>
|
||||
))}
|
||||
</DocumentChildren>
|
||||
)}
|
||||
</SidebarLink>
|
||||
</DropToImport>
|
||||
</Flex>
|
||||
}
|
||||
depth={depth}
|
||||
exact={false}
|
||||
menuOpen={this.menuOpen}
|
||||
menu={
|
||||
document ? (
|
||||
<Fade>
|
||||
<DocumentMenu
|
||||
position="right"
|
||||
document={document}
|
||||
onOpen={() => (this.menuOpen = true)}
|
||||
onClose={() => (this.menuOpen = false)}
|
||||
/>
|
||||
</Fade>
|
||||
) : undefined
|
||||
}
|
||||
index={index}
|
||||
draggableId={node.id}
|
||||
>
|
||||
{this.hasChildDocuments() && !disableChildDrops && (
|
||||
<Observer>
|
||||
{() =>
|
||||
node.children.map((childNode, index) => (
|
||||
<DocumentLink
|
||||
key={childNode.id}
|
||||
index={index}
|
||||
collection={collection}
|
||||
node={childNode}
|
||||
documents={documents}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
canUpdate={canUpdate}
|
||||
depth={depth + 1}
|
||||
isDropDisabled={disableChildDrops}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Observer>
|
||||
)}
|
||||
</SidebarLink>
|
||||
);
|
||||
}}
|
||||
</SidebarDnDContext.Consumer>
|
||||
{/* </DropToImport> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Draggable as DnDDraggable } from "react-beautiful-dnd";
|
||||
import type {
|
||||
DraggableProvided,
|
||||
DraggableStateSnapshot,
|
||||
} from "react-beautiful-dnd";
|
||||
import ReactDOM from "react-dom";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
|
||||
type Props = {
|
||||
draggableId: string,
|
||||
index: number,
|
||||
children: React.Node,
|
||||
};
|
||||
|
||||
type InnerProps = {
|
||||
provided: DraggableProvided,
|
||||
snapshot: DraggableStateSnapshot,
|
||||
children: React.Node,
|
||||
};
|
||||
|
||||
class Inner extends React.Component<InnerProps> {
|
||||
render() {
|
||||
const { provided, snapshot, children } = this.props;
|
||||
|
||||
const child = (
|
||||
<DropContainer
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
isDraggingOver={Boolean(snapshot.combineTargetFor)}
|
||||
>
|
||||
{children}
|
||||
</DropContainer>
|
||||
);
|
||||
|
||||
const portalContainer = document.getElementById(
|
||||
"sidebar-collections-portal"
|
||||
);
|
||||
|
||||
if (snapshot.isDragging && portalContainer) {
|
||||
return ReactDOM.createPortal(child, portalContainer);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class Draggable extends React.Component<Props> {
|
||||
render() {
|
||||
const { draggableId, index, children } = this.props;
|
||||
|
||||
return (
|
||||
<DnDDraggable draggableId={draggableId} index={index}>
|
||||
{(provided, snapshot) => children}
|
||||
</DnDDraggable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const DropContainer = styled.div((props) => ({
|
||||
backgroundColor: props.isDraggingOver
|
||||
? props.theme.sidebarDroppableBackground
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
export default withTheme(Draggable);
|
||||
@@ -0,0 +1,63 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Droppable as DnDDroppable } from "react-beautiful-dnd";
|
||||
import type {
|
||||
DroppableProvided,
|
||||
DroppableStateSnapshot,
|
||||
} from "react-beautiful-dnd";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import {
|
||||
DROPPABLE_COLLECTION_SUFFIX,
|
||||
DROPPABLE_DOCUMENT_SUFFIX,
|
||||
DROPPABLE_DOCUMENT_SEPARATOR,
|
||||
} from "utils/dnd";
|
||||
|
||||
type Props = {
|
||||
collectionId: string,
|
||||
documentId?: string,
|
||||
isDropDisabled?: boolean,
|
||||
children(
|
||||
provided: DroppableProvided,
|
||||
snapshot: DroppableStateSnapshot
|
||||
): React.Node,
|
||||
};
|
||||
|
||||
class Droppable extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
isDropDisabled: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collectionId, documentId, isDropDisabled, children } = this.props;
|
||||
let droppableId;
|
||||
|
||||
if (documentId) {
|
||||
droppableId = `${DROPPABLE_DOCUMENT_SUFFIX}${documentId}${DROPPABLE_DOCUMENT_SEPARATOR}${collectionId}`;
|
||||
} else {
|
||||
droppableId = `${DROPPABLE_COLLECTION_SUFFIX}${collectionId}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<DnDDroppable droppableId={droppableId} isDropDisabled={isDropDisabled}>
|
||||
{(provided, snapshot) => (
|
||||
<DropContainer
|
||||
isDraggingOver={snapshot.isDraggingOver}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{children(provided, snapshot)}
|
||||
{provided.placeholder}
|
||||
</DropContainer>
|
||||
)}
|
||||
</DnDDroppable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const DropContainer = styled.div((props) => ({
|
||||
backgroundColor: props.isDraggingOver
|
||||
? props.theme.sidebarDroppableBackground
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
export default withTheme(Droppable);
|
||||
@@ -9,7 +9,7 @@ type Props = {|
|
||||
canUpdate: boolean,
|
||||
|};
|
||||
|
||||
function EditableTitle({ title, onSubmit, canUpdate }: Props) {
|
||||
function EditableTitle({ title, onSubmit, canUpdate, ...rest }: Props) {
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const [originalValue, setOriginalValue] = React.useState(title);
|
||||
const [value, setValue] = React.useState(title);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { Draggable as DnDDraggable } from "react-beautiful-dnd";
|
||||
import { withRouter, NavLink } from "react-router-dom";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
@@ -17,6 +18,8 @@ type Props = {
|
||||
menu?: React.Node,
|
||||
menuOpen?: boolean,
|
||||
hideDisclosure?: boolean,
|
||||
draggableId?: string,
|
||||
index?: number,
|
||||
iconColor?: string,
|
||||
active?: boolean,
|
||||
theme: Object,
|
||||
@@ -34,6 +37,8 @@ function SidebarLink({
|
||||
menu,
|
||||
menuOpen,
|
||||
hideDisclosure,
|
||||
draggableId,
|
||||
index,
|
||||
theme,
|
||||
exact,
|
||||
href,
|
||||
@@ -75,28 +80,45 @@ function SidebarLink({
|
||||
...style,
|
||||
};
|
||||
|
||||
const linkElement = (props) => (
|
||||
<StyledNavLink
|
||||
activeStyle={activeStyle}
|
||||
style={active ? activeStyle : style}
|
||||
onClick={onClick}
|
||||
exact={exact !== false}
|
||||
to={to}
|
||||
as={to ? undefined : href ? "a" : "div"}
|
||||
href={href}
|
||||
{...props}
|
||||
>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<Label onClick={handleExpand}>
|
||||
{showDisclosure && (
|
||||
<Disclosure expanded={expanded} onClick={handleClick} />
|
||||
)}
|
||||
{label}
|
||||
</Label>
|
||||
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
|
||||
</StyledNavLink>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper column>
|
||||
<StyledNavLink
|
||||
activeStyle={activeStyle}
|
||||
style={active ? activeStyle : style}
|
||||
onClick={onClick}
|
||||
exact={exact !== false}
|
||||
to={to}
|
||||
as={to ? undefined : href ? "a" : "div"}
|
||||
href={href}
|
||||
>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<Label onClick={handleExpand}>
|
||||
{showDisclosure && (
|
||||
<Disclosure expanded={expanded} onClick={handleClick} />
|
||||
)}
|
||||
{label}
|
||||
</Label>
|
||||
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
|
||||
</StyledNavLink>
|
||||
<>
|
||||
{draggableId ? (
|
||||
<DnDDraggable draggableId={draggableId} index={index}>
|
||||
{(provided, snapshot) =>
|
||||
linkElement({
|
||||
innerRef: provided.innerRef,
|
||||
...provided.draggableProps,
|
||||
...provided.dragHandleProps,
|
||||
})
|
||||
}
|
||||
</DnDDraggable>
|
||||
) : (
|
||||
linkElement()
|
||||
)}
|
||||
{expanded && children}
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,4 +192,8 @@ const Disclosure = styled(CollapsedIcon)`
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
`;
|
||||
|
||||
const ChildrenWrapper = styled.div(({ expanded }) => ({
|
||||
display: expanded ? "block" : "none",
|
||||
}));
|
||||
|
||||
export default withRouter(withTheme(observer(SidebarLink)));
|
||||
|
||||
@@ -100,6 +100,60 @@ export default class Collection extends BaseModel {
|
||||
return [];
|
||||
}
|
||||
|
||||
@action
|
||||
addDocumentToStructure(
|
||||
document: NavigationNode,
|
||||
parentDocumentId: ?string,
|
||||
index: ?number
|
||||
) {
|
||||
if (!parentDocumentId) {
|
||||
this.documents.splice(
|
||||
index !== undefined && index !== null ? index : this.documents.length,
|
||||
0,
|
||||
document
|
||||
);
|
||||
} else {
|
||||
const recursivelyAddDocument = (nodes: NavigationNode[]) => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].id === parentDocumentId) {
|
||||
nodes[i].children.splice(
|
||||
index !== undefined && index !== null
|
||||
? index
|
||||
: nodes[i].children.length,
|
||||
0,
|
||||
document
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
const isAdded = recursivelyAddDocument(nodes[i].children);
|
||||
if (isAdded) return true;
|
||||
}
|
||||
};
|
||||
|
||||
recursivelyAddDocument(this.documents);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
removeDocumentInStructure(documentId: string) {
|
||||
const recursivelyRemoveDocument = (nodes) => {
|
||||
const index = nodes.findIndex((item) => item.id === documentId);
|
||||
if (index !== -1) {
|
||||
nodes.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const isFound = recursivelyRemoveDocument(nodes[i].children);
|
||||
if (isFound) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return recursivelyRemoveDocument(this.documents);
|
||||
}
|
||||
|
||||
toJS = () => {
|
||||
return pick(this, [
|
||||
"id",
|
||||
|
||||
@@ -7,7 +7,12 @@ import naturalSort from "shared/utils/naturalSort";
|
||||
import BaseStore from "stores/BaseStore";
|
||||
import RootStore from "stores/RootStore";
|
||||
import Document from "models/Document";
|
||||
import type { FetchOptions, PaginationParams, SearchResult } from "types";
|
||||
import type {
|
||||
FetchOptions,
|
||||
PaginationParams,
|
||||
SearchResult,
|
||||
NavigationNode,
|
||||
} from "types";
|
||||
import { client } from "utils/ApiClient";
|
||||
|
||||
type ImportOptions = {
|
||||
@@ -430,15 +435,49 @@ export default class DocumentsStore extends BaseStore<Document> {
|
||||
move = async (
|
||||
document: Document,
|
||||
collectionId: string,
|
||||
parentDocumentId: ?string
|
||||
parentDocumentId: ?string,
|
||||
index: ?number
|
||||
) => {
|
||||
const oldCollection = this.rootStore.collections.get(document.collectionId);
|
||||
let newCollection = oldCollection;
|
||||
|
||||
if (document.collectionId !== collectionId) {
|
||||
newCollection = this.rootStore.collections.get(collectionId);
|
||||
}
|
||||
|
||||
// Update UI
|
||||
if (oldCollection && newCollection) {
|
||||
// Retrive all children documents
|
||||
const childDocuments = oldCollection.getDocumentChildren(document.id);
|
||||
// Remove document from old collection
|
||||
oldCollection.removeDocumentInStructure(document.id);
|
||||
|
||||
// Recreate navigation node object
|
||||
const navigationNode: NavigationNode = {
|
||||
id: document.id,
|
||||
title: document.title,
|
||||
url: document.url,
|
||||
children: childDocuments,
|
||||
};
|
||||
|
||||
// Move document to new location
|
||||
newCollection.addDocumentToStructure(
|
||||
navigationNode,
|
||||
parentDocumentId,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
// Send data to server
|
||||
const res = await client.post("/documents.move", {
|
||||
id: document.id,
|
||||
collectionId,
|
||||
parentDocumentId,
|
||||
index: String(index),
|
||||
});
|
||||
invariant(res && res.data, "Data not available");
|
||||
|
||||
// Apply data from the server
|
||||
res.data.documents.forEach(this.add);
|
||||
res.data.collections.forEach(this.rootStore.collections.add);
|
||||
this.addPolicies(res.policies);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// @flow
|
||||
export const DROPPABLE_COLLECTION_SUFFIX = "droppable-collection-";
|
||||
export const DROPPABLE_DOCUMENT_SUFFIX = "droppable-document-";
|
||||
export const DROPPABLE_DOCUMENT_SEPARATOR = "_collection_";
|
||||
@@ -133,6 +133,7 @@
|
||||
"react": "^16.8.6",
|
||||
"react-autosize-textarea": "^6.0.0",
|
||||
"react-avatar-editor": "^10.3.0",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
"react-color": "^2.17.3",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-dropzone": "4.2.1",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import naturalSort from "../../shared/utils/naturalSort";
|
||||
import { Collection } from "../models";
|
||||
|
||||
type Document = {
|
||||
@@ -9,15 +8,6 @@ type Document = {
|
||||
url: string,
|
||||
};
|
||||
|
||||
const sortDocuments = (documents: Document[]): Document[] => {
|
||||
const orderedDocs = naturalSort(documents, "title");
|
||||
|
||||
return orderedDocs.map((document) => ({
|
||||
...document,
|
||||
children: sortDocuments(document.children),
|
||||
}));
|
||||
};
|
||||
|
||||
export default function present(collection: Collection) {
|
||||
const data = {
|
||||
id: collection.id,
|
||||
@@ -30,11 +20,8 @@ export default function present(collection: Collection) {
|
||||
createdAt: collection.createdAt,
|
||||
updatedAt: collection.updatedAt,
|
||||
deletedAt: collection.deletedAt,
|
||||
documents: undefined,
|
||||
documents: collection.documentStructure ? collection.documentStructure : [],
|
||||
};
|
||||
|
||||
// Force alphabetical sorting
|
||||
data.documents = sortDocuments(collection.documentStructure);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ export const light = {
|
||||
sidebarBackground: colors.warmGrey,
|
||||
sidebarItemBackground: colors.black10,
|
||||
sidebarText: "rgb(78, 92, 110)",
|
||||
sidebarDroppableBackground: "rgba(0, 0, 0, .05)",
|
||||
shadow: "rgba(0, 0, 0, 0.2)",
|
||||
|
||||
menuBackground: colors.white,
|
||||
@@ -182,6 +183,7 @@ export const dark = {
|
||||
sidebarBackground: colors.veryDarkBlue,
|
||||
sidebarItemBackground: colors.veryDarkBlue,
|
||||
sidebarText: colors.slate,
|
||||
sidebarDroppableBackground: "rgba(255, 255, 255, .05)",
|
||||
shadow: "rgba(0, 0, 0, 0.6)",
|
||||
|
||||
menuBorder: lighten(0.1, colors.almostBlack),
|
||||
|
||||
Reference in New Issue
Block a user