From c42d4eeaceb50a4195d854a48fe6122f086724e5 Mon Sep 17 00:00:00 2001 From: themeteorchef Date: Fri, 4 Nov 2016 00:09:14 -0500 Subject: [PATCH] handful of changes - Remove dependency on faker NPM package. - Consolidate insert and update methods into single upsert method. - Add /edit and /new pages for editing and creating documents. - Move all exports to default exports (linter thing). - Add a module for editing and creating documents. - Update method tests for documents collection to use new upsert method. - Oh, I'm sure there's something else... --- .meteor/versions | 6 +-- imports/api/documents/documents.js | 11 ++++-- imports/api/documents/documents.tests.js | 2 +- imports/api/documents/methods.js | 26 ++++--------- imports/api/documents/methods.tests.js | 19 ++++++---- imports/api/documents/server/publications.js | 15 +++++++- imports/modules/document-editor.js | 55 ++++++++++++++++++++++++++++ imports/modules/login.js | 2 +- imports/startup/client/routes.js | 24 ++++++++---- imports/ui/components/AppNavigation.js | 4 +- imports/ui/components/DocumentEditor.js | 45 +++++++++++++++++++++++ imports/ui/components/DocumentsList.js | 9 ++--- imports/ui/containers/AppNavigation.js | 2 +- imports/ui/containers/DocumentsList.js | 8 ++-- imports/ui/containers/EditDocument.js | 16 ++++++++ imports/ui/containers/ViewDocument.js | 16 ++++++++ imports/ui/pages/EditDocument.js | 15 ++++++++ imports/ui/pages/NewDocument.js | 11 ++++++ imports/ui/pages/RecoverPassword.js | 6 +-- imports/ui/pages/ResetPassword.js | 6 +-- imports/ui/pages/ViewDocument.js | 39 ++++++++++++++++++++ imports/ui/pages/documents.js | 13 +++++-- imports/ui/pages/login.js | 6 +-- imports/ui/pages/signup.js | 6 +-- package.json | 8 ++-- 25 files changed, 294 insertions(+), 76 deletions(-) create mode 100644 imports/modules/document-editor.js create mode 100644 imports/ui/components/DocumentEditor.js create mode 100644 imports/ui/containers/EditDocument.js create mode 100644 imports/ui/containers/ViewDocument.js create mode 100644 imports/ui/pages/EditDocument.js create mode 100644 imports/ui/pages/NewDocument.js create mode 100644 imports/ui/pages/ViewDocument.js diff --git a/.meteor/versions b/.meteor/versions index 6092391..0d8d35d 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -4,13 +4,13 @@ alanning:roles@1.2.15 aldeed:collection2@2.10.0 aldeed:collection2-core@1.2.0 aldeed:schema-deny@1.1.0 -aldeed:schema-index@1.1.0 +aldeed:schema-index@1.1.1 aldeed:simple-schema@1.5.3 allow-deny@1.0.5 audit-argument-checks@1.0.7 autoupdate@1.3.12 babel-compiler@6.13.0 -babel-runtime@0.1.12 +babel-runtime@0.1.13 base64@1.0.10 binary-heap@1.0.10 blaze@2.1.9 @@ -40,7 +40,7 @@ email@1.1.18 es5-shim@4.6.15 fastclick@1.0.13 fortawesome:fontawesome@4.6.3 -fourseven:scss@3.10.0 +fourseven:scss@3.10.1 geojson-utils@1.0.10 hot-code-push@1.0.4 html-tools@1.0.11 diff --git a/imports/api/documents/documents.js b/imports/api/documents/documents.js index 5578ed5..58c3ae4 100644 --- a/imports/api/documents/documents.js +++ b/imports/api/documents/documents.js @@ -1,9 +1,9 @@ -if (Meteor.isClient) import faker from 'faker'; import { Mongo } from 'meteor/mongo'; import { SimpleSchema } from 'meteor/aldeed:simple-schema'; import { Factory } from 'meteor/dburles:factory'; -export const Documents = new Mongo.Collection('Documents'); +const Documents = new Mongo.Collection('Documents'); +export default Documents; Documents.allow({ insert: () => false, @@ -22,10 +22,15 @@ Documents.schema = new SimpleSchema({ type: String, label: 'The title of the document.', }, + body: { + type: String, + label: 'The body of the document.', + }, }); Documents.attachSchema(Documents.schema); Factory.define('document', Documents, { - title: () => faker.hacker.phrase(), + title: () => 'Factory Title', + body: () => 'Factory Body', }); diff --git a/imports/api/documents/documents.tests.js b/imports/api/documents/documents.tests.js index d8028e4..4b9b04c 100644 --- a/imports/api/documents/documents.tests.js +++ b/imports/api/documents/documents.tests.js @@ -2,7 +2,7 @@ /* eslint-disable func-names, prefer-arrow-callback */ import { assert } from 'meteor/practicalmeteor:chai'; -import { Documents } from './documents.js'; +import Documents from './documents.js'; describe('Documents collection', function () { it('registers the collection with Mongo properly', function () { diff --git a/imports/api/documents/methods.js b/imports/api/documents/methods.js index dac5b4c..7668628 100644 --- a/imports/api/documents/methods.js +++ b/imports/api/documents/methods.js @@ -1,26 +1,17 @@ import { SimpleSchema } from 'meteor/aldeed:simple-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { Documents } from './documents'; +import Documents from './documents'; import { rateLimit } from '../../modules/rate-limit.js'; -export const insertDocument = new ValidatedMethod({ - name: 'documents.insert', +export const upsertDocument = new ValidatedMethod({ + name: 'documents.upsert', validate: new SimpleSchema({ - title: { type: String }, + _id: { type: String, optional: true }, + title: { type: String, optional: true }, + body: { type: String, optional: true }, }).validator(), run(document) { - Documents.insert(document); - }, -}); - -export const updateDocument = new ValidatedMethod({ - name: 'documents.update', - validate: new SimpleSchema({ - _id: { type: String }, - 'update.title': { type: String, optional: true }, - }).validator(), - run({ _id, update }) { - Documents.update(_id, { $set: update }); + return Documents.upsert({ _id: document._id }, { $set: document }); }, }); @@ -36,8 +27,7 @@ export const removeDocument = new ValidatedMethod({ rateLimit({ methods: [ - insertDocument, - updateDocument, + upsertDocument, removeDocument, ], limit: 5, diff --git a/imports/api/documents/methods.tests.js b/imports/api/documents/methods.tests.js index 477455d..2294b50 100644 --- a/imports/api/documents/methods.tests.js +++ b/imports/api/documents/methods.tests.js @@ -5,8 +5,8 @@ import { Meteor } from 'meteor/meteor'; import { assert } from 'meteor/practicalmeteor:chai'; import { resetDatabase } from 'meteor/xolvio:cleaner'; import { Factory } from 'meteor/dburles:factory'; -import { Documents } from './documents.js'; -import { insertDocument, updateDocument, removeDocument } from './methods.js'; +import Documents from './documents.js'; +import { upsertDocument, removeDocument } from './methods.js'; describe('Documents methods', function () { beforeEach(function () { @@ -16,19 +16,22 @@ describe('Documents methods', function () { }); it('inserts a document into the Documents collection', function () { - insertDocument.call({ title: 'You can\'t arrest me, I\'m the Cake Boss!' }); + upsertDocument.call({ + title: 'You can\'t arrest me, I\'m the Cake Boss!', + body: 'They went nuts!', + }); + const getDocument = Documents.findOne({ title: 'You can\'t arrest me, I\'m the Cake Boss!' }); - assert.equal(getDocument.title, 'You can\'t arrest me, I\'m the Cake Boss!'); + assert.equal(getDocument.body, 'They went nuts!'); }); it('updates a document in the Documents collection', function () { const { _id } = Factory.create('document'); - updateDocument.call({ + upsertDocument.call({ _id, - update: { - title: 'You can\'t arrest me, I\'m the Cake Boss!', - }, + title: 'You can\'t arrest me, I\'m the Cake Boss!', + body: 'They went nuts!', }); const getDocument = Documents.findOne(_id); diff --git a/imports/api/documents/server/publications.js b/imports/api/documents/server/publications.js index 595014d..e94239c 100644 --- a/imports/api/documents/server/publications.js +++ b/imports/api/documents/server/publications.js @@ -1,4 +1,15 @@ import { Meteor } from 'meteor/meteor'; -import { Documents } from '../documents'; +import { check } from 'meteor/check'; +import Documents from '../documents'; -Meteor.publish('documents', () => Documents.find()); +Meteor.publish('documents.list', () => Documents.find()); + +Meteor.publish('documents.view', (_id) => { + check(_id, String); + return Documents.find(_id); +}); + +Meteor.publish('documents.edit', (_id) => { + check(_id, String); + return Documents.find(_id); +}); diff --git a/imports/modules/document-editor.js b/imports/modules/document-editor.js new file mode 100644 index 0000000..dfc82ad --- /dev/null +++ b/imports/modules/document-editor.js @@ -0,0 +1,55 @@ +import $ from 'jquery'; +import 'jquery-validation'; +import { browserHistory } from 'react-router'; +import { Bert } from 'meteor/themeteorchef:bert'; +import { upsertDocument } from '../api/documents/methods.js'; + +let component; + +const handleUpsert = () => { + const { doc } = component.props; + const confirmation = doc && doc._id ? 'Document updated!' : 'Document added!'; + const upsert = { + title: document.querySelector('[name="title"]').value.trim(), + body: document.querySelector('[name="body"]').value.trim(), + }; + + if (doc && doc._id) upsert._id = doc._id; + + upsertDocument.call(upsert, (error, { insertedId }) => { + if (error) { + Bert.alert(error.reason, 'danger'); + } else { + component.form.reset(); + Bert.alert(confirmation, 'success'); + browserHistory.push(`/documents/${insertedId || doc._id}`); + } + }); +}; + +const validate = () => { + $(component.form).validate({ + rules: { + title: { + required: true, + }, + body: { + required: true, + }, + }, + messages: { + title: { + required: 'Need a title in here, Seuss.', + }, + body: { + required: 'This thneeds a body, please.', + }, + }, + submitHandler() { handleUpsert(); }, + }); +}; + +export default function handleLogin(options) { + component = options.component; + validate(); +} diff --git a/imports/modules/login.js b/imports/modules/login.js index f5ce896..5404d0a 100644 --- a/imports/modules/login.js +++ b/imports/modules/login.js @@ -27,7 +27,7 @@ const login = () => { }; const validate = () => { - $(component.refs.login).validate({ + $('.login').validate({ rules: { emailAddress: { required: true, diff --git a/imports/startup/client/routes.js b/imports/startup/client/routes.js index 2ca0b69..f15a73b 100644 --- a/imports/startup/client/routes.js +++ b/imports/startup/client/routes.js @@ -1,15 +1,20 @@ +/* eslint-disable max-len */ + import React from 'react'; import { render } from 'react-dom'; import { Router, Route, IndexRoute, browserHistory } from 'react-router'; import { Meteor } from 'meteor/meteor'; -import { App } from '../../ui/layouts/App.js'; -import { Documents } from '../../ui/pages/Documents.js'; -import { Index } from '../../ui/pages/Index.js'; -import { Login } from '../../ui/pages/Login.js'; -import { NotFound } from '../../ui/pages/NotFound.js'; -import { RecoverPassword } from '../../ui/pages/RecoverPassword.js'; -import { ResetPassword } from '../../ui/pages/ResetPassword.js'; -import { Signup } from '../../ui/pages/Signup.js'; +import App from '../../ui/layouts/App.js'; +import Documents from '../../ui/pages/Documents.js'; +import NewDocument from '../../ui/pages/NewDocument.js'; +import EditDocument from '../../ui/containers/EditDocument.js'; +import ViewDocument from '../../ui/containers/ViewDocument.js'; +import Index from '../../ui/pages/Index.js'; +import Login from '../../ui/pages/Login.js'; +import NotFound from '../../ui/pages/NotFound.js'; +import RecoverPassword from '../../ui/pages/RecoverPassword.js'; +import ResetPassword from '../../ui/pages/ResetPassword.js'; +import Signup from '../../ui/pages/Signup.js'; const requireAuth = (nextState, replace) => { if (!Meteor.loggingIn() && !Meteor.userId()) { @@ -26,6 +31,9 @@ Meteor.startup(() => { + + + diff --git a/imports/ui/components/AppNavigation.js b/imports/ui/components/AppNavigation.js index a22edb8..319c936 100644 --- a/imports/ui/components/AppNavigation.js +++ b/imports/ui/components/AppNavigation.js @@ -1,8 +1,8 @@ import React from 'react'; import { Navbar } from 'react-bootstrap'; import { Link } from 'react-router'; -import { PublicNavigation } from './PublicNavigation.js'; -import { AuthenticatedNavigation } from './AuthenticatedNavigation.js'; +import PublicNavigation from './PublicNavigation.js'; +import AuthenticatedNavigation from './AuthenticatedNavigation.js'; const renderNavigation = hasUser => (hasUser ? : ); diff --git a/imports/ui/components/DocumentEditor.js b/imports/ui/components/DocumentEditor.js new file mode 100644 index 0000000..0c4d111 --- /dev/null +++ b/imports/ui/components/DocumentEditor.js @@ -0,0 +1,45 @@ +/* eslint-disable max-len, no-return-assign */ + +import React from 'react'; +import { FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; +import documentEditor from '../../modules/document-editor.js'; + +export default class DocumentEditor extends React.Component { + componentDidMount() { + documentEditor({ component: this }); + } + + render() { + const { doc } = this.props; + return (
this.form = form } + onSubmit={ event => event.preventDefault() } + > + + Title + + + + Body + + + +
); + } +} + +DocumentEditor.propTypes = { + doc: React.PropTypes.object, +}; diff --git a/imports/ui/components/DocumentsList.js b/imports/ui/components/DocumentsList.js index 288613b..93e91e3 100644 --- a/imports/ui/components/DocumentsList.js +++ b/imports/ui/components/DocumentsList.js @@ -1,11 +1,10 @@ import React from 'react'; -import { ListGroup, Alert } from 'react-bootstrap'; -import { Document } from './Document.js'; +import { ListGroup, ListGroupItem, Alert } from 'react-bootstrap'; const DocumentsList = ({ documents }) => ( - documents.length > 0 ? - {documents.map(doc => ( - + documents.length > 0 ? + {documents.map(({ _id, title }) => ( + { title } ))} : No documents yet. diff --git a/imports/ui/containers/AppNavigation.js b/imports/ui/containers/AppNavigation.js index f272227..65f743d 100644 --- a/imports/ui/containers/AppNavigation.js +++ b/imports/ui/containers/AppNavigation.js @@ -1,6 +1,6 @@ import { composeWithTracker } from 'react-komposer'; import { Meteor } from 'meteor/meteor'; -import { AppNavigation } from '../components/AppNavigation.js'; +import AppNavigation from '../components/AppNavigation.js'; const composer = (props, onData) => onData(null, { hasUser: Meteor.user() }); diff --git a/imports/ui/containers/DocumentsList.js b/imports/ui/containers/DocumentsList.js index 0768ded..248d63e 100644 --- a/imports/ui/containers/DocumentsList.js +++ b/imports/ui/containers/DocumentsList.js @@ -1,11 +1,11 @@ import { composeWithTracker } from 'react-komposer'; import { Meteor } from 'meteor/meteor'; -import { Documents } from '../../api/documents/documents.js'; -import { DocumentsList } from '../components/DocumentsList.js'; -import { Loading } from '../components/Loading.js'; +import Documents from '../../api/documents/documents.js'; +import DocumentsList from '../components/DocumentsList.js'; +import Loading from '../components/Loading.js'; const composer = (params, onData) => { - const subscription = Meteor.subscribe('documents'); + const subscription = Meteor.subscribe('documents.list'); if (subscription.ready()) { const documents = Documents.find().fetch(); onData(null, { documents }); diff --git a/imports/ui/containers/EditDocument.js b/imports/ui/containers/EditDocument.js new file mode 100644 index 0000000..3d6641c --- /dev/null +++ b/imports/ui/containers/EditDocument.js @@ -0,0 +1,16 @@ +import { Meteor } from 'meteor/meteor'; +import { composeWithTracker } from 'react-komposer'; +import Documents from '../../api/documents/documents.js'; +import EditDocument from '../pages/EditDocument.js'; +import { Loading } from '../components/Loading.js'; + +const composer = ({ params }, onData) => { + const subscription = Meteor.subscribe('documents.edit', params._id); + + if (subscription.ready()) { + const doc = Documents.findOne(); + onData(null, { doc }); + } +}; + +export default composeWithTracker(composer, Loading)(EditDocument); diff --git a/imports/ui/containers/ViewDocument.js b/imports/ui/containers/ViewDocument.js new file mode 100644 index 0000000..c9f8930 --- /dev/null +++ b/imports/ui/containers/ViewDocument.js @@ -0,0 +1,16 @@ +import { Meteor } from 'meteor/meteor'; +import { composeWithTracker } from 'react-komposer'; +import Documents from '../../api/documents/documents.js'; +import ViewDocument from '../pages/ViewDocument.js'; +import { Loading } from '../components/Loading.js'; + +const composer = ({ params }, onData) => { + const subscription = Meteor.subscribe('documents.view', params._id); + + if (subscription.ready()) { + const doc = Documents.findOne(); + onData(null, { doc }); + } +}; + +export default composeWithTracker(composer, Loading)(ViewDocument); diff --git a/imports/ui/pages/EditDocument.js b/imports/ui/pages/EditDocument.js new file mode 100644 index 0000000..207baaf --- /dev/null +++ b/imports/ui/pages/EditDocument.js @@ -0,0 +1,15 @@ +import React from 'react'; +import DocumentEditor from '../components/DocumentEditor.js'; + +const EditDocument = ({ doc }) => ( +
+

Editing "{ doc.title }"

+ +
+); + +EditDocument.propTypes = { + doc: React.PropTypes.object, +}; + +export default EditDocument; diff --git a/imports/ui/pages/NewDocument.js b/imports/ui/pages/NewDocument.js new file mode 100644 index 0000000..05c5795 --- /dev/null +++ b/imports/ui/pages/NewDocument.js @@ -0,0 +1,11 @@ +import React from 'react'; +import DocumentEditor from '../components/DocumentEditor.js'; + +const NewDocument = () => ( +
+

New Document

+ +
+); + +export default NewDocument; diff --git a/imports/ui/pages/RecoverPassword.js b/imports/ui/pages/RecoverPassword.js index 114e8ba..622fb16 100644 --- a/imports/ui/pages/RecoverPassword.js +++ b/imports/ui/pages/RecoverPassword.js @@ -1,14 +1,14 @@ import React from 'react'; import { Row, Col, Alert, FormGroup, FormControl, Button } from 'react-bootstrap'; -import { handleRecoverPassword } from '../../modules/recover-password'; +import handleRecoverPassword from '../../modules/recover-password'; export default class RecoverPassword extends React.Component { componentDidMount() { handleRecoverPassword({ component: this }); } - handleSubmit() { - this.preventDefault(); + handleSubmit(event) { + event.preventDefault(); } render() { diff --git a/imports/ui/pages/ResetPassword.js b/imports/ui/pages/ResetPassword.js index e8c7154..17862f0 100644 --- a/imports/ui/pages/ResetPassword.js +++ b/imports/ui/pages/ResetPassword.js @@ -1,14 +1,14 @@ import React from 'react'; import { Row, Col, Alert, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; -import { handleResetPassword } from '../../modules/reset-password'; +import handleResetPassword from '../../modules/reset-password'; export default class ResetPassword extends React.Component { componentDidMount() { handleResetPassword({ component: this, token: this.props.params.token }); } - handleSubmit() { - this.preventDefault(); + handleSubmit(event) { + event.preventDefault(); } render() { diff --git a/imports/ui/pages/ViewDocument.js b/imports/ui/pages/ViewDocument.js new file mode 100644 index 0000000..f3f7183 --- /dev/null +++ b/imports/ui/pages/ViewDocument.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { ButtonToolbar, ButtonGroup, Button } from 'react-bootstrap'; +import { browserHistory } from 'react-router'; +import { Bert } from 'meteor/themeteorchef:bert'; +import { removeDocument } from '../../api/documents/methods.js'; + +const handleRemove = (_id) => { + if (confirm('Are you sure? This is permanent!')) { + removeDocument.call({ _id }, (error) => { + if (error) { + Bert.alert(error.reason, 'danger'); + } else { + Bert.alert('Document deleted!', 'success'); + browserHistory.push('/documents'); + } + }); + } +}; + +const ViewDocument = ({ doc }) => ( +
+
+

{ doc.title }

+ + + + + + +
+ { doc.body } +
+); + +ViewDocument.propTypes = { + doc: React.PropTypes.object.isRequired, +}; + +export default ViewDocument; diff --git a/imports/ui/pages/documents.js b/imports/ui/pages/documents.js index 8d1601b..4190cf1 100644 --- a/imports/ui/pages/documents.js +++ b/imports/ui/pages/documents.js @@ -1,13 +1,18 @@ import React from 'react'; -import { Row, Col } from 'react-bootstrap'; +import { Row, Col, Button } from 'react-bootstrap'; import DocumentsList from '../containers/DocumentsList.js'; -import { AddDocument } from '../components/AddDocument.js'; const Documents = () => ( -

Documents

- +
+

Documents

+ +
diff --git a/imports/ui/pages/login.js b/imports/ui/pages/login.js index 7d00da3..7a14d4f 100644 --- a/imports/ui/pages/login.js +++ b/imports/ui/pages/login.js @@ -1,15 +1,15 @@ import React from 'react'; import { Link } from 'react-router'; import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; -import { handleLogin } from '../../modules/login'; +import handleLogin from '../../modules/login'; export default class Login extends React.Component { componentDidMount() { handleLogin({ component: this }); } - handleSubmit() { - this.preventDefault(); + handleSubmit(event) { + event.preventDefault(); } render() { diff --git a/imports/ui/pages/signup.js b/imports/ui/pages/signup.js index 6784e50..f32bdc6 100644 --- a/imports/ui/pages/signup.js +++ b/imports/ui/pages/signup.js @@ -1,15 +1,15 @@ import React from 'react'; import { Link } from 'react-router'; import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; -import { handleSignup } from '../../modules/signup'; +import handleSignup from '../../modules/signup'; export default class Signup extends React.Component { componentDidMount() { handleSignup({ component: this }); } - handleSubmit() { - this.preventDefault(); + handleSubmit(event) { + event.preventDefault(); } render() { diff --git a/package.json b/package.json index 5207367..e82b3ca 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "eslint-plugin-import": "^1.16.0", "eslint-plugin-jsx-a11y": "^2.2.3", "eslint-plugin-meteor": "^4.0.1", - "eslint-plugin-react": "^6.4.1", - "faker": "^3.1.0" + "eslint-plugin-react": "^6.4.1" }, "eslintConfig": { "parserOptions": { @@ -58,13 +57,14 @@ "_name" ] } - ] + ], + "class-methods-use-this": 0 } }, "dependencies": { "bcrypt": "^0.8.7", "bootstrap": "^3.3.7", - "jquery": "^3.1.1", + "jquery": "^2.2.4", "jquery-validation": "^1.15.1", "react": "^15.3.2", "react-addons-pure-render-mixin": "^15.3.2", -- 2.0.0