From 3089ac7bc8298892a17314052ca6ee15c2781f03 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 15 Aug 2016 21:41:51 +0200 Subject: [PATCH] Use clearer urls fro documents --- .../components/AtlasPreview/AtlasPreview.js | 15 +++--- .../components/DocumentLink/DocumentLink.js | 2 +- .../DocumentPreview/DocumentPreview.js | 4 +- frontend/index.js | 6 +-- frontend/scenes/Atlas/Atlas.js | 2 +- .../scenes/DocumentEdit/DocumentEditStore.js | 9 ++-- .../scenes/DocumentScene/DocumentScene.js | 6 +-- .../DocumentScene/DocumentSceneStore.js | 2 +- .../components/Sidebar/Sidebar.js | 2 +- server/api/documents.js | 47 ++++++++++--------- .../20160815142720-app-collection-urlId.js | 18 +++++++ server/models/Atlas.js | 14 +++++- server/models/Document.js | 14 ++---- server/presenters.js | 3 +- 14 files changed, 88 insertions(+), 56 deletions(-) create mode 100644 server/migrations/20160815142720-app-collection-urlId.js diff --git a/frontend/components/AtlasPreview/AtlasPreview.js b/frontend/components/AtlasPreview/AtlasPreview.js index 000be7efc..6eca19c38 100644 --- a/frontend/components/AtlasPreview/AtlasPreview.js +++ b/frontend/components/AtlasPreview/AtlasPreview.js @@ -5,8 +5,8 @@ import Link from 'react-router/lib/Link'; import DocumentLink from './components/DocumentLink'; import styles from './AtlasPreview.scss'; -import classNames from 'classnames/bind'; -const cx = classNames.bind(styles); +// import classNames from 'classnames/bind'; +// const cx = classNames.bind(styles); @observer class AtlasPreview extends React.Component { @@ -19,18 +19,21 @@ class AtlasPreview extends React.Component { return (
-

{ data.name }

+

{ data.name }

