diff --git a/app/components/Sidebar/components/Bubble.js b/app/components/Sidebar/components/Bubble.js index 2d0b4c5d..8f10490d 100644 --- a/app/components/Sidebar/components/Bubble.js +++ b/app/components/Sidebar/components/Bubble.js @@ -14,18 +14,18 @@ const Bubble = ({ count }: Props) => { const Count = styled.div` animation: ${bounceIn} 600ms; transform-origin: center center; - border-radius: 100%; color: ${props => props.theme.white}; background: ${props => props.theme.slateDark}; display: inline-block; font-feature-settings: 'tnum'; font-weight: 600; font-size: 9px; - line-height: 16px; white-space: nowrap; vertical-align: baseline; min-width: 16px; min-height: 16px; + line-height: 16px; + border-radius: 8px; text-align: center; padding: 0 4px; margin-left: 8px; diff --git a/app/models/Document.js b/app/models/Document.js index a19d4e19..e22781f9 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -44,7 +44,11 @@ export default class Document extends BaseModel { @action updateTitle() { - set(this, parseTitle(this.text)); + const { title, emoji } = parseTitle(this.text); + + if (title) { + set(this, { title, emoji }); + } } @computed diff --git a/app/routes.js b/app/routes.js index 9a4005c6..cb192f7f 100644 --- a/app/routes.js +++ b/app/routes.js @@ -7,8 +7,8 @@ import Starred from 'scenes/Starred'; import Drafts from 'scenes/Drafts'; import Archive from 'scenes/Archive'; import Collection from 'scenes/Collection'; -import Document from 'scenes/Document'; import KeyedDocument from 'scenes/Document/KeyedDocument'; +import DocumentNew from 'scenes/DocumentNew'; import Search from 'scenes/Search'; import Settings from 'scenes/Settings'; import Details from 'scenes/Settings/Details'; @@ -30,7 +30,6 @@ import RouteSidebarHidden from 'components/RouteSidebarHidden'; import { matchDocumentSlug as slug } from 'utils/routeHelpers'; const NotFound = () => ; -const NewDocument = () => ; const RedirectDocument = ({ match }: { match: Object }) => ( ); @@ -77,7 +76,7 @@ export default function Routes() { { @observable editorComponent = EditorImport; @observable document: ?Document; @observable revision: ?Revision; - @observable newDocument: ?Document; @observable isUploading: boolean = false; @observable isSaving: boolean = false; @observable isPublishing: boolean = false; @@ -138,67 +136,50 @@ class DocumentScene extends React.Component { } loadDocument = async props => { - if (props.newDocument) { - this.document = new Document( - { - collectionId: props.match.params.id, - parentDocumentId: new URLSearchParams(props.location.search).get( - 'parentDocumentId' - ), - title: '', - text: '', - }, - props.documents + const { shareId, revisionId } = props.match.params; + + try { + this.document = await props.documents.fetch( + props.match.params.documentSlug, + { shareId } ); - } else { - const { shareId, revisionId } = props.match.params; - try { - this.document = await props.documents.fetch( + if (revisionId) { + this.revision = await props.revisions.fetch( props.match.params.documentSlug, - { shareId } + { revisionId } ); + } else { + this.revision = undefined; + } + } catch (err) { + this.error = err; + return; + } - if (revisionId) { - this.revision = await props.revisions.fetch( - props.match.params.documentSlug, - { revisionId } - ); - } else { - this.revision = undefined; - } - } catch (err) { - this.error = err; - return; + this.isDirty = false; + this.isEmpty = false; + + const document = this.document; + + if (document) { + this.props.ui.setActiveDocument(document); + + if (document.isArchived && this.isEditing) { + return this.goToDocumentCanonical(); } - this.isDirty = false; - this.isEmpty = false; - - const document = this.document; - - if (document) { - this.props.ui.setActiveDocument(document); - - if (document.isArchived && this.isEditing) { - return this.goToDocumentCanonical(); + if (this.props.auth.user && !shareId) { + if (!this.isEditing && document.publishedAt) { + this.viewTimeout = setTimeout(document.view, MARK_AS_VIEWED_AFTER); } - if (this.props.auth.user && !shareId) { - if (!this.isEditing && document.publishedAt) { - this.viewTimeout = setTimeout(document.view, MARK_AS_VIEWED_AFTER); - } - - const isMove = props.location.pathname.match(/move$/); - const canRedirect = !this.revision && !isMove; - if (canRedirect) { - const canonicalUrl = updateDocumentUrl( - props.match.url, - document.url - ); - if (props.location.pathname !== canonicalUrl) { - props.history.replace(canonicalUrl); - } + const isMove = props.location.pathname.match(/move$/); + const canRedirect = !this.revision && !isMove; + if (canRedirect) { + const canonicalUrl = updateDocumentUrl(props.match.url, document.url); + if (props.location.pathname !== canonicalUrl) { + props.history.replace(canonicalUrl); } } } @@ -327,7 +308,7 @@ class DocumentScene extends React.Component { title={location.state ? location.state.title : 'Untitled'} /> - + ); @@ -446,10 +427,6 @@ const Container = styled(Flex)` margin-top: ${props => (props.isShare ? '50px' : '0')}; `; -const LoadingState = styled(LoadingPlaceholder)` - margin: 40px 0; -`; - export default withRouter( inject('ui', 'auth', 'documents', 'revisions')(DocumentScene) ); diff --git a/app/scenes/Document/components/LoadingPlaceholder.js b/app/scenes/Document/components/LoadingPlaceholder.js index b7897e49..05073b9b 100644 --- a/app/scenes/Document/components/LoadingPlaceholder.js +++ b/app/scenes/Document/components/LoadingPlaceholder.js @@ -14,17 +14,21 @@ const randomValues = Array.from( const LoadingPlaceholder = (props: Object) => { return ( - + - + ); }; +const Wrapper = styled(Fade)` + margin: 40px 0; +`; + const Mask = styled(Flex)` height: ${props => (props.header ? 28 : 18)}px; margin-bottom: ${props => (props.header ? 32 : 14)}px; diff --git a/app/scenes/DocumentNew.js b/app/scenes/DocumentNew.js new file mode 100644 index 00000000..aa7345b7 --- /dev/null +++ b/app/scenes/DocumentNew.js @@ -0,0 +1,49 @@ +// @flow +import * as React from 'react'; +import { inject } from 'mobx-react'; +import type { RouterHistory, Location } from 'react-router-dom'; +import Flex from 'shared/components/Flex'; +import CenteredContent from 'components/CenteredContent'; +import LoadingPlaceholder from 'scenes/Document/components/LoadingPlaceholder'; +import DocumentsStore from 'stores/DocumentsStore'; +import UiStore from 'stores/UiStore'; +import { documentEditUrl } from 'utils/routeHelpers'; + +type Props = { + history: RouterHistory, + location: Location, + documents: DocumentsStore, + ui: UiStore, + match: Object, +}; + +class DocumentNew extends React.Component { + async componentDidMount() { + try { + const document = await this.props.documents.create({ + collectionId: this.props.match.params.id, + parentDocumentId: new URLSearchParams(this.props.location.search).get( + 'parentDocumentId' + ), + title: '', + text: '', + }); + this.props.history.replace(documentEditUrl(document)); + } catch (err) { + this.props.ui.showToast('Couldn’t create the document, try again?'); + this.props.history.goBack(); + } + } + + render() { + return ( + + + + + + ); + } +} + +export default inject('documents', 'ui')(DocumentNew); diff --git a/server/api/documents.js b/server/api/documents.js index 0a152fa7..a14a2dba 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -585,15 +585,14 @@ router.post('documents.unstar', auth(), async ctx => { router.post('documents.create', auth(), async ctx => { const { - title, - text, + title = '', + text = '', publish, collectionId, parentDocumentId, index, } = ctx.body; ctx.assertUuid(collectionId, 'collectionId must be an uuid'); - ctx.assertPresent(text, 'text is required'); if (parentDocumentId) { ctx.assertUuid(parentDocumentId, 'parentDocumentId must be an uuid'); } diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 76f6f9cf..82f9c67d 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -1039,22 +1039,6 @@ describe('#documents.create', async () => { expect(newDocument.collection.id).toBe(collection.id); }); - it('should fallback to a default title', async () => { - const { user, collection } = await seed(); - const res = await server.post('/api/documents.create', { - body: { - token: user.getJwtToken(), - collectionId: collection.id, - title: ' ', - text: ' ', - }, - }); - const body = await res.json(); - expect(res.status).toEqual(200); - expect(body.data.title).toBe('Untitled document'); - expect(body.data.text).toBe('# Untitled document'); - }); - it('should not allow very long titles', async () => { const { user, collection } = await seed(); const res = await server.post('/api/documents.create', { @@ -1183,25 +1167,6 @@ describe('#documents.update', async () => { expect(revisionRecords).toBe(prevRevisionRecords); }); - it('should fallback to a default title', async () => { - const { user, document } = await seed(); - - const res = await server.post('/api/documents.update', { - body: { - token: user.getJwtToken(), - id: document.id, - title: ' ', - text: ' ', - lastRevision: document.revision, - }, - }); - const body = await res.json(); - - expect(res.status).toEqual(200); - expect(body.data.title).toBe('Untitled document'); - expect(body.data.text).toBe('# Untitled document'); - }); - it('should fail if document lastRevision does not match', async () => { const { user, document } = await seed(); diff --git a/server/models/Document.js b/server/models/Document.js index fc2fe41a..8af2f21a 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -17,7 +17,7 @@ import Revision from './Revision'; const Op = Sequelize.Op; const Markdown = new MarkdownSerializer(); const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{10,15})$/; -const DEFAULT_TITLE = 'Untitled document'; +const DEFAULT_TITLE = 'Untitled'; slug.defaults.mode = 'rfc3986'; const slugify = text => @@ -55,10 +55,9 @@ const beforeSave = async doc => { // emoji in the title is split out for easier display doc.emoji = emoji; - // ensure document has a title + // ensure documents have a title if (!title) { doc.title = DEFAULT_TITLE; - doc.text = doc.text.replace(/^.*$/m, `# ${DEFAULT_TITLE}`); } // add the current user as a collaborator on this doc diff --git a/server/presenters/document.js b/server/presenters/document.js index 84ba31bf..83f10af6 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -13,11 +13,6 @@ export default async function present(document: Document, options: ?Options) { ...options, }; - // For empty document content, return the title - if (!document.text.trim()) { - document.text = `# ${document.title}`; - } - const data = { id: document.id, url: document.url,