Use clearer urls fro documents

This commit is contained in:
Jori Lallo
2016-08-15 21:41:51 +02:00
parent 537341c01c
commit 3089ac7bc8
14 changed files with 88 additions and 56 deletions

View File

@@ -5,8 +5,8 @@ import Link from 'react-router/lib/Link';
import DocumentLink from './components/DocumentLink'; import DocumentLink from './components/DocumentLink';
import styles from './AtlasPreview.scss'; import styles from './AtlasPreview.scss';
import classNames from 'classnames/bind'; // import classNames from 'classnames/bind';
const cx = classNames.bind(styles); // const cx = classNames.bind(styles);
@observer @observer
class AtlasPreview extends React.Component { class AtlasPreview extends React.Component {
@@ -19,18 +19,21 @@ class AtlasPreview extends React.Component {
return ( return (
<div className={ styles.container }> <div className={ styles.container }>
<h2><Link to={ `/collections/${data.id}` } className={ styles.atlasLink }>{ data.name }</Link></h2> <h2><Link to={ data.url } className={ styles.atlasLink }>{ data.name }</Link></h2>
{ data.recentDocuments.length > 0 ? { data.recentDocuments.length > 0 ?
data.recentDocuments.map(document => { data.recentDocuments.map(document => {
return ( return (
<DocumentLink document={ document } key={ document.id } />) <DocumentLink document={ document } key={ document.id } />
);
}) })
: ( : (
<div className={ styles.description }>No documents. Why not <Link to={ `/collections/${data.id}/new` }>create one</Link>?</div> <div className={ styles.description }>
No documents. Why not <Link to={ `${data.url}/new` }>create one</Link>?
</div>
) } ) }
</div> </div>
); );
} }
}; }
export default AtlasPreview; export default AtlasPreview;

View File

