Use clearer urls fro documents
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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...
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
/
|
/
|
||||||
<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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
18
server/migrations/20160815142720-app-collection-urlId.js
Normal file
18
server/migrations/20160815142720-app-collection-urlId.js
Normal 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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user