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:
Tom Moor 2020-09-15 18:01:40 -07:00 committed by GitHub
parent ab3613af48
commit b3b71d2dc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 152 additions and 19 deletions

View File

@ -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}
/>
))}

View File

@ -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}
/>
))}

View File

@ -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>

View File

@ -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;

View File

@ -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();

View File

@ -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"
}
}