@@ -8,7 +8,7 @@ import styles from './DocumentLink.scss';
const DocumentLink = observer((props) => { const DocumentLink = observer((props) => {
return ( return (
<Link to={ `/documents/${props.document.id}` } className={ styles.link }> <Link to={ props.document.url } className={ styles.link }>
<h3 className={ styles.title }>{ props.document.title }</h3> <h3 className={ styles.title }>{ props.document.title }</h3>
<span className={ styles.timestamp }>{ moment(props.document.updatedAt).fromNow() }</span> <span className={ styles.timestamp }>{ moment(props.document.updatedAt).fromNow() }</span>
</Link> </Link>

View File

@@ -24,7 +24,7 @@ class Document extends React.Component {
/> />
<Link <Link
to={ `/documents/${this.props.document.id}` } to={ this.props.document.url }
className={ styles.title } className={ styles.title }
> >
<h2>{ this.props.document.title }</h2> <h2>{ this.props.document.title }</h2>
@@ -34,7 +34,7 @@ class Document extends React.Component {
<div> <div>
<Link <Link
to={ `/documents/${this.props.document.id}` } to={ this.props.document.url }
className={ styles.continueLink } className={ styles.continueLink }
> >
Continue reading... Continue reading...

View File

@@ -57,10 +57,10 @@ render((
onEnter={ requireAuth } onEnter={ requireAuth }
newDocument newDocument
/> />
<Route path="/documents/:id" component={ DocumentScene } onEnter={ requireAuth } /> <Route path="/d/:id" component={ DocumentScene } onEnter={ requireAuth } />
<Route path="/documents/:id/edit" component={ DocumentEdit } onEnter={ requireAuth } /> <Route path="/d/:id/edit" component={ DocumentEdit } onEnter={ requireAuth } />
<Route <Route
path="/documents/:id/new" path="/d/:id/new"
component={ DocumentEdit } component={ DocumentEdit }
onEnter={ requireAuth } onEnter={ requireAuth }
newChildDocument newChildDocument

View File

@@ -41,7 +41,7 @@ class Atlas extends React.Component {
onCreate = (event) => { onCreate = (event) => {
if (event) event.preventDefault(); if (event) event.preventDefault();
browserHistory.push(`/collections/${store.collection.id}/new`); browserHistory.push(`${store.collection.url}/new`);
} }
render() { render() {

View File

@@ -67,10 +67,10 @@ class DocumentEditStore {
title: this.title, title: this.title,
text: this.text, text: this.text,
}, { cache: true }); }, { cache: true });
const { id } = data.data; const { url } = data.data;
this.hasPendingChanges = false; this.hasPendingChanges = false;
browserHistory.push(`/documents/${id}`); browserHistory.push(url);
} catch (e) { } catch (e) {
console.error("Something went wrong"); console.error("Something went wrong");
} }
@@ -83,14 +83,15 @@ class DocumentEditStore {
this.isSaving = true; this.isSaving = true;
try { try {
await client.post('/documents.update', { const data = await client.post('/documents.update', {
id: this.documentId, id: this.documentId,
title: this.title, title: this.title,
text: this.text, text: this.text,
}, { cache: true }); }, { cache: true });
const { url } = data.data;
this.hasPendingChanges = false; this.hasPendingChanges = false;
browserHistory.push(`/documents/${this.documentId}`); browserHistory.push(url);
} catch (e) { } catch (e) {
console.error("Something went wrong"); console.error("Something went wrong");
} }

View File

@@ -90,12 +90,12 @@ class DocumentScene extends React.Component {
} }
onEdit = () => { onEdit = () => {
const url = `/documents/${this.store.document.id}/edit`; const url = `${this.store.document.url}/edit`;
browserHistory.push(url); browserHistory.push(url);
} }
onCreate = () => { onCreate = () => {
const url = `/documents/${this.store.document.id}/new`; const url = `${this.store.document.url}/new`;
browserHistory.push(url); browserHistory.push(url);
} }
@@ -147,7 +147,7 @@ class DocumentScene extends React.Component {
title = ( title = (
<span> <span>
&nbsp;/&nbsp; &nbsp;/&nbsp;
<Link to={ `/collections/${doc.collection.id}` }>{ doc.collection.name }</Link> <Link to={ doc.collection.url }>{ doc.collection.name }</Link>
{ ` / ${doc.title}` } { ` / ${doc.title}` }
</span> </span>
); );

View File

@@ -72,7 +72,7 @@ class DocumentSceneStore {
try { try {
await client.post('/documents.delete', { id: this.document.id }); await client.post('/documents.delete', { id: this.document.id });
browserHistory.push(`/collections/${this.document.collection.id}`); browserHistory.push(this.document.collection.url);
} catch (e) { } catch (e) {
console.error("Something went wrong"); console.error("Something went wrong");
} }

View File

@@ -53,7 +53,7 @@ class Sidebar extends React.Component {
</Flex> </Flex>
<Flex auto className={ styles.actions }> <Flex auto className={ styles.actions }>
<Link <Link
to={ `/documents/${this.props.navigationTree.id}/new` } to={ this.props.navigationTree.url }
className={ cx(styles.action) } className={ cx(styles.action) }
> >
Add document Add document

View File

@@ -4,6 +4,8 @@ import {
sequelize, sequelize,
} from '../sequelize'; } from '../sequelize';
const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{15})$/;
import auth from './authentication'; import auth from './authentication';
// import pagination from './middlewares/pagination'; // import pagination from './middlewares/pagination';
import { presentDocument } from '../presenters'; import { presentDocument } from '../presenters';
@@ -11,16 +13,29 @@ import { Document, Atlas } from '../models';
const router = new Router(); 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 :/ // 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; const { id } = ctx.body;
ctx.assertPresent(id, 'id is required'); ctx.assertPresent(id, 'id is required');
const document = await getDocumentForId(id);
const document = await Document.findOne({
where: {
id,
},
});
if (!document) throw httpErrors.NotFound(); if (!document) throw httpErrors.NotFound();
@@ -156,14 +171,9 @@ router.post('documents.update', auth(), async (ctx) => {
ctx.assertPresent(text, 'text is required'); ctx.assertPresent(text, 'text is required');
const user = ctx.state.user; const user = ctx.state.user;
const document = await Document.findOne({ const document = await getDocumentForId(id);
where: {
id,
teamId: user.teamId,
},
});
if (!document) throw httpErrors.BadRequest(); if (!document || document.teamId !== user.teamId) throw httpErrors.BadRequest();
// Update document // Update document
document.title = title; document.title = title;
@@ -192,15 +202,10 @@ router.post('documents.delete', auth(), async (ctx) => {
ctx.assertPresent(id, 'id is required'); ctx.assertPresent(id, 'id is required');
const user = ctx.state.user; const user = ctx.state.user;
const document = await Document.findOne({ const document = await getDocumentForId(id);
where: {
id,
teamId: user.teamId,
},
});
const collection = await Atlas.findById(document.atlasId); 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') { if (collection.type === 'atlas') {
// Don't allow deletion of root docs // Don't allow deletion of root docs

View File

@@ -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');
}
};

View File

@@ -1,3 +1,5 @@
import slug from 'slug';
import randomstring from 'randomstring';
import { import {
DataTypes, DataTypes,
sequelize, sequelize,
@@ -5,10 +7,13 @@ import {
import _ from 'lodash'; import _ from 'lodash';
import Document from './Document'; import Document from './Document';
slug.defaults.mode = 'rfc3986';
const allowedAtlasTypes = [['atlas', 'journal']]; const allowedAtlasTypes = [['atlas', 'journal']];
const Atlas = sequelize.define('atlas', { const Atlas = sequelize.define('atlas', {
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
urlId: { type: DataTypes.STRING, unique: true },
name: DataTypes.STRING, name: DataTypes.STRING,
description: DataTypes.STRING, description: DataTypes.STRING,
type: { type: DataTypes.STRING, validate: { isIn: allowedAtlasTypes } }, type: { type: DataTypes.STRING, validate: { isIn: allowedAtlasTypes } },
@@ -20,6 +25,9 @@ const Atlas = sequelize.define('atlas', {
tableName: 'atlases', tableName: 'atlases',
paranoid: true, paranoid: true,
hooks: { hooks: {
beforeValidate: (collection) => {
collection.urlId = collection.urlId || randomstring.generate(10);
},
afterCreate: async (collection) => { afterCreate: async (collection) => {
if (collection.type !== 'atlas') return; if (collection.type !== 'atlas') return;
@@ -38,8 +46,12 @@ const Atlas = sequelize.define('atlas', {
}, },
}, },
instanceMethods: { instanceMethods: {
getUrl() {
// const slugifiedName = slug(this.name);
// return `/${slugifiedName}-c${this.urlId}`;
return `/collections/${this.id}`;
},
async buildStructure() { async buildStructure() {
console.log('start');
if (this.navigationTree) return this.navigationTree; if (this.navigationTree) return this.navigationTree;
const getNodeForDocument = async (document) => { const getNodeForDocument = async (document) => {

View File

@@ -16,11 +16,6 @@ import Revision from './Revision';
slug.defaults.mode = 'rfc3986'; slug.defaults.mode = 'rfc3986';
const generateSlug = (title, urlId) => {
const slugifiedTitle = slug(title);
return `${slugifiedTitle}-${urlId}`;
};
const createRevision = async (doc) => { const createRevision = async (doc) => {
// Create revision of the current (latest) // Create revision of the current (latest)
await Revision.create({ await Revision.create({
@@ -80,7 +75,7 @@ const Document = sequelize.define('document', {
paranoid: true, paranoid: true,
hooks: { hooks: {
beforeValidate: (doc) => { beforeValidate: (doc) => {
doc.urlId = randomstring.generate(15); doc.urlId = doc.urlId || randomstring.generate(10);
}, },
beforeCreate: documentBeforeSave, beforeCreate: documentBeforeSave,
beforeUpdate: documentBeforeSave, beforeUpdate: documentBeforeSave,
@@ -88,12 +83,9 @@ const Document = sequelize.define('document', {
afterUpdate: async (doc) => await createRevision(doc), afterUpdate: async (doc) => await createRevision(doc),
}, },
instanceMethods: { instanceMethods: {
buildUrl() {
const slugifiedTitle = slug(this.title);
return `${slugifiedTitle}-${this.urlId}`;
},
getUrl() { getUrl() {
return `/documents/${this.id}`; const slugifiedTitle = slug(this.title);
return `/d/${slugifiedTitle}-${this.urlId}`;
}, },
}, },
}); });

View File

@@ -37,7 +37,7 @@ export async function presentDocument(ctx, document, options) {
const data = { const data = {
id: document.id, id: document.id,
url: document.buildUrl(), url: document.getUrl(),
private: document.private, private: document.private,
title: document.title, title: document.title,
text: document.text, text: document.text,
@@ -96,6 +96,7 @@ export function presentCollection(ctx, collection, includeRecentDocuments=false)
return new Promise(async (resolve, _reject) => { return new Promise(async (resolve, _reject) => {
const data = { const data = {
id: collection.id, id: collection.id,
url: collection.getUrl(),
name: collection.name, name: collection.name,
description: collection.description, description: collection.description,
type: collection.type, type: collection.type,