feat: Editable titles in sidebar (#1544)
* feat/editable-titles * feat: Double click to edit titles in the sidebar * Take into account policies * fix: Title update on another client * Improved styling
This commit is contained in:
parent
ab3613af48
commit
b3b71d2dc7
|
@ -10,27 +10,34 @@ import CollectionIcon from "components/CollectionIcon";
|
|||
import DropToImport from "components/DropToImport";
|
||||
import Flex from "components/Flex";
|
||||
import DocumentLink from "./DocumentLink";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import CollectionMenu from "menus/CollectionMenu";
|
||||
|
||||
type Props = {
|
||||
type Props = {|
|
||||
collection: Collection,
|
||||
ui: UiStore,
|
||||
canUpdate: boolean,
|
||||
documents: DocumentsStore,
|
||||
activeDocument: ?Document,
|
||||
prefetchDocument: (id: string) => Promise<void>,
|
||||
};
|
||||
|};
|
||||
|
||||
@observer
|
||||
class CollectionLink extends React.Component<Props> {
|
||||
@observable menuOpen = false;
|
||||
|
||||
handleTitleChange = async (name: string) => {
|
||||
await this.props.collection.save({ name });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
collection,
|
||||
documents,
|
||||
activeDocument,
|
||||
prefetchDocument,
|
||||
canUpdate,
|
||||
ui,
|
||||
} = this.props;
|
||||
const expanded = collection.id === ui.activeCollectionId;
|
||||
|
@ -49,7 +56,13 @@ class CollectionLink extends React.Component<Props> {
|
|||
expanded={expanded}
|
||||
hideDisclosure
|
||||
menuOpen={this.menuOpen}
|
||||
label={collection.name}
|
||||
label={
|
||||
<EditableTitle
|
||||
title={collection.name}
|
||||
onSubmit={this.handleTitleChange}
|
||||
canUpdate={canUpdate}
|
||||
/>
|
||||
}
|
||||
exact={false}
|
||||
menu={
|
||||
<CollectionMenu
|
||||
|
@ -69,6 +82,7 @@ class CollectionLink extends React.Component<Props> {
|
|||
collection={collection}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
canUpdate={canUpdate}
|
||||
depth={1.5}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -52,7 +52,7 @@ class Collections extends React.Component<Props> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { collections, ui, documents } = this.props;
|
||||
const { collections, ui, policies, documents } = this.props;
|
||||
|
||||
const content = (
|
||||
<>
|
||||
|
@ -63,6 +63,7 @@ class Collections extends React.Component<Props> {
|
|||
collection={collection}
|
||||
activeDocument={documents.active}
|
||||
prefetchDocument={documents.prefetchDocument}
|
||||
canUpdate={policies.abilities(collection.id).update}
|
||||
ui={ui}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -9,19 +9,21 @@ import Document from "models/Document";
|
|||
import DropToImport from "components/DropToImport";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import DocumentMenu from "menus/DocumentMenu";
|
||||
import { type NavigationNode } from "types";
|
||||
|
||||
type Props = {
|
||||
type Props = {|
|
||||
node: NavigationNode,
|
||||
documents: DocumentsStore,
|
||||
canUpdate: boolean,
|
||||
collection?: Collection,
|
||||
activeDocument: ?Document,
|
||||
activeDocumentRef?: (?HTMLElement) => void,
|
||||
prefetchDocument: (documentId: string) => Promise<void>,
|
||||
depth: number,
|
||||
};
|
||||
|};
|
||||
|
||||
@observer
|
||||
class DocumentLink extends React.Component<Props> {
|
||||
|
@ -49,6 +51,18 @@ class DocumentLink extends React.Component<Props> {
|
|||
prefetchDocument(node.id);
|
||||
};
|
||||
|
||||
handleTitleChange = async (title: string) => {
|
||||
const document = this.props.documents.get(this.props.node.id);
|
||||
if (!document) return;
|
||||
|
||||
await this.props.documents.update({
|
||||
id: document.id,
|
||||
lastRevision: document.revision,
|
||||
text: document.text,
|
||||
title,
|
||||
});
|
||||
};
|
||||
|
||||
isActiveDocument = () => {
|
||||
return (
|
||||
this.props.activeDocument &&
|
||||
|
@ -69,6 +83,7 @@ class DocumentLink extends React.Component<Props> {
|
|||
activeDocumentRef,
|
||||
prefetchDocument,
|
||||
depth,
|
||||
canUpdate,
|
||||
} = this.props;
|
||||
|
||||
const showChildren = !!(
|
||||
|
@ -81,6 +96,7 @@ class DocumentLink extends React.Component<Props> {
|
|||
this.isActiveDocument())
|
||||
);
|
||||
const document = documents.get(node.id);
|
||||
const title = node.title || "Untitled";
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
@ -96,7 +112,13 @@ class DocumentLink extends React.Component<Props> {
|
|||
state: { title: node.title },
|
||||
}}
|
||||
expanded={showChildren ? true : undefined}
|
||||
label={node.title || "Untitled"}
|
||||
label={
|
||||
<EditableTitle
|
||||
title={title}
|
||||
onSubmit={this.handleTitleChange}
|
||||
canUpdate={canUpdate}
|
||||
/>
|
||||
}
|
||||
depth={depth}
|
||||
exact={false}
|
||||
menuOpen={this.menuOpen}
|
||||
|
@ -124,6 +146,7 @@ class DocumentLink extends React.Component<Props> {
|
|||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
depth={depth + 1}
|
||||
canUpdate={canUpdate}
|
||||
/>
|
||||
))}
|
||||
</DocumentChildren>
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import useStores from "hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
onSubmit: (title: string) => Promise<void>,
|
||||
title: string,
|
||||
canUpdate: boolean,
|
||||
|};
|
||||
|
||||
function EditableTitle({ title, onSubmit, canUpdate }: Props) {
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const [originalValue, setOriginalValue] = React.useState(title);
|
||||
const [value, setValue] = React.useState(title);
|
||||
const { ui } = useStores();
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(title);
|
||||
}, [title]);
|
||||
|
||||
const handleChange = React.useCallback((event) => {
|
||||
setValue(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleDoubleClick = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsEditing(true);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event) => {
|
||||
if (event.key === "Escape") {
|
||||
setIsEditing(false);
|
||||
setValue(originalValue);
|
||||
}
|
||||
},
|
||||
[originalValue]
|
||||
);
|
||||
|
||||
const handleSave = React.useCallback(async () => {
|
||||
setIsEditing(false);
|
||||
|
||||
if (value === originalValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document) {
|
||||
try {
|
||||
await onSubmit(value);
|
||||
setOriginalValue(value);
|
||||
} catch (error) {
|
||||
setValue(originalValue);
|
||||
ui.showToast(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}, [ui, originalValue, value, onSubmit]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isEditing ? (
|
||||
<form onSubmit={handleSave}>
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
onBlur={handleSave}
|
||||
autoFocus
|
||||
/>
|
||||
</form>
|
||||
) : (
|
||||
<span onDoubleClick={canUpdate ? handleDoubleClick : undefined}>
|
||||
{value}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Input = styled.input`
|
||||
margin-left: -4px;
|
||||
background: ${(props) => props.theme.background};
|
||||
width: calc(100% - 10px);
|
||||
border-radius: 3px;
|
||||
border: 1px solid ${(props) => props.theme.inputBorderFocused};
|
||||
padding: 5px 6px;
|
||||
margin: -4px;
|
||||
height: 32px;
|
||||
|
||||
&:focus {
|
||||
outline-color: ${(props) => props.theme.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
export default EditableTitle;
|
|
@ -75,16 +75,10 @@ class DocumentScene extends React.Component<Props> {
|
|||
@observable isDirty: boolean = false;
|
||||
@observable isEmpty: boolean = true;
|
||||
@observable moveModalOpen: boolean = false;
|
||||
@observable lastRevision: number;
|
||||
@observable title: string;
|
||||
@observable lastRevision: number = this.props.document.revision;
|
||||
@observable title: string = this.props.document.title;
|
||||
getEditorText: () => string = () => this.props.document.text;
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
this.title = props.document.title;
|
||||
this.lastRevision = props.document.revision;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateIsDirty();
|
||||
this.updateBackground();
|
||||
|
@ -112,10 +106,13 @@ class DocumentScene extends React.Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
if (document.injectTemplate) {
|
||||
this.isDirty = true;
|
||||
if (!this.isDirty && document.title !== this.title) {
|
||||
this.title = document.title;
|
||||
}
|
||||
|
||||
if (document.injectTemplate) {
|
||||
document.injectTemplate = false;
|
||||
this.isDirty = true;
|
||||
}
|
||||
|
||||
this.updateBackground();
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"build:webpack": "webpack --config webpack.config.prod.js",
|
||||
"build": "yarn clean && yarn build:webpack && yarn build:server",
|
||||
"start": "node ./build/server/index.js",
|
||||
"dev": "nodemon --exec \"yarn build:server && node build/server/index.js\" -e js --ignore build/",
|
||||
"dev": "nodemon --exec \"yarn build:server && node build/server/index.js\" -e js --ignore build/ --ignore app/",
|
||||
"lint": "eslint app server shared",
|
||||
"flow": "flow",
|
||||
"deploy": "git push heroku master",
|
||||
|
@ -193,4 +193,4 @@
|
|||
"js-yaml": "^3.13.1"
|
||||
},
|
||||
"version": "0.47.1"
|
||||
}
|
||||
}
|
Reference in New Issue