{ data.recentDocuments.length > 0 ? data.recentDocuments.map(document => { return ( - ) + + ); }) : ( -
No documents. Why not create one?
+
+ No documents. Why not create one? +
) }
); } -}; +} export default AtlasPreview; diff --git a/frontend/components/AtlasPreview/components/DocumentLink/DocumentLink.js b/frontend/components/AtlasPreview/components/DocumentLink/DocumentLink.js index 6b8f452d7..b413a23bf 100644 --- a/frontend/components/AtlasPreview/components/DocumentLink/DocumentLink.js +++ b/frontend/components/AtlasPreview/components/DocumentLink/DocumentLink.js @@ -8,7 +8,7 @@ import styles from './DocumentLink.scss'; const DocumentLink = observer((props) => { return ( - +

{ props.document.title }

{ moment(props.document.updatedAt).fromNow() } diff --git a/frontend/components/DocumentPreview/DocumentPreview.js b/frontend/components/DocumentPreview/DocumentPreview.js index 8ecc55033..3c9c6fce5 100644 --- a/frontend/components/DocumentPreview/DocumentPreview.js +++ b/frontend/components/DocumentPreview/DocumentPreview.js @@ -24,7 +24,7 @@ class Document extends React.Component { />

{ this.props.document.title }

@@ -34,7 +34,7 @@ class Document extends React.Component {
Continue reading... diff --git a/frontend/index.js b/frontend/index.js index 2a8b070cd..83636a3fb 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -57,10 +57,10 @@ render(( onEnter={ requireAuth } newDocument /> - - + + { if (event) event.preventDefault(); - browserHistory.push(`/collections/${store.collection.id}/new`); + browserHistory.push(`${store.collection.url}/new`); } render() { diff --git a/frontend/scenes/DocumentEdit/DocumentEditStore.js b/frontend/scenes/DocumentEdit/DocumentEditStore.js index 3bda46f80..3e8ed2a51 100644 --- a/frontend/scenes/DocumentEdit/DocumentEditStore.js +++ b/frontend/scenes/DocumentEdit/DocumentEditStore.js @@ -67,10 +67,10 @@ class DocumentEditStore { title: this.title, text: this.text, }, { cache: true }); - const { id } = data.data; + const { url } = data.data; this.hasPendingChanges = false; - browserHistory.push(`/documents/${id}`); + browserHistory.push(url); } catch (e) { console.error("Something went wrong"); } @@ -83,14 +83,15 @@ class DocumentEditStore { this.isSaving = true; try { - await client.post('/documents.update', { + const data = await client.post('/documents.update', { id: this.documentId, title: this.title, text: this.text, }, { cache: true }); + const { url } = data.data; this.hasPendingChanges = false; - browserHistory.push(`/documents/${this.documentId}`); + browserHistory.push(url); } catch (e) { console.error("Something went wrong"); } diff --git a/frontend/scenes/DocumentScene/DocumentScene.js b/frontend/scenes/DocumentScene/DocumentScene.js index fde2f1214..e1fd7e9c4 100644 --- a/frontend/scenes/DocumentScene/DocumentScene.js +++ b/frontend/scenes/DocumentScene/DocumentScene.js @@ -90,12 +90,12 @@ class DocumentScene extends React.Component { } onEdit = () => { - const url = `/documents/${this.store.document.id}/edit`; + const url = `${this.store.document.url}/edit`; browserHistory.push(url); } onCreate = () => { - const url = `/documents/${this.store.document.id}/new`; + const url = `${this.store.document.url}/new`; browserHistory.push(url); } @@ -147,7 +147,7 @@ class DocumentScene extends React.Component { title = (  /  - { doc.collection.name } + { doc.collection.name } { ` / ${doc.title}` } ); diff --git a/frontend/scenes/DocumentScene/DocumentSceneStore.js b/frontend/scenes/DocumentScene/DocumentSceneStore.js index ff7d8ebab..3db364ba9 100644 --- a/frontend/scenes/DocumentScene/DocumentSceneStore.js +++ b/frontend/scenes/DocumentScene/DocumentSceneStore.js @@ -72,7 +72,7 @@ class DocumentSceneStore { try { await client.post('/documents.delete', { id: this.document.id }); - browserHistory.push(`/collections/${this.document.collection.id}`); + browserHistory.push(this.document.collection.url); } catch (e) { console.error("Something went wrong"); } diff --git a/frontend/scenes/DocumentScene/components/Sidebar/Sidebar.js b/frontend/scenes/DocumentScene/components/Sidebar/Sidebar.js index 4998f3fbb..a7de8daf5 100644 --- a/frontend/scenes/DocumentScene/components/Sidebar/Sidebar.js +++ b/frontend/scenes/DocumentScene/components/Sidebar/Sidebar.js @@ -53,7 +53,7 @@ class Sidebar extends React.Component { Add document diff --git a/server/api/documents.js b/server/api/documents.js index 15bd98840..3a8ba59fa 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -4,6 +4,8 @@ import { sequelize, } from '../sequelize'; +const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{15})$/; + import auth from './authentication'; // import pagination from './middlewares/pagination'; import { presentDocument } from '../presenters'; @@ -11,16 +13,29 @@ import { Document, Atlas } from '../models'; const router = new Router(); +const getDocumentForId = async (id) => { + let document; + if (id.match(URL_REGEX)) { + document = await Document.findOne({ + where: { + urlId: id.match(URL_REGEX)[1], + }, + }); + } else { + document = await Document.findOne({ + where: { + id, + }, + }); + } + return document; +}; + // FIXME: This really needs specs :/ -router.post('documents.info', auth({ require: false }), async (ctx) => { +router.post('documents.info', auth(), async (ctx) => { const { id } = ctx.body; ctx.assertPresent(id, 'id is required'); - - const document = await Document.findOne({ - where: { - id, - }, - }); + const document = await getDocumentForId(id); if (!document) throw httpErrors.NotFound(); @@ -156,14 +171,9 @@ router.post('documents.update', auth(), async (ctx) => { ctx.assertPresent(text, 'text is required'); const user = ctx.state.user; - const document = await Document.findOne({ - where: { - id, - teamId: user.teamId, - }, - }); + const document = await getDocumentForId(id); - if (!document) throw httpErrors.BadRequest(); + if (!document || document.teamId !== user.teamId) throw httpErrors.BadRequest(); // Update document document.title = title; @@ -192,15 +202,10 @@ router.post('documents.delete', auth(), async (ctx) => { ctx.assertPresent(id, 'id is required'); const user = ctx.state.user; - const document = await Document.findOne({ - where: { - id, - teamId: user.teamId, - }, - }); + const document = await getDocumentForId(id); const collection = await Atlas.findById(document.atlasId); - if (!document) throw httpErrors.BadRequest(); + if (!document || document.teamId !== user.teamId) throw httpErrors.BadRequest(); if (collection.type === 'atlas') { // Don't allow deletion of root docs diff --git a/server/migrations/20160815142720-app-collection-urlId.js b/server/migrations/20160815142720-app-collection-urlId.js new file mode 100644 index 000000000..bd9c1aeff --- /dev/null +++ b/server/migrations/20160815142720-app-collection-urlId.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + queryInterface.addColumn( + 'atlases', + 'urlId', + { + type: Sequelize.STRING, + unique: true, + } + ); + }, + + down: function (queryInterface, Sequelize) { + queryInterface.removeColumn('atlases', 'urlId'); + } +}; diff --git a/server/models/Atlas.js b/server/models/Atlas.js index 5938804bd..b4b0672b5 100644 --- a/server/models/Atlas.js +++ b/server/models/Atlas.js @@ -1,3 +1,5 @@ +import slug from 'slug'; +import randomstring from 'randomstring'; import { DataTypes, sequelize, @@ -5,10 +7,13 @@ import { import _ from 'lodash'; import Document from './Document'; +slug.defaults.mode = 'rfc3986'; + const allowedAtlasTypes = [['atlas', 'journal']]; const Atlas = sequelize.define('atlas', { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, + urlId: { type: DataTypes.STRING, unique: true }, name: DataTypes.STRING, description: DataTypes.STRING, type: { type: DataTypes.STRING, validate: { isIn: allowedAtlasTypes } }, @@ -20,6 +25,9 @@ const Atlas = sequelize.define('atlas', { tableName: 'atlases', paranoid: true, hooks: { + beforeValidate: (collection) => { + collection.urlId = collection.urlId || randomstring.generate(10); + }, afterCreate: async (collection) => { if (collection.type !== 'atlas') return; @@ -38,8 +46,12 @@ const Atlas = sequelize.define('atlas', { }, }, instanceMethods: { + getUrl() { + // const slugifiedName = slug(this.name); + // return `/${slugifiedName}-c${this.urlId}`; + return `/collections/${this.id}`; + }, async buildStructure() { - console.log('start'); if (this.navigationTree) return this.navigationTree; const getNodeForDocument = async (document) => { diff --git a/server/models/Document.js b/server/models/Document.js index 830ae14ba..43ea21899 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -16,11 +16,6 @@ import Revision from './Revision'; slug.defaults.mode = 'rfc3986'; -const generateSlug = (title, urlId) => { - const slugifiedTitle = slug(title); - return `${slugifiedTitle}-${urlId}`; -}; - const createRevision = async (doc) => { // Create revision of the current (latest) await Revision.create({ @@ -80,7 +75,7 @@ const Document = sequelize.define('document', { paranoid: true, hooks: { beforeValidate: (doc) => { - doc.urlId = randomstring.generate(15); + doc.urlId = doc.urlId || randomstring.generate(10); }, beforeCreate: documentBeforeSave, beforeUpdate: documentBeforeSave, @@ -88,12 +83,9 @@ const Document = sequelize.define('document', { afterUpdate: async (doc) => await createRevision(doc), }, instanceMethods: { - buildUrl() { - const slugifiedTitle = slug(this.title); - return `${slugifiedTitle}-${this.urlId}`; - }, getUrl() { - return `/documents/${this.id}`; + const slugifiedTitle = slug(this.title); + return `/d/${slugifiedTitle}-${this.urlId}`; }, }, }); diff --git a/server/presenters.js b/server/presenters.js index ada922646..83e64f87b 100644 --- a/server/presenters.js +++ b/server/presenters.js @@ -37,7 +37,7 @@ export async function presentDocument(ctx, document, options) { const data = { id: document.id, - url: document.buildUrl(), + url: document.getUrl(), private: document.private, title: document.title, text: document.text, @@ -96,6 +96,7 @@ export function presentCollection(ctx, collection, includeRecentDocuments=false) return new Promise(async (resolve, _reject) => { const data = { id: collection.id, + url: collection.getUrl(), name: collection.name, description: collection.description, type: collection.type,