diff --git a/.gitignore b/.gitignore index 45f7459556..148e02309a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ bundle.tar.gz jsdoc-conf.json jsdoc.json packages/nova-router/.npm +npm-debug.log diff --git a/.meteor/versions b/.meteor/versions index dc907cf521..ab622b8de0 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -19,7 +19,6 @@ callback-hook@1.0.10 check@1.2.4 chuangbo:cookie@1.1.0 coffeescript@1.11.1_4 -customization-demo@0.0.0 dburles:collection-helpers@1.1.0 ddp@1.2.5 ddp-client@1.3.2 @@ -34,7 +33,6 @@ ejson@1.0.13 email@1.1.18 fortawesome:fontawesome@4.5.0 fourseven:scss@3.10.1 -framework-demo@0.0.0 geojson-utils@1.0.10 hot-code-push@1.0.4 html-tools@1.0.11 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53349cc23e..a1bfdfb9bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ - Come check-in in the [Telescope Slack channel](http://slack.telescopeapp.org/). 👋 -- Completely new features should be shipped as external packages with their own repos (see [3rd party packages](https://github.com/TelescopeJS/Telescope#third-party-plugins)). Don't hesitate to come by the [Slack channel](http://slack.telescopeapp.org/) to speak about it. +- Completely new features should be shipped as external packages with their own repos (see [3rd party packages](http://nova-docs.telescopeapp.org/plugins.html)). Don't hesitate to come by the [Slack channel](http://slack.telescopeapp.org/) to speak about it. - We don't have test at the moment, and Travis integration is broken. If you know how to fix it, you are welcome (see [#1253](https://github.com/TelescopeJS/Telescope/issues/1253)! diff --git a/README.md b/README.md index 77979e9bd0..e96b9f6ca9 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,12 @@ This is the Apollo/GraphQL version of Telescope Nova. [You can find the documentation here](http://nova-docs.telescopeapp.org/). +The fastest way to get started is: +```sh +npm install +npm start +``` + ### Other Versions You can find the older, non-Apollo version of Telescope Nova on the [nova-classic](https://github.com/TelescopeJS/Telescope/tree/nova-classic) branch. diff --git a/license.md b/license.md index 72e616d10e..1c225eccf1 100644 --- a/license.md +++ b/license.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Telescope Nova +Copyright (c) 2017 Telescope Nova Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/npm-debug.log.14739240 b/npm-debug.log.14739240 deleted file mode 100644 index 9a44a301a8..0000000000 --- a/npm-debug.log.14739240 +++ /dev/null @@ -1,25 +0,0 @@ -0 info it worked if it ends with ok -1 verbose cli [ '/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/node', -1 verbose cli '/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/npm', -1 verbose cli 'config', -1 verbose cli '--loglevel=warn', -1 verbose cli 'get', -1 verbose cli 'prefix' ] -2 info using npm@3.10.10 -3 info using node@v7.2.1 -4 verbose exit [ 0, true ] -5 verbose stack Error: write EPIPE -5 verbose stack at exports._errnoException (util.js:1022:11) -5 verbose stack at WriteWrap.afterWrite [as oncomplete] (net.js:804:14) -6 verbose cwd /Users/sachagreif/Dev/Telescope -7 error Darwin 16.3.0 -8 error argv "/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/node" "/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/npm" "config" "--loglevel=warn" "get" "prefix" -9 error node v7.2.1 -10 error npm v3.10.10 -11 error code EPIPE -12 error errno EPIPE -13 error syscall write -14 error write EPIPE -15 error If you need help, you may report this error at: -15 error -16 verbose exit [ 1, true ] diff --git a/npm-debug.log.748773427 b/npm-debug.log.748773427 deleted file mode 100644 index 9a44a301a8..0000000000 --- a/npm-debug.log.748773427 +++ /dev/null @@ -1,25 +0,0 @@ -0 info it worked if it ends with ok -1 verbose cli [ '/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/node', -1 verbose cli '/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/npm', -1 verbose cli 'config', -1 verbose cli '--loglevel=warn', -1 verbose cli 'get', -1 verbose cli 'prefix' ] -2 info using npm@3.10.10 -3 info using node@v7.2.1 -4 verbose exit [ 0, true ] -5 verbose stack Error: write EPIPE -5 verbose stack at exports._errnoException (util.js:1022:11) -5 verbose stack at WriteWrap.afterWrite [as oncomplete] (net.js:804:14) -6 verbose cwd /Users/sachagreif/Dev/Telescope -7 error Darwin 16.3.0 -8 error argv "/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/node" "/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/npm" "config" "--loglevel=warn" "get" "prefix" -9 error node v7.2.1 -10 error npm v3.10.10 -11 error code EPIPE -12 error errno EPIPE -13 error syscall write -14 error write EPIPE -15 error If you need help, you may report this error at: -15 error -16 verbose exit [ 1, true ] diff --git a/package.json b/package.json index 9eae93d888..3ed8980561 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,13 @@ "npm": "^3.0" }, "scripts": { + "prestart": "sh prestart_nova.sh", + "start": "meteor --settings settings.json", "lint": "eslint --cache --ext .jsx,js packages" }, "dependencies": { - "apollo-client": "^0.7.3", + "analytics-node": "^2.1.1", + "apollo-client": "^0.8.1", "babel-runtime": "^6.18.0", "bcrypt": "^0.8.7", "body-parser": "^1.15.2", @@ -44,7 +47,7 @@ "optics-agent": "^1.0.5", "react": "^15.4.1", "react-addons-pure-render-mixin": "^15.4.1", - "react-apollo": "^0.8.1", + "react-apollo": "^0.9.0", "react-bootstrap": "^0.30.7", "react-bootstrap-datetimepicker": "0.0.22", "react-cookie": "^0.4.6", diff --git a/packages/_react-router-ssr/lib/client.jsx b/packages/_react-router-ssr/lib/client.jsx index 88cd0a214e..6b37835192 100644 --- a/packages/_react-router-ssr/lib/client.jsx +++ b/packages/_react-router-ssr/lib/client.jsx @@ -9,12 +9,6 @@ const ReactRouterSSR = { clientOptions = {}; } - let history = browserHistory; - - if(typeof clientOptions.historyHook === 'function') { - history = clientOptions.historyHook(history); - } - Meteor.startup(function() { const rootElementName = clientOptions.rootElement || 'react-app'; const rootElementType = clientOptions.rootElementType || 'div'; @@ -47,6 +41,12 @@ const ReactRouterSSR = { }); } + let history = browserHistory; + + if(typeof clientOptions.historyHook === 'function') { + history = clientOptions.historyHook(history); + } + let app = ( { @@ -194,6 +194,7 @@ function generateSSRData(clientOptions, serverOptions, req, res, renderProps) { global.__STYLE_COLLECTOR_MODULES__ = []; global.__STYLE_COLLECTOR__ = ''; + req.css = ''; renderProps = { ...renderProps, @@ -203,9 +204,9 @@ function generateSSRData(clientOptions, serverOptions, req, res, renderProps) { // fetchComponentData(serverOptions, renderProps); let app = ; - if (typeof clientOptions.wrapperHook === 'function') { + if (typeof serverOptions.wrapperHook === 'function') { const loginToken = req.cookies['meteor_login_token']; - app = clientOptions.wrapperHook(app, loginToken); + app = serverOptions.wrapperHook(req, res, app, loginToken); } if (serverOptions.preRender) { @@ -221,7 +222,7 @@ function generateSSRData(clientOptions, serverOptions, req, res, renderProps) { css = global.__STYLE_COLLECTOR__; if (typeof serverOptions.dehydrateHook === 'function') { - const data = serverOptions.dehydrateHook(); + const data = serverOptions.dehydrateHook(req, res); InjectData.pushData(res, 'dehydrated-initial-data', JSON.stringify(data)); } @@ -229,6 +230,8 @@ function generateSSRData(clientOptions, serverOptions, req, res, renderProps) { serverOptions.postRender(req, res); } + css = css + req.css; + // I'm pretty sure this could be avoided in a more elegant way? const context = FastRender.frContext.get(); const data = context.getData(); diff --git a/packages/customization-demo/lib/components/CustomPostsItem.jsx b/packages/customization-demo/lib/components/CustomPostsItem.jsx index d29f7a38ef..692972959f 100644 --- a/packages/customization-demo/lib/components/CustomPostsItem.jsx +++ b/packages/customization-demo/lib/components/CustomPostsItem.jsx @@ -1,9 +1,8 @@ -import { Components, getRawComponent, replaceComponent } from 'meteor/nova:lib'; +import { Components, getRawComponent, replaceComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { FormattedMessage, FormattedRelative } from 'react-intl'; import { Link } from 'react-router'; import Posts from "meteor/nova:posts"; -import gql from 'graphql-tag'; class CustomPostsItem extends getRawComponent('PostsItem') { @@ -60,55 +59,4 @@ class CustomPostsItem extends getRawComponent('PostsItem') { } } -CustomPostsItem.propTypes = { - currentUser: React.PropTypes.object, - post: React.PropTypes.object.isRequired -}; - -CustomPostsItem.fragment = gql` - fragment CustomPostsItemFragment on Post { - _id - title - url - slug - thumbnailUrl - baseScore - postedAt - sticky - status - categories { - # ...minimumCategoryInfo - _id - name - slug - } - commentCount - commenters { - # ...avatarUserInfo - _id - displayName - emailHash - slug - } - upvoters { - _id - } - downvoters { - _id - } - upvotes # should be asked only for admins? - score # should be asked only for admins? - viewCount # should be asked only for admins? - clickCount # should be asked only for admins? - user { - # ...avatarUserInfo - _id - displayName - emailHash - slug - } - color - } -`; - replaceComponent('PostsItem', CustomPostsItem); diff --git a/packages/customization-demo/lib/fragments.js b/packages/customization-demo/lib/fragments.js new file mode 100644 index 0000000000..7a59405969 --- /dev/null +++ b/packages/customization-demo/lib/fragments.js @@ -0,0 +1,9 @@ +import { extendFragment } from 'meteor/nova:core'; + +extendFragment('PostsList', ` + color # new custom property! +`); + +extendFragment('PostsPage', ` + color # new custom property! +`); \ No newline at end of file diff --git a/packages/customization-demo/lib/modules.js b/packages/customization-demo/lib/modules.js index 360183c59e..1be9d586d6 100644 --- a/packages/customization-demo/lib/modules.js +++ b/packages/customization-demo/lib/modules.js @@ -8,6 +8,7 @@ import "./emails.js" import "./custom_fields.js" import "./i18n.js" import "./groups.js" +import "./fragments.js" // custom components import "./components/CustomLogo.jsx"; diff --git a/packages/framework-demo/lib/components/MoviesItem.jsx b/packages/framework-demo/lib/components/MoviesItem.jsx index 7f26371cd7..ab675ede82 100644 --- a/packages/framework-demo/lib/components/MoviesItem.jsx +++ b/packages/framework-demo/lib/components/MoviesItem.jsx @@ -7,8 +7,7 @@ Wrapped with the "withCurrentUser" container. import React, { PropTypes, Component } from 'react'; import { Button } from 'react-bootstrap'; -import { ModalTrigger } from 'meteor/nova:core'; -import { Components, registerComponent, withCurrentUser } from 'meteor/nova:core'; +import { Components, registerComponent, withCurrentUser, ModalTrigger } from 'meteor/nova:core'; import Movies from '../collection.js'; class MoviesItem extends Component { diff --git a/packages/framework-demo/lib/mutations.js b/packages/framework-demo/lib/mutations.js index 0906441393..ef024abbc7 100644 --- a/packages/framework-demo/lib/mutations.js +++ b/packages/framework-demo/lib/mutations.js @@ -14,11 +14,11 @@ Each mutation has: */ -import { newMutation, editMutation, removeMutation } from 'meteor/nova:core'; +import { newMutation, editMutation, removeMutation, Utils } from 'meteor/nova:core'; import Users from 'meteor/nova:users'; const performCheck = (mutation, user, document) => { - if (!mutation.check(user, document)) throw new Error(`Sorry, you don't have the rights to perform the mutation ${mutation.name} on document _id = ${document._id}`); + if (!mutation.check(user, document)) throw new Error(Utils.encodeIntlError({id: `app.mutation_not_allowed`, value: `"${mutation.name}" on _id "${document._id}"`})); } const mutations = { diff --git a/packages/framework-demo/lib/schema.js b/packages/framework-demo/lib/schema.js index 28aab33a02..0188d14f17 100644 --- a/packages/framework-demo/lib/schema.js +++ b/packages/framework-demo/lib/schema.js @@ -5,7 +5,6 @@ A SimpleSchema-compatible JSON schema */ import Users from 'meteor/nova:users'; -import { GraphQLSchema } from 'meteor/nova:core'; // define schema const schema = { diff --git a/packages/nova-apollo/lib/export.js b/packages/nova-apollo/lib/export.js index a2747706ca..d186cb728f 100644 --- a/packages/nova-apollo/lib/export.js +++ b/packages/nova-apollo/lib/export.js @@ -1,4 +1,4 @@ -import { GraphQLSchema } from 'meteor/nova:lib'; +import { GraphQLSchema } from 'meteor/nova:core'; import { makeExecutableSchema } from 'graphql-tools'; diff --git a/packages/nova-apollo/lib/schema.js b/packages/nova-apollo/lib/schema.js index e8ddab4748..47b911b14c 100644 --- a/packages/nova-apollo/lib/schema.js +++ b/packages/nova-apollo/lib/schema.js @@ -1,4 +1,4 @@ -import { GraphQLSchema } from 'meteor/nova:lib'; +import { GraphQLSchema } from 'meteor/nova:core'; const generateTypeDefs = () => [` diff --git a/packages/nova-apollo/lib/server.js b/packages/nova-apollo/lib/server.js index 4d7512aaa2..83c3188261 100644 --- a/packages/nova-apollo/lib/server.js +++ b/packages/nova-apollo/lib/server.js @@ -19,7 +19,7 @@ import { _ } from 'meteor/underscore'; import Users from 'meteor/nova:users'; -import { GraphQLSchema } from 'meteor/nova:lib'; +import { GraphQLSchema } from 'meteor/nova:core'; import OpticsAgent from 'optics-agent' diff --git a/packages/nova-base-components/lib/categories/CategoriesEditForm.jsx b/packages/nova-base-components/lib/categories/CategoriesEditForm.jsx index 4c8a5a4aa6..dbbec16609 100644 --- a/packages/nova-base-components/lib/categories/CategoriesEditForm.jsx +++ b/packages/nova-base-components/lib/categories/CategoriesEditForm.jsx @@ -1,23 +1,25 @@ import React, { PropTypes, Component } from 'react'; import { intlShape } from 'react-intl'; -import { Components, registerComponent, getRawComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, getFragment, withMessages } from 'meteor/nova:core'; import Categories from "meteor/nova:categories"; -import { withMessages } from 'meteor/nova:core'; const CategoriesEditForm = (props, context) => { return (
+
+
ID: {props.category._id}
+
{ - props.closeCallback(); + props.closeModal(); props.flash(context.intl.formatMessage({id: 'categories.edit_success'}, {name: category.name}), "success"); }} removeSuccessCallback={({documentId, documentTitle}) => { - props.closeCallback(); + props.closeModal(); props.flash(context.intl.formatMessage({id: 'categories.delete_success'}, {name: documentTitle}), "success"); // context.events.track("category deleted", {_id: documentId}); }} @@ -30,7 +32,7 @@ const CategoriesEditForm = (props, context) => { CategoriesEditForm.propTypes = { category: React.PropTypes.object.isRequired, - closeCallback: React.PropTypes.func, + closeModal: React.PropTypes.func, flash: React.PropTypes.func, } diff --git a/packages/nova-base-components/lib/categories/CategoriesList.jsx b/packages/nova-base-components/lib/categories/CategoriesList.jsx index b2488bfca8..56ac0430d4 100644 --- a/packages/nova-base-components/lib/categories/CategoriesList.jsx +++ b/packages/nova-base-components/lib/categories/CategoriesList.jsx @@ -1,81 +1,13 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { ModalTrigger, Components, registerComponent, withList, Utils } from "meteor/nova:core"; import React, { PropTypes, Component } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Button, DropdownButton, MenuItem, Modal } from 'react-bootstrap'; -import { ShowIf, withList } from "meteor/nova:core"; +import { Button, DropdownButton, MenuItem } from 'react-bootstrap'; import { withRouter } from 'react-router' import { LinkContainer } from 'react-router-bootstrap'; import Categories from 'meteor/nova:categories'; -import gql from 'graphql-tag'; - -// note: cannot use ModalTrigger component because of https://github.com/react-bootstrap/react-bootstrap/issues/1808 class CategoriesList extends Component { - constructor() { - super(); - this.openCategoryEditModal = this.openCategoryEditModal.bind(this); - this.openCategoryNewModal = this.openCategoryNewModal.bind(this); - this.closeModal = this.closeModal.bind(this); - this.state = { - openModal: false - } - } - - openCategoryNewModal() { - // new category modal has number 0 - this.setState({openModal: 0}); - } - - openCategoryEditModal(index) { - // edit category modals are numbered from 1 to n - this.setState({openModal: index+1}); - } - - closeModal() { - this.setState({openModal: false}); - } - - renderCategoryEditModal(category, index) { - - return ( - - - - - - - - - ) - } - - renderCategoryNewModal() { - - return ( - - - - - - - - - ) - } - - renderCategoryNewButton() { - return ( -
- - - -
- ); - } - render() { const categories = this.props.results; @@ -83,6 +15,9 @@ class CategoriesList extends Component { const currentQuery = _.clone(this.props.router.location.query); delete currentQuery.cat; + const categoriesClone = _.map(categories, _.clone); // we don't want to modify the objects we got from props + const nestedCategories = Utils.unflatten(categoriesClone, '_id', 'parentId'); + return (
0 ? - categories.map((category, index) => ) + nestedCategories && nestedCategories.length > 0 ? + nestedCategories.map((category, index) => ) // not any category found : null // categories are loading :
} - {this.renderCategoryNewButton()} + +
+ } component={}> + + +
+
-
- { - /* - Modals cannot be inside DropdownButton component (see GH issue link on top of the file) - -> we place them in a
outside the component - */ - - /* Modals for each category to edit */ - // categories data are loaded - !this.props.loading ? - // there are currently categories - categories && categories.length > 0 ? - categories.map((category, index) => this.renderCategoryEditModal(category, index)) - // not any category found - : null - // categories are loading - : null - } - - { - /* modal for creating a new category */ - this.renderCategoryNewModal() - } -
) @@ -146,23 +63,13 @@ CategoriesList.propTypes = { results: React.PropTypes.array, }; -CategoriesList.fragment = gql` - fragment categoriesListFragment on Category { - _id - name - description - order - slug - image - } -`; -const categoriesListOptions = { +const options = { collection: Categories, queryName: 'categoriesListQuery', - fragment: CategoriesList.fragment, + fragmentName: 'CategoriesList', limit: 0, pollInterval: 0, }; -registerComponent('CategoriesList', CategoriesList, withRouter, withList(categoriesListOptions)); +registerComponent('CategoriesList', CategoriesList, withRouter, [withList, options]); diff --git a/packages/nova-base-components/lib/categories/CategoriesNewForm.jsx b/packages/nova-base-components/lib/categories/CategoriesNewForm.jsx index 5923a49916..689c29ad42 100644 --- a/packages/nova-base-components/lib/categories/CategoriesNewForm.jsx +++ b/packages/nova-base-components/lib/categories/CategoriesNewForm.jsx @@ -1,17 +1,17 @@ import React, { PropTypes, Component } from 'react'; import { intlShape } from 'react-intl'; -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, getFragment, withMessages } from 'meteor/nova:core'; import Categories from "meteor/nova:categories"; -import { withMessages } from 'meteor/nova:core'; const CategoriesNewForm = (props, context) => { return (
{ - props.closeCallback(); + props.closeModal(); props.flash(context.intl.formatMessage({id: 'categories.new_success'}, {name: category.name}), "success"); }} /> diff --git a/packages/nova-base-components/lib/categories/CategoriesNode.jsx b/packages/nova-base-components/lib/categories/CategoriesNode.jsx new file mode 100644 index 0000000000..2eb06fb1e0 --- /dev/null +++ b/packages/nova-base-components/lib/categories/CategoriesNode.jsx @@ -0,0 +1,39 @@ +import { Components, registerComponent } from 'meteor/nova:core'; +import React, { PropTypes, Component } from 'react'; + +class CategoriesNode extends Component { + + renderCategory(category) { + return ( + + ) + } + + renderChildren(children) { + return ( +
+ {children.map(category => )} +
+ ) + } + + render() { + + const category = this.props.category; + const children = this.props.category.childrenResults; + + return ( +
+ {this.renderCategory(category)} + {children ? this.renderChildren(children) : null} +
+ ) + } + +} + +CategoriesNode.propTypes = { + category: React.PropTypes.object.isRequired, // the current category +}; + +registerComponent('CategoriesNode', CategoriesNode); diff --git a/packages/nova-base-components/lib/categories/Category.jsx b/packages/nova-base-components/lib/categories/Category.jsx index 591b52900f..acfc7207b9 100644 --- a/packages/nova-base-components/lib/categories/Category.jsx +++ b/packages/nova-base-components/lib/categories/Category.jsx @@ -1,24 +1,18 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { ModalTrigger, Components, registerComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { LinkContainer } from 'react-router-bootstrap'; import { MenuItem } from 'react-bootstrap'; import { withRouter } from 'react-router' import Categories from 'meteor/nova:categories'; -import { ShowIf } from 'meteor/nova:core'; class Category extends Component { renderEdit() { return ( - - - - ); - // return ( - // }> - // - // - // ) + }> + + + ) } render() { diff --git a/packages/nova-base-components/lib/client.js b/packages/nova-base-components/lib/client.js index 87c7840147..aa7fcec8fd 100644 --- a/packages/nova-base-components/lib/client.js +++ b/packages/nova-base-components/lib/client.js @@ -1,3 +1,4 @@ +import './fragments.js'; import './components.js'; import './config.js'; import './routes.js'; diff --git a/packages/nova-base-components/lib/comments/CommentsEditForm.jsx b/packages/nova-base-components/lib/comments/CommentsEditForm.jsx index 4f7f037b17..868dba6e9d 100644 --- a/packages/nova-base-components/lib/comments/CommentsEditForm.jsx +++ b/packages/nova-base-components/lib/comments/CommentsEditForm.jsx @@ -1,7 +1,6 @@ -import { Components, registerComponent, getRawComponent } from 'meteor/nova:core'; +import { Components, registerComponent, getFragment, withMessages } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import Comments from "meteor/nova:comments"; -import { withMessages } from 'meteor/nova:core'; const CommentsEditForm = (props, context) => { return ( @@ -14,7 +13,7 @@ const CommentsEditForm = (props, context) => { cancelCallback={props.cancelCallback} removeSuccessCallback={props.removeSuccessCallback} showRemove={true} - mutationFragment={getRawComponent('PostsCommentsThread').fragment} + mutationFragment={getFragment('CommentsList')} />
) @@ -26,4 +25,4 @@ CommentsEditForm.propTypes = { cancelCallback: React.PropTypes.func }; -registerComponent('CommentsEditForm', CommentsEditForm, withMessages); \ No newline at end of file +registerComponent('CommentsEditForm', CommentsEditForm, withMessages); diff --git a/packages/nova-base-components/lib/comments/CommentsItem.jsx b/packages/nova-base-components/lib/comments/CommentsItem.jsx index cdc164a7dd..3be206f08c 100644 --- a/packages/nova-base-components/lib/comments/CommentsItem.jsx +++ b/packages/nova-base-components/lib/comments/CommentsItem.jsx @@ -1,7 +1,6 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, withCurrentUser, withMessages } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { intlShape, FormattedMessage, FormattedRelative } from 'react-intl'; -import { ShowIf, withCurrentUser, withMessages } from 'meteor/nova:core'; import Comments from 'meteor/nova:comments'; class CommentsItem extends Component{ @@ -100,6 +99,9 @@ class CommentsItem extends Component{
+
+ +
diff --git a/packages/nova-base-components/lib/comments/CommentsList.jsx b/packages/nova-base-components/lib/comments/CommentsList.jsx index 6a3da843cf..0a3ba157ea 100644 --- a/packages/nova-base-components/lib/comments/CommentsList.jsx +++ b/packages/nova-base-components/lib/comments/CommentsList.jsx @@ -1,4 +1,4 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React from 'react'; import { FormattedMessage } from 'react-intl'; diff --git a/packages/nova-base-components/lib/comments/CommentsLoadMore.jsx b/packages/nova-base-components/lib/comments/CommentsLoadMore.jsx index 2629a50c72..48c9060691 100644 --- a/packages/nova-base-components/lib/comments/CommentsLoadMore.jsx +++ b/packages/nova-base-components/lib/comments/CommentsLoadMore.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React from 'react'; const CommentsLoadMore = ({loadMore, count, totalCount}) => { diff --git a/packages/nova-base-components/lib/comments/CommentsNew.jsx b/packages/nova-base-components/lib/comments/CommentsNew.jsx deleted file mode 100644 index f518160b3e..0000000000 --- a/packages/nova-base-components/lib/comments/CommentsNew.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { PropTypes, Component } from 'react'; -import Comments from "meteor/nova:comments"; - -class CommentsNew extends Component { - - render() { - - let prefilledProps = {postId: this.props.postId}; - - if (this.props.parentComment) { - prefilledProps = Object.assign(prefilledProps, { - parentCommentId: this.props.parentComment._id, - // if parent comment has a topLevelCommentId use it; if it doesn't then it *is* the top level comment - topLevelCommentId: this.props.parentComment.topLevelCommentId || this.props.parentComment._id - }); - } - - return ( -
- -
- ) - } - -} - -CommentsNew.propTypes = { - postId: React.PropTypes.string.isRequired, - type: React.PropTypes.string, // "comment" or "reply" - parentComment: React.PropTypes.object, // if reply, the comment being replied to - parentCommentId: React.PropTypes.string, // if reply - topLevelCommentId: React.PropTypes.string, // if reply - successCallback: React.PropTypes.func, // a callback to execute when the submission has been successful - cancelCallback: React.PropTypes.func -} - -CommentsNew.contextTypes = { - currentUser: React.PropTypes.object -} - -module.exports = CommentsNew; diff --git a/packages/nova-base-components/lib/comments/CommentsNewForm.jsx b/packages/nova-base-components/lib/comments/CommentsNewForm.jsx index 7a8167a845..08f0d2be24 100644 --- a/packages/nova-base-components/lib/comments/CommentsNewForm.jsx +++ b/packages/nova-base-components/lib/comments/CommentsNewForm.jsx @@ -1,7 +1,6 @@ -import { Components, registerComponent, getRawComponent } from 'meteor/nova:core'; +import { Components, registerComponent, getFragment, withMessages } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import Comments from "meteor/nova:comments"; -import { ShowIf, withMessages } from 'meteor/nova:core'; import { FormattedMessage } from 'react-intl'; const CommentsNewForm = (props, context) => { @@ -24,7 +23,7 @@ const CommentsNewForm = (props, context) => {
{ diff --git a/packages/nova-base-components/lib/common/Footer.jsx b/packages/nova-base-components/lib/common/Footer.jsx index 9a2f96159a..28aee1efdb 100644 --- a/packages/nova-base-components/lib/common/Footer.jsx +++ b/packages/nova-base-components/lib/common/Footer.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React from 'react'; import { FormattedMessage } from 'react-intl'; diff --git a/packages/nova-base-components/lib/common/HeadTags.jsx b/packages/nova-base-components/lib/common/HeadTags.jsx index 23db5197d5..800fe9779c 100644 --- a/packages/nova-base-components/lib/common/HeadTags.jsx +++ b/packages/nova-base-components/lib/common/HeadTags.jsx @@ -22,6 +22,7 @@ class HeadTags extends Component { image = Utils.getSiteUrl() + image; } + // add markup specific to the page rendered const meta = Headtags.meta.concat([ { charset: "utf-8" }, { name: "description", content: description }, @@ -40,6 +41,7 @@ class HeadTags extends Component { { name: "twitter:description", content: description } ]); + // add markup specific to the page rendered const link = Headtags.link.concat([ { rel: "canonical", href: Utils.getSiteUrl() }, { rel: "shortcut icon", href: getSetting("faviconUrl", "/img/favicon.ico") } @@ -47,7 +49,7 @@ class HeadTags extends Component { return (
- +
); } diff --git a/packages/nova-base-components/lib/common/Layout.jsx b/packages/nova-base-components/lib/common/Layout.jsx index f3995ecd15..c10b0e19d5 100644 --- a/packages/nova-base-components/lib/common/Layout.jsx +++ b/packages/nova-base-components/lib/common/Layout.jsx @@ -1,4 +1,4 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; class Layout extends Component { diff --git a/packages/nova-base-components/lib/common/Logo.jsx b/packages/nova-base-components/lib/common/Logo.jsx index 01cfbd8765..09ce34fc46 100644 --- a/packages/nova-base-components/lib/common/Logo.jsx +++ b/packages/nova-base-components/lib/common/Logo.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React from 'react'; import { IndexLink } from 'react-router'; diff --git a/packages/nova-base-components/lib/common/Newsletter.jsx b/packages/nova-base-components/lib/common/Newsletter.jsx index ff3c5df41d..4888b6fc50 100644 --- a/packages/nova-base-components/lib/common/Newsletter.jsx +++ b/packages/nova-base-components/lib/common/Newsletter.jsx @@ -1,4 +1,4 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, withCurrentUser, withMutation, withMessages } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import Formsy from 'formsy-react'; @@ -6,7 +6,6 @@ import { Input } from 'formsy-react-components'; import { Button } from 'react-bootstrap'; import Cookie from 'react-cookie'; import Users from 'meteor/nova:users'; -import { withCurrentUser, withMutation, withMessages } from 'meteor/nova:core'; class Newsletter extends Component { @@ -27,13 +26,13 @@ class Newsletter extends Component { } } - subscribeEmail(data) { - this.props.addEmailNewsletter({email: data.email}).then(result => { + async subscribeEmail(data) { + try { + const result = await this.props.addEmailNewsletter({email: data.email}); this.successCallbackSubscription(result); - }).catch(error => { - console.log(error); + } catch(error) { this.props.flash(error.message, "error"); - }); + } } successCallbackSubscription(result) { @@ -100,8 +99,6 @@ function showBanner (user) { return ( // showBanner cookie either doesn't exist or is not set to "no" Cookie.load('showBanner') !== "no" - // and showBanner user setting either doesn't exist or is set to true - // && Users.getSetting(user, 'newsletter.showBanner', true) // and user is not subscribed to the newsletter already (setting either DNE or is not set to false) && !Users.getSetting(user, 'newsletter_subscribeToNewsletter', false) ); diff --git a/packages/nova-base-components/lib/common/NewsletterButton.jsx b/packages/nova-base-components/lib/common/NewsletterButton.jsx index d00ff15cf7..767d6dacd3 100644 --- a/packages/nova-base-components/lib/common/NewsletterButton.jsx +++ b/packages/nova-base-components/lib/common/NewsletterButton.jsx @@ -1,8 +1,7 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, withMutation, withCurrentUser, withMessages } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'react-bootstrap'; -import { withMutation, withCurrentUser, withMessages } from 'meteor/nova:core'; class NewsletterButton extends Component { constructor(props) { diff --git a/packages/nova-base-components/lib/common/SearchForm.jsx b/packages/nova-base-components/lib/common/SearchForm.jsx index 2b0871ccf5..65803688bd 100644 --- a/packages/nova-base-components/lib/common/SearchForm.jsx +++ b/packages/nova-base-components/lib/common/SearchForm.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { intlShape } from 'react-intl'; import Formsy from 'formsy-react'; diff --git a/packages/nova-base-components/lib/common/Vote.jsx b/packages/nova-base-components/lib/common/Vote.jsx index a298509152..20fb267f9a 100644 --- a/packages/nova-base-components/lib/common/Vote.jsx +++ b/packages/nova-base-components/lib/common/Vote.jsx @@ -1,7 +1,6 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, withMessages } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import classNames from 'classnames'; -import { withCurrentUser, withMessages } from 'meteor/nova:core'; import { withVote, hasUpvoted, hasDownvoted } from 'meteor/nova:voting'; class Vote extends Component { diff --git a/packages/nova-base-components/lib/components.js b/packages/nova-base-components/lib/components.js index 71e42046de..3a5a21463f 100644 --- a/packages/nova-base-components/lib/components.js +++ b/packages/nova-base-components/lib/components.js @@ -51,14 +51,11 @@ import './comments/CommentsLoadMore.jsx'; // categories import './categories/CategoriesList.jsx'; +import './categories/CategoriesNode.jsx'; import './categories/Category.jsx'; import './categories/CategoriesEditForm.jsx'; import './categories/CategoriesNewForm.jsx'; -// permissions - -import './permissions/CanDo.jsx'; - // users import './users/UsersSingle.jsx'; diff --git a/packages/nova-base-components/lib/fragments.js b/packages/nova-base-components/lib/fragments.js new file mode 100644 index 0000000000..84a416289c --- /dev/null +++ b/packages/nova-base-components/lib/fragments.js @@ -0,0 +1,183 @@ +import { registerFragment } from 'meteor/nova:core'; + +// ------------------------------ Vote ------------------------------ // + +// note: fragment used by default on the UsersProfile fragment +registerFragment(` + fragment VotedItem on Vote { + # nova:voting + itemId + power + votedAt + } +`); + +// ------------------------------ Users ------------------------------ // + +// note: fragment used by default on UsersProfile, PostsList & CommentsList fragments +registerFragment(` + fragment UsersMinimumInfo on User { + # nova:users + _id + slug + username + displayName + emailHash + } +`); + +registerFragment(` + fragment UsersProfile on User { + # nova:users + ...UsersMinimumInfo + createdAt + isAdmin + bio + htmlBio + twitterUsername + website + groups + karma + # nova:posts + postCount + # nova:comments + commentCount + # nova:newsletter + newsletter_subscribeToNewsletter + # nova:notifications + notifications_users + notifications_posts + # nova:voting + downvotedComments { + ...VotedItem + } + downvotedPosts { + ...VotedItem + } + upvotedComments { + ...VotedItem + } + upvotedPosts { + ...VotedItem + } + } +`); + +// ------------------------------ Categories ------------------------------ // + +// note: fragment used by default on CategoriesList & PostsList fragments +registerFragment(` + fragment CategoriesMinimumInfo on Category { + # nova:categories + _id + name + slug + } +`); + +registerFragment(` + fragment CategoriesList on Category { + # nova:categories + ...CategoriesMinimumInfo + description + order + image + parentId + parent { + ...CategoriesMinimumInfo + } + } +`); + +// ------------------------------ Posts ------------------------------ // + +registerFragment(` + fragment PostsList on Post { + # nova:posts + _id + title + url + slug + postedAt + sticky + status + body + htmlBody + excerpt + viewCount + clickCount + # nova:users + userId + user { + ...UsersMinimumInfo + } + # nova:embedly + thumbnailUrl + # nova:categories + categories { + ...CategoriesMinimumInfo + } + # nova:comments + commentCount + commenters { + ...UsersMinimumInfo + } + # nova:voting + upvoters { + _id + } + downvoters { + _id + } + upvotes + downvotes + baseScore + score + } +`); + +registerFragment(` + fragment PostsPage on Post { + ...PostsList + } +`); + + +// ----------------------------- Comments ------------------------------ // + +registerFragment(` + fragment CommentsList on Comment { + # nova:comments + _id + postId + parentCommentId + topLevelCommentId + body + htmlBody + postedAt + # nova:users + userId + user { + ...UsersMinimumInfo + } + # nova:posts + post { + _id + commentCount + commenters { + ...UsersMinimumInfo + } + } + # nova:voting + upvoters { + _id + } + downvoters { + _id + } + upvotes + downvotes + baseScore + score + } +`); diff --git a/packages/nova-base-components/lib/permissions/CanCreatePost.jsx b/packages/nova-base-components/lib/permissions/CanCreatePost.jsx deleted file mode 100644 index 8f89b85e50..0000000000 --- a/packages/nova-base-components/lib/permissions/CanCreatePost.jsx +++ /dev/null @@ -1,35 +0,0 @@ -// Deprecated way to handle permission in components, check CanDo component - -// import Telescope from 'meteor/nova:lib'; -// import React, { PropTypes, Component } from 'react'; -// import { FormattedMessage } from 'react-intl'; -// import Users from 'meteor/nova:users'; - -// const CanCreatePost = (props, context) => { - -// const currentUser = context.currentUser; - -// const children = props.children; -// const UsersAccountForm = Telescope.components.UsersAccountForm; - -// if (!currentUser){ -// return ( -//
-//

-// -//
-// ) -// } else if (Users.canDo(currentUser, "posts.new")) { -// return children; -// } else { -// return

; -// } -// }; - -// CanCreatePost.contextTypes = { -// currentUser: React.PropTypes.object -// }; - -// CanCreatePost.displayName = "CanCreatePost"; - -// module.exports = CanCreatePost; \ No newline at end of file diff --git a/packages/nova-base-components/lib/permissions/CanDo.jsx b/packages/nova-base-components/lib/permissions/CanDo.jsx deleted file mode 100644 index 853a1fa1e2..0000000000 --- a/packages/nova-base-components/lib/permissions/CanDo.jsx +++ /dev/null @@ -1,59 +0,0 @@ -// import Telescope from 'meteor/nova:lib'; -// import React, { PropTypes } from 'react'; -// import { FormattedMessage } from 'react-intl'; -// import Users from 'meteor/nova:users'; -// import { withCurrentUser } from 'meteor/nova:core'; - -// const CanDo = (props, context) => { - -// // no user login, display the login form -// if (!props.currentUser && props.displayNoPermissionMessage) { -// return ( -//
-//

-// -//
-// ); -// } - -// // default permission, is the user allowed to perform this action? -// let permission = Users.canDo(props.currentUser, props.action); - -// // the permission is about viewing a document, check if the user is allowed -// if (props.document && props.action.indexOf('view') > -1) { -// // use the permission shortcut canView on the current user and requested document -// permission = Users.canView(props.currentUser, props.document); -// } - -// // the permission is about editing a document, check if the user is allowed -// if (props.document && props.action.indexOf('edit') > -1) { -// // use the permission shortcut canEdit on the current user and requested document -// permission = Users.canEdit(props.currentUser, props.document); -// } - - -// // the user can perform the intented action in the component: display the component, -// // else: display a not allowed message -// if (permission) { -// return props.children; -// } else { -// return props.displayNoPermissionMessage ?

: null; -// } -// }; - -// CanDo.propTypes = { -// action: React.PropTypes.string.isRequired, -// currentUser: React.PropTypes.object, -// document: React.PropTypes.object, -// noPermissionMessage: React.PropTypes.string, -// displayNoPermissionMessage: React.PropTypes.bool, -// }; - -// CanDo.defaultProps = { -// noPermissionMessage: 'app.noPermission', -// displayNoPermissionMessage: false, -// }; - -// CanDo.displayName = "CanDo"; - -// Telescope.registerComponent('CanDo', CanDo, withCurrentUser); \ No newline at end of file diff --git a/packages/nova-base-components/lib/permissions/CanEditPost.jsx b/packages/nova-base-components/lib/permissions/CanEditPost.jsx deleted file mode 100644 index bb0044ba45..0000000000 --- a/packages/nova-base-components/lib/permissions/CanEditPost.jsx +++ /dev/null @@ -1,23 +0,0 @@ -// Deprecated way to handle permission in components, check CanDo component - -// import React, { PropTypes, Component } from 'react'; -// import Users from 'meteor/nova:users'; - -// const CanEditPost = ({user, post, children}) => { -// if (Users.canEdit(user, post)) { -// return children; -// } else if (!user){ -// return

Please log in.

; -// } else { -// return

Sorry, you do not have permissions to edit this post at this time

; -// } -// }; - -// CanEditPost.propTypes = { -// user: React.PropTypes.object, -// post: React.PropTypes.object -// } - -// CanEditPost.displayName = "CanEditPost"; - -// module.exports = CanEditPost; \ No newline at end of file diff --git a/packages/nova-base-components/lib/permissions/CanEditUser.jsx b/packages/nova-base-components/lib/permissions/CanEditUser.jsx deleted file mode 100644 index 31c2df578f..0000000000 --- a/packages/nova-base-components/lib/permissions/CanEditUser.jsx +++ /dev/null @@ -1,23 +0,0 @@ -// Deprecated way to handle permission in components, check CanDo component - -// import React, { PropTypes, Component } from 'react'; -// import Users from 'meteor/nova:users'; - -// const CanEditUser = ({user, userToEdit, children}) => { -// if (!user){ -// return

Please log in.

; -// } else if (Users.canEdit(user, userToEdit)) { -// return children; -// } else { -// return

Sorry, you do not have permissions to edit this user at this time

; -// } -// }; - -// CanEditUser.propTypes = { -// user: React.PropTypes.object, -// userToEdit: React.PropTypes.object -// } - -// CanEditUser.displayName = "CanEditUser"; - -// module.exports = CanEditUser; \ No newline at end of file diff --git a/packages/nova-base-components/lib/permissions/CanView.jsx b/packages/nova-base-components/lib/permissions/CanView.jsx deleted file mode 100644 index ad12cfa417..0000000000 --- a/packages/nova-base-components/lib/permissions/CanView.jsx +++ /dev/null @@ -1,22 +0,0 @@ -// Deprecated way to handle permission in components, check CanDo component - -// import React, { PropTypes, Component } from 'react'; -// import Users from 'meteor/nova:users'; - -// const CanView = ({user, children}) => { -// if (Users.canDo(user, "posts.view.approved.all")) { -// return children; -// } else if (!user){ -// return

Please log in.

; -// } else { -// return

Sorry, you do not have permissions to post at this time

; -// } -// }; - -// CanView.propTypes = { -// user: React.PropTypes.object -// } - -// CanView.displayName = "CanView"; - -// module.exports = CanView; \ No newline at end of file diff --git a/packages/nova-base-components/lib/permissions/CanViewPost.jsx b/packages/nova-base-components/lib/permissions/CanViewPost.jsx deleted file mode 100644 index 02046d7d58..0000000000 --- a/packages/nova-base-components/lib/permissions/CanViewPost.jsx +++ /dev/null @@ -1,23 +0,0 @@ -// Deprecated way to handle permission in components, check CanDo component - -// import React, { PropTypes, Component } from 'react'; -// import Users from 'meteor/nova:users'; - -// const CanViewPost = ({user, post, children}) => { -// if (Users.canView(this.props.user, this.props.document)) { -// return this.props.children; -// } else if (!this.props.user){ -// return

Please log in.

; -// } else { -// return

Sorry, you do not have permissions to post at this time

; -// } -// }; - -// CanViewPost.propTypes = { -// user: React.PropTypes.object, -// post: React.PropTypes.object -// } - -// CanViewPost.displayName = "CanViewPost"; - -// module.exports = CanViewPost; \ No newline at end of file diff --git a/packages/nova-base-components/lib/posts/PostsCategories.jsx b/packages/nova-base-components/lib/posts/PostsCategories.jsx index 8c39237914..a93dd290a7 100644 --- a/packages/nova-base-components/lib/posts/PostsCategories.jsx +++ b/packages/nova-base-components/lib/posts/PostsCategories.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React from 'react'; import { Link } from 'react-router'; diff --git a/packages/nova-base-components/lib/posts/PostsCommenters.jsx b/packages/nova-base-components/lib/posts/PostsCommenters.jsx index 539e6bd4c7..26b0262389 100644 --- a/packages/nova-base-components/lib/posts/PostsCommenters.jsx +++ b/packages/nova-base-components/lib/posts/PostsCommenters.jsx @@ -1,4 +1,4 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React from 'react'; import { Link } from 'react-router'; import Posts from "meteor/nova:posts"; diff --git a/packages/nova-base-components/lib/posts/PostsCommentsThread.jsx b/packages/nova-base-components/lib/posts/PostsCommentsThread.jsx index 3f9b51a36e..8befb11ccf 100644 --- a/packages/nova-base-components/lib/posts/PostsCommentsThread.jsx +++ b/packages/nova-base-components/lib/posts/PostsCommentsThread.jsx @@ -2,7 +2,6 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { ModalTrigger, withList, withCurrentUser, Components, registerComponent, Utils } from 'meteor/nova:core'; import Comments from 'meteor/nova:comments'; -import gql from 'graphql-tag'; const PostsCommentsThread = (props, context) => { @@ -46,40 +45,11 @@ PostsCommentsThread.propTypes = { currentUser: React.PropTypes.object }; -PostsCommentsThread.fragment = gql` - fragment commentsListFragment on Comment { - _id - postId - parentCommentId - topLevelCommentId - body - htmlBody - postedAt - user { - _id - displayName - emailHash - slug - } - post { - _id - commentCount - commenters { - _id - displayName - emailHash - slug - } - } - userId - } -`; - const options = { collection: Comments, queryName: 'commentsListQuery', - fragment: PostsCommentsThread.fragment, + fragmentName: 'CommentsList', limit: 0, }; -registerComponent('PostsCommentsThread', PostsCommentsThread, withList(options), withCurrentUser); +registerComponent('PostsCommentsThread', PostsCommentsThread, [withList, options], withCurrentUser); diff --git a/packages/nova-base-components/lib/posts/PostsDailyList.jsx b/packages/nova-base-components/lib/posts/PostsDailyList.jsx index 0e17165798..30b6086870 100644 --- a/packages/nova-base-components/lib/posts/PostsDailyList.jsx +++ b/packages/nova-base-components/lib/posts/PostsDailyList.jsx @@ -111,8 +111,8 @@ PostsDailyList.defaultProps = { const options = { collection: Posts, queryName: 'postsDailyListQuery', - fragment: getRawComponent('PostsList').fragment, + fragmentName: 'PostsList', limit: 0, }; -registerComponent('PostsDailyList', PostsDailyList, withCurrentUser, withList(options)); \ No newline at end of file +registerComponent('PostsDailyList', PostsDailyList, withCurrentUser, [withList, options]); \ No newline at end of file diff --git a/packages/nova-base-components/lib/posts/PostsDay.jsx b/packages/nova-base-components/lib/posts/PostsDay.jsx index eb4732f2c3..54c09d42c9 100644 --- a/packages/nova-base-components/lib/posts/PostsDay.jsx +++ b/packages/nova-base-components/lib/posts/PostsDay.jsx @@ -1,11 +1,11 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; class PostsDay extends Component { render() { - const {date, networkStatus, posts} = this.props; + const {date, posts} = this.props; const noPosts = posts.length === 0; return ( diff --git a/packages/nova-base-components/lib/posts/PostsEditForm.jsx b/packages/nova-base-components/lib/posts/PostsEditForm.jsx index 75fb1735eb..84f69dd30f 100644 --- a/packages/nova-base-components/lib/posts/PostsEditForm.jsx +++ b/packages/nova-base-components/lib/posts/PostsEditForm.jsx @@ -1,9 +1,8 @@ -import { Components, registerComponent, getRawComponent } from 'meteor/nova:core'; +import { Components, registerComponent, getFragment, withMessages } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { intlShape } from 'react-intl'; import Posts from "meteor/nova:posts"; import { withRouter } from 'react-router' -import { ShowIf, withMessages } from 'meteor/nova:core'; class PostsEditForm extends Component { @@ -26,7 +25,7 @@ class PostsEditForm extends Component { { this.props.closeModal(); this.props.flash(this.context.intl.formatMessage({id: "posts.edit_success"}, {title: post.title}), 'success'); diff --git a/packages/nova-base-components/lib/posts/PostsHome.jsx b/packages/nova-base-components/lib/posts/PostsHome.jsx index 706716a778..5a29dfbc49 100644 --- a/packages/nova-base-components/lib/posts/PostsHome.jsx +++ b/packages/nova-base-components/lib/posts/PostsHome.jsx @@ -1,4 +1,4 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; const PostsHome = (props, context) => { diff --git a/packages/nova-base-components/lib/posts/PostsItem.jsx b/packages/nova-base-components/lib/posts/PostsItem.jsx index 4952e495f7..bde2375e14 100644 --- a/packages/nova-base-components/lib/posts/PostsItem.jsx +++ b/packages/nova-base-components/lib/posts/PostsItem.jsx @@ -1,10 +1,8 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, ModalTrigger } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { FormattedMessage, FormattedRelative } from 'react-intl'; -import { ModalTrigger } from "meteor/nova:core"; import { Link } from 'react-router'; import Posts from "meteor/nova:posts"; -// import { withCurrentUser } from 'meteor/nova:core'; class PostsItem extends Component { diff --git a/packages/nova-base-components/lib/posts/PostsList.jsx b/packages/nova-base-components/lib/posts/PostsList.jsx index d5406dcd1d..c137d492ff 100644 --- a/packages/nova-base-components/lib/posts/PostsList.jsx +++ b/packages/nova-base-components/lib/posts/PostsList.jsx @@ -1,13 +1,10 @@ -import { Components, getRawComponent, registerComponent } from 'meteor/nova:lib'; +import { Components, getRawComponent, registerComponent, withList, withCurrentUser } from 'meteor/nova:core'; import React from 'react'; -import { withList } from 'meteor/nova:core'; import Posts from 'meteor/nova:posts'; -import gql from 'graphql-tag'; -import { withCurrentUser } from 'meteor/nova:core'; const PostsList = (props) => { - const {results, terms, loading, count, totalCount, loadMore, showHeader = true, networkStatus, currentUser} = props; + const {results, loading, count, totalCount, loadMore, showHeader = true, networkStatus, currentUser} = props; const loadingMore = networkStatus === 2; @@ -59,57 +56,10 @@ PostsList.propTypes = { showHeader: React.PropTypes.bool, }; - -PostsList.fragment = gql` - fragment PostsItemFragment on Post { - _id - title - url - slug - thumbnailUrl - baseScore - postedAt - sticky - status - categories { - # ...minimumCategoryInfo - _id - name - slug - } - commentCount - commenters { - # ...avatarUserInfo - _id - displayName - emailHash - slug - } - upvoters { - _id - } - downvoters { - _id - } - upvotes # should be asked only for admins? - score # should be asked only for admins? - viewCount # should be asked only for admins? - clickCount # should be asked only for admins? - user { - # ...avatarUserInfo - _id - displayName - emailHash - slug - } - userId - } -`; - const options = { collection: Posts, queryName: 'postsListQuery', - fragment: PostsList.fragment, + fragmentName: 'PostsList', }; -registerComponent('PostsList', PostsList, withCurrentUser, withList(options)); +registerComponent('PostsList', PostsList, withCurrentUser, [withList, options]); diff --git a/packages/nova-base-components/lib/posts/PostsListHeader.jsx b/packages/nova-base-components/lib/posts/PostsListHeader.jsx index d5e29ac894..5e20b25714 100644 --- a/packages/nova-base-components/lib/posts/PostsListHeader.jsx +++ b/packages/nova-base-components/lib/posts/PostsListHeader.jsx @@ -1,6 +1,5 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React from 'react'; -import Categories from "meteor/nova:categories"; const PostsListHeader = () => { diff --git a/packages/nova-base-components/lib/posts/PostsLoadMore.jsx b/packages/nova-base-components/lib/posts/PostsLoadMore.jsx index a3778b13c9..64770627cc 100644 --- a/packages/nova-base-components/lib/posts/PostsLoadMore.jsx +++ b/packages/nova-base-components/lib/posts/PostsLoadMore.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React from 'react'; import { FormattedMessage } from 'react-intl'; diff --git a/packages/nova-base-components/lib/posts/PostsLoading.jsx b/packages/nova-base-components/lib/posts/PostsLoading.jsx index 883432c9eb..16af9ac97f 100644 --- a/packages/nova-base-components/lib/posts/PostsLoading.jsx +++ b/packages/nova-base-components/lib/posts/PostsLoading.jsx @@ -1,4 +1,4 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React from 'react'; const PostsLoading = props => { diff --git a/packages/nova-base-components/lib/posts/PostsNewButton.jsx b/packages/nova-base-components/lib/posts/PostsNewButton.jsx index 2826dac247..679f34fa8a 100644 --- a/packages/nova-base-components/lib/posts/PostsNewButton.jsx +++ b/packages/nova-base-components/lib/posts/PostsNewButton.jsx @@ -1,19 +1,16 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, withCurrentUser } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import { Button } from 'react-bootstrap'; -import { ModalTrigger } from "meteor/nova:core"; -import Posts from "meteor/nova:posts"; -import { withCurrentUser } from 'meteor/nova:core'; const PostsNewButton = (props, context) => { const size = props.currentUser ? "large" : "small"; const button = ; return ( - + - + ) } @@ -28,4 +25,4 @@ PostsNewButton.contextTypes = { intl: intlShape }; -registerComponent('PostsNewButton', PostsNewButton, withCurrentUser); \ No newline at end of file +registerComponent('PostsNewButton', PostsNewButton, withCurrentUser); diff --git a/packages/nova-base-components/lib/posts/PostsNewForm.jsx b/packages/nova-base-components/lib/posts/PostsNewForm.jsx index 7e24522eef..29fa7172a4 100644 --- a/packages/nova-base-components/lib/posts/PostsNewForm.jsx +++ b/packages/nova-base-components/lib/posts/PostsNewForm.jsx @@ -1,5 +1,4 @@ -import { Components, registerComponent, getRawComponent } from 'meteor/nova:core'; -import { ShowIf, withMessages } from 'meteor/nova:core'; +import { Components, registerComponent, getRawComponent, getFragment, withMessages } from 'meteor/nova:core'; import Posts from "meteor/nova:posts"; import React, { PropTypes, Component } from 'react'; import { intlShape } from 'react-intl'; @@ -14,7 +13,7 @@ const PostsNewForm = (props, context) => {
{ props.closeModal(); // props.router.push({pathname: Posts.getPageUrl(post)}); @@ -39,4 +38,4 @@ PostsNewForm.contextTypes = { PostsNewForm.displayName = "PostsNewForm"; -registerComponent('PostsNewForm', PostsNewForm, withRouter, withMessages); \ No newline at end of file +registerComponent('PostsNewForm', PostsNewForm, withRouter, withMessages); diff --git a/packages/nova-base-components/lib/posts/PostsNoMore.jsx b/packages/nova-base-components/lib/posts/PostsNoMore.jsx index 420a422fe8..80086c1db2 100644 --- a/packages/nova-base-components/lib/posts/PostsNoMore.jsx +++ b/packages/nova-base-components/lib/posts/PostsNoMore.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React from "react"; import { FormattedMessage } from "react-intl"; diff --git a/packages/nova-base-components/lib/posts/PostsNoResults.jsx b/packages/nova-base-components/lib/posts/PostsNoResults.jsx index f9e6c96e0f..d3ea47c935 100644 --- a/packages/nova-base-components/lib/posts/PostsNoResults.jsx +++ b/packages/nova-base-components/lib/posts/PostsNoResults.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React from 'react'; import { FormattedMessage } from "react-intl"; diff --git a/packages/nova-base-components/lib/posts/PostsPage.jsx b/packages/nova-base-components/lib/posts/PostsPage.jsx index d8777be149..fc1888697f 100644 --- a/packages/nova-base-components/lib/posts/PostsPage.jsx +++ b/packages/nova-base-components/lib/posts/PostsPage.jsx @@ -1,95 +1,104 @@ -import { Components, registerComponent, withDocument, withCurrentUser } from 'meteor/nova:core'; -import React from 'react'; +import { Components, registerComponent, withDocument, withCurrentUser, getActions, withMutation } from 'meteor/nova:core'; import Posts from 'meteor/nova:posts'; -import gql from 'graphql-tag'; - -const PostsPage = (props) => { - - if (props.loading) { - - return
- - } else { - - const post = props.document; - - const htmlBody = {__html: post.htmlBody}; - - return ( -
- +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +class PostsPage extends Component { + + render() { + if (this.props.loading) { + + return
+ + } else { + + const post = this.props.document; + + const htmlBody = {__html: post.htmlBody}; + + return ( +
+ + + + + {post.htmlBody ?
: null} + + + +
+ ); + + } + } + + // triggered after the component did mount on the client + async componentDidMount() { + try { + + // destructure the relevant props + const { + // from the parent component, used in withDocument, GraphQL HOC + documentId, + // from connect, Redux HOC + setViewed, + postsViewed, + // from withMutation, GraphQL HOC + increasePostViewCount, + } = this.props; + + // a post id has been found & it's has not been seen yet on this client session + if (documentId && !postsViewed.includes(documentId)) { - - - {post.htmlBody ?
: null} - - {/**/} - - - -
- ) + // trigger the asynchronous mutation with postId as an argument + await increasePostViewCount({postId: documentId}); + + // once the mutation is done, update the redux store + setViewed(documentId); + } + + } catch(error) { + console.log(error); // eslint-disable-line + } } -}; +} PostsPage.displayName = "PostsPage"; PostsPage.propTypes = { - document: React.PropTypes.object + documentId: PropTypes.string, + document: PropTypes.object, + postsViewed: PropTypes.array, + setViewed: PropTypes.func, + increasePostViewCount: PropTypes.func, } -PostsPage.fragment = gql` - fragment PostsSingleFragment on Post { - _id - title - url - body # extra - htmlBody # extra - slug - thumbnailUrl - baseScore - postedAt - sticky - status - categories { - # ...minimumCategoryInfo - _id - name - slug - } - commentCount - commenters { - # ...avatarUserInfo - _id - displayName - emailHash - slug - } - upvoters { - _id - } - downvoters { - _id - } - upvotes # should be asked only for admins? - score # should be asked only for admins? - viewCount # should be asked only for admins? - clickCount # should be asked only for admins? - user { - # ...avatarUserInfo - _id - displayName - emailHash - slug - } - userId - } -`; - -const options = { +const queryOptions = { collection: Posts, queryName: 'postsSingleQuery', - fragment: PostsPage.fragment, + fragmentName: 'PostsPage', +}; + +const mutationOptions = { + name: 'increasePostViewCount', + args: {postId: 'String'}, }; -registerComponent('PostsPage', PostsPage, withCurrentUser, withDocument(options)); +const mapStateToProps = state => ({ postsViewed: state.postsViewed }); +const mapDispatchToProps = dispatch => bindActionCreators(getActions().postsViewed, dispatch); + +registerComponent( + // component name used by Nova + 'PostsPage', + // React component + PostsPage, + // HOC to give access to the current user + withCurrentUser, + // HOC to load the data of the document, based on queryOptions & a documentId props + [withDocument, queryOptions], + // HOC to provide a single mutation, based on mutationOptions + withMutation(mutationOptions), + // HOC to give access to the redux store & related actions + connect(mapStateToProps, mapDispatchToProps) +); diff --git a/packages/nova-base-components/lib/posts/PostsSingle.jsx b/packages/nova-base-components/lib/posts/PostsSingle.jsx index d093b518c1..d9a5a03feb 100644 --- a/packages/nova-base-components/lib/posts/PostsSingle.jsx +++ b/packages/nova-base-components/lib/posts/PostsSingle.jsx @@ -1,6 +1,5 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React from 'react'; -import Posts from "meteor/nova:posts"; const PostsSingle = (props, context) => { return @@ -8,4 +7,4 @@ const PostsSingle = (props, context) => { PostsSingle.displayName = "PostsSingle"; -registerComponent('PostsSingle', PostsSingle); \ No newline at end of file +registerComponent('PostsSingle', PostsSingle); diff --git a/packages/nova-base-components/lib/posts/PostsStats.jsx b/packages/nova-base-components/lib/posts/PostsStats.jsx index 741fe1e2b4..c3ac83cdb0 100644 --- a/packages/nova-base-components/lib/posts/PostsStats.jsx +++ b/packages/nova-base-components/lib/posts/PostsStats.jsx @@ -1,4 +1,4 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React from 'react'; const PostsStats = ({post}) => { diff --git a/packages/nova-base-components/lib/posts/PostsThumbnail.jsx b/packages/nova-base-components/lib/posts/PostsThumbnail.jsx index 6591ba3d3d..2da6c9e65a 100644 --- a/packages/nova-base-components/lib/posts/PostsThumbnail.jsx +++ b/packages/nova-base-components/lib/posts/PostsThumbnail.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React from 'react'; import Posts from "meteor/nova:posts"; diff --git a/packages/nova-base-components/lib/posts/PostsViews.jsx b/packages/nova-base-components/lib/posts/PostsViews.jsx index aabc3a51bf..19ea73ae22 100644 --- a/packages/nova-base-components/lib/posts/PostsViews.jsx +++ b/packages/nova-base-components/lib/posts/PostsViews.jsx @@ -1,11 +1,10 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent, withCurrentUser } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import { DropdownButton, MenuItem } from 'react-bootstrap'; import { LinkContainer } from 'react-router-bootstrap'; import { withRouter } from 'react-router' import Users from 'meteor/nova:users'; -import { withCurrentUser } from 'meteor/nova:core'; const PostsViews = (props, context) => { diff --git a/packages/nova-base-components/lib/server.js b/packages/nova-base-components/lib/server.js index 87c7840147..aa7fcec8fd 100644 --- a/packages/nova-base-components/lib/server.js +++ b/packages/nova-base-components/lib/server.js @@ -1,3 +1,4 @@ +import './fragments.js'; import './components.js'; import './config.js'; import './routes.js'; diff --git a/packages/nova-base-components/lib/users/UsersAccount.jsx b/packages/nova-base-components/lib/users/UsersAccount.jsx index c23ebc1b03..e21dc79ed4 100644 --- a/packages/nova-base-components/lib/users/UsersAccount.jsx +++ b/packages/nova-base-components/lib/users/UsersAccount.jsx @@ -1,6 +1,5 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, withCurrentUser } from 'meteor/nova:core'; import React from 'react'; -import { withCurrentUser } from 'meteor/nova:core'; const UsersAccount = (props, context) => { // note: terms is as the same as a document-shape the SmartForm edit-mode expects to receive diff --git a/packages/nova-base-components/lib/users/UsersAccountForm.jsx b/packages/nova-base-components/lib/users/UsersAccountForm.jsx index 7147f4be76..7c83d89ca0 100644 --- a/packages/nova-base-components/lib/users/UsersAccountForm.jsx +++ b/packages/nova-base-components/lib/users/UsersAccountForm.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { Button, FormControl } from 'react-bootstrap'; import { Accounts } from 'meteor/std:accounts-ui'; diff --git a/packages/nova-base-components/lib/users/UsersAccountMenu.jsx b/packages/nova-base-components/lib/users/UsersAccountMenu.jsx index 3981f01ea6..050406abc6 100644 --- a/packages/nova-base-components/lib/users/UsersAccountMenu.jsx +++ b/packages/nova-base-components/lib/users/UsersAccountMenu.jsx @@ -1,4 +1,4 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { FormattedMessage } from 'react-intl'; import { Dropdown } from 'react-bootstrap'; diff --git a/packages/nova-base-components/lib/users/UsersAvatar.jsx b/packages/nova-base-components/lib/users/UsersAvatar.jsx index 685f0affb8..c7194d81e0 100644 --- a/packages/nova-base-components/lib/users/UsersAvatar.jsx +++ b/packages/nova-base-components/lib/users/UsersAvatar.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import Users from 'meteor/nova:users'; import { Link } from 'react-router'; diff --git a/packages/nova-base-components/lib/users/UsersEditForm.jsx b/packages/nova-base-components/lib/users/UsersEditForm.jsx index 41f8b2482b..bd3732edab 100644 --- a/packages/nova-base-components/lib/users/UsersEditForm.jsx +++ b/packages/nova-base-components/lib/users/UsersEditForm.jsx @@ -1,8 +1,7 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, withCurrentUser, withMessages } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import Users from 'meteor/nova:users'; -import { ShowIf, withCurrentUser, withMessages } from 'meteor/nova:core'; const UsersEditForm = (props, context) => { return ( diff --git a/packages/nova-base-components/lib/users/UsersMenu.jsx b/packages/nova-base-components/lib/users/UsersMenu.jsx index ffd91a470d..22ea54c23c 100644 --- a/packages/nova-base-components/lib/users/UsersMenu.jsx +++ b/packages/nova-base-components/lib/users/UsersMenu.jsx @@ -1,11 +1,10 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, withCurrentUser } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { FormattedMessage } from 'react-intl'; import { Meteor } from 'meteor/meteor'; import { Dropdown, MenuItem } from 'react-bootstrap'; import { LinkContainer } from 'react-router-bootstrap'; import Users from 'meteor/nova:users'; -import { withCurrentUser } from 'meteor/nova:core'; import { withApollo } from 'react-apollo'; class UsersMenu extends Component { diff --git a/packages/nova-base-components/lib/users/UsersName.jsx b/packages/nova-base-components/lib/users/UsersName.jsx index 43da7dfe1d..dd002370fa 100644 --- a/packages/nova-base-components/lib/users/UsersName.jsx +++ b/packages/nova-base-components/lib/users/UsersName.jsx @@ -1,4 +1,4 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import Users from 'meteor/nova:users'; import { Link } from 'react-router'; diff --git a/packages/nova-base-components/lib/users/UsersProfile.jsx b/packages/nova-base-components/lib/users/UsersProfile.jsx index fdb9c8de7e..a6c9833c44 100644 --- a/packages/nova-base-components/lib/users/UsersProfile.jsx +++ b/packages/nova-base-components/lib/users/UsersProfile.jsx @@ -1,10 +1,8 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent, withDocument, withCurrentUser } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { FormattedMessage } from 'react-intl'; import Users from 'meteor/nova:users'; import { Link } from 'react-router'; -import { ShowIf, withDocument, withCurrentUser } from 'meteor/nova:core'; -import gql from 'graphql-tag'; const UsersProfile = (props) => { if (props.loading) { @@ -42,53 +40,10 @@ UsersProfile.propTypes = { UsersProfile.displayName = "UsersProfile"; -UsersProfile.fragment = gql` - fragment usersProfileFragment on User { - _id - username - createdAt - isAdmin - bio - commentCount - displayName - downvotedComments { - itemId - power - votedAt - } - downvotedPosts { - itemId - power - votedAt - } - emailHash - groups - htmlBio - karma - newsletter_subscribeToNewsletter - notifications_users - notifications_posts - postCount - slug - twitterUsername - upvotedComments { - itemId - power - votedAt - } - upvotedPosts { - itemId - power - votedAt - } - website - } -`; - const options = { collection: Users, queryName: 'usersSingleQuery', - fragment: UsersProfile.fragment, + fragmentName: 'UsersProfile', }; -registerComponent('UsersProfile', UsersProfile, withCurrentUser, withDocument(options)); +registerComponent('UsersProfile', UsersProfile, withCurrentUser, [withDocument, options]); diff --git a/packages/nova-base-components/lib/users/UsersResetPassword.jsx b/packages/nova-base-components/lib/users/UsersResetPassword.jsx index a13874e303..020339c865 100644 --- a/packages/nova-base-components/lib/users/UsersResetPassword.jsx +++ b/packages/nova-base-components/lib/users/UsersResetPassword.jsx @@ -1,9 +1,8 @@ -import { registerComponent } from 'meteor/nova:lib'; +import { registerComponent, withCurrentUser } from 'meteor/nova:core'; import React, { Component } from 'react'; import { Accounts, STATES } from 'meteor/std:accounts-ui'; import { T9n } from 'meteor/softwarerero:accounts-t9n'; import { Link } from 'react-router'; -import { withCurrentUser } from 'meteor/nova:core'; class UsersResetPassword extends Component { componentDidMount() { diff --git a/packages/nova-base-components/lib/users/UsersSingle.jsx b/packages/nova-base-components/lib/users/UsersSingle.jsx index a014f35500..c536279e42 100644 --- a/packages/nova-base-components/lib/users/UsersSingle.jsx +++ b/packages/nova-base-components/lib/users/UsersSingle.jsx @@ -1,6 +1,5 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React from 'react'; -import Users from 'meteor/nova:users'; const UsersSingle = (props, context) => { return @@ -8,4 +7,4 @@ const UsersSingle = (props, context) => { UsersSingle.displayName = "UsersSingle"; -registerComponent('UsersSingle', UsersSingle); \ No newline at end of file +registerComponent('UsersSingle', UsersSingle); diff --git a/packages/nova-base-styles/lib/stylesheets/_categories.scss b/packages/nova-base-styles/lib/stylesheets/_categories.scss index e68d6200a7..ff4fe4a7ed 100644 --- a/packages/nova-base-styles/lib/stylesheets/_categories.scss +++ b/packages/nova-base-styles/lib/stylesheets/_categories.scss @@ -5,4 +5,23 @@ top: 4px; right: 5px; } +} + +.categories-new-button{ + text-align: center; + padding: $vmargin/2; + button{ + display: inline-block; + } +} + +.categories-node .categories-node{ + border-left: 10px $lightest-border solid; + padding-left: $hmargin; +} + +.categories-edit-form-admin{ + @include flex-center; + justify-content: space-between; + margin-bottom: $vmargin * 2; } \ No newline at end of file diff --git a/packages/nova-base-styles/lib/stylesheets/_comments.scss b/packages/nova-base-styles/lib/stylesheets/_comments.scss index aad78130bc..517c66e930 100644 --- a/packages/nova-base-styles/lib/stylesheets/_comments.scss +++ b/packages/nova-base-styles/lib/stylesheets/_comments.scss @@ -31,6 +31,11 @@ .comments-item-meta{ @include flex-center; margin-bottom: $vmargin; + + .comments-item-vote{ + margin-right: 10px; + } + .users-avatar{ margin-right: 5px; } diff --git a/packages/nova-base-styles/lib/stylesheets/bootstrap.css b/packages/nova-base-styles/lib/stylesheets/bootstrap.css index 79742bda39..5279924d5f 100644 --- a/packages/nova-base-styles/lib/stylesheets/bootstrap.css +++ b/packages/nova-base-styles/lib/stylesheets/bootstrap.css @@ -6387,4 +6387,4 @@ a.text-danger:focus, a.text-danger:hover { display: none !important; } } -/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file +/*# sourceMappingURL=bootstrap.css.map */ diff --git a/packages/nova-categories/lib/callbacks.js b/packages/nova-categories/lib/callbacks.js index fff8bb7e8e..2acf015e29 100644 --- a/packages/nova-categories/lib/callbacks.js +++ b/packages/nova-categories/lib/callbacks.js @@ -23,7 +23,7 @@ var checkCategories = function (post) { var categoryCount = Categories.find({_id: {$in: post.categories}}).count(); if (post.categories.length !== categoryCount) { - throw new Meteor.Error('invalid_category', 'invalid_category'); + throw new Error({id: 'categories.invalid'}); } }; diff --git a/packages/nova-categories/lib/client.js b/packages/nova-categories/lib/client.js index 9c0ae12430..1c346b5f88 100644 --- a/packages/nova-categories/lib/client.js +++ b/packages/nova-categories/lib/client.js @@ -1,3 +1,4 @@ -import Categories from './modules.js'; +import Categories, { getCategories, getCategoriesAsOptions } from './modules.js'; -export default Categories; \ No newline at end of file +export { getCategories, getCategoriesAsOptions }; +export default Categories; diff --git a/packages/nova-categories/lib/custom_fields.js b/packages/nova-categories/lib/custom_fields.js index ba0f7fb3a0..c11111e1ba 100644 --- a/packages/nova-categories/lib/custom_fields.js +++ b/packages/nova-categories/lib/custom_fields.js @@ -1,4 +1,5 @@ import Posts from "meteor/nova:posts"; +import { getCategoriesAsOptions } from './schema.js'; Posts.addField( { @@ -14,36 +15,8 @@ Posts.addField( noselect: true, type: "bootstrap-category", order: 50, - options: function (formProps) { - - // catch the ApolloClient from the form props - const {client} = formProps; - - // get the current data of the store - const apolloData = client.store.getState().apollo.data; - - // filter these data based on their typename: we are interested in the categories data - const categories = _.filter(apolloData, (object, key) => { - return object.__typename === 'Category' - }); - - // give the form component (here: checkboxgroup) exploitable data - const categoriesOptions = categories.map(function (category) { - return { - value: category._id, - label: category.name, - slug: category.slug, // note: it may be used to look up from prefilled props - }; - }); - - return categoriesOptions; - } + options: formProps => getCategoriesAsOptions(formProps.client), }, - // publish: true, - // join: { - // joinAs: "categoriesArray", - // collection: () => Categories - // }, resolveAs: 'categories: [Category]' } } diff --git a/packages/nova-categories/lib/helpers.js b/packages/nova-categories/lib/helpers.js index 35aee90b0c..aa19724350 100644 --- a/packages/nova-categories/lib/helpers.js +++ b/packages/nova-categories/lib/helpers.js @@ -60,6 +60,7 @@ Categories.getUrl = function (category, isAbsolute) { * @summary Get a category's counter name * @param {Object} category */ - Categories.getCounterName = function (category) { +Categories.getCounterName = function (category) { return category._id + "-postsCount"; - } +} + diff --git a/packages/nova-categories/lib/modules.js b/packages/nova-categories/lib/modules.js index 285ae1fa53..bf66d63a09 100644 --- a/packages/nova-categories/lib/modules.js +++ b/packages/nova-categories/lib/modules.js @@ -1,6 +1,6 @@ import Categories from './collection.js'; -import './schema.js'; +export { getCategories, getCategoriesAsOptions } from './schema.js'; import './helpers.js'; import './callbacks.js'; import './parameters.js'; @@ -9,4 +9,4 @@ import './permissions.js'; import './resolvers.js'; import './mutations.js'; -export default Categories; \ No newline at end of file +export default Categories; diff --git a/packages/nova-categories/lib/mutations.js b/packages/nova-categories/lib/mutations.js index 9eef79c2db..413dd3a529 100644 --- a/packages/nova-categories/lib/mutations.js +++ b/packages/nova-categories/lib/mutations.js @@ -2,7 +2,7 @@ import { newMutation, editMutation, removeMutation } from 'meteor/nova:core'; import Users from 'meteor/nova:users'; const performCheck = (mutation, user, document) => { - if (!mutation.check(user, document)) throw new Error(`Sorry, you don't have the rights to perform the mutation ${mutation.name} on document _id = ${document._id}`); + if (!mutation.check(user, document)) throw new Error(Utils.encodeIntlError({id: `app.mutation_not_allowed`, value: `"${mutation.name}" on _id "${document._id}"`})); }; const mutations = { @@ -85,4 +85,4 @@ const mutations = { }; -export default mutations; \ No newline at end of file +export default mutations; diff --git a/packages/nova-categories/lib/parameters.js b/packages/nova-categories/lib/parameters.js index 8985a9a25a..3bfa0e692f 100644 --- a/packages/nova-categories/lib/parameters.js +++ b/packages/nova-categories/lib/parameters.js @@ -1,18 +1,19 @@ -import Categories from "./collection.js"; +import Categories from './collection.js'; import { addCallback } from 'meteor/nova:core'; +import { getCategories } from './schema.js'; // Category Default Sorting by Ascending order (1, 2, 3..) -const CategoriesAscOrderSorting = (parameters, terms) => { +function CategoriesAscOrderSorting(parameters, terms) { parameters.options.sort = {order: 1}; return parameters; -}; +} addCallback('categories.parameters', CategoriesAscOrderSorting); // Category Posts Parameters // Add a "categories" property to terms which can be used to filter *all* existing Posts views. -const PostsCategoryParameter = (parameters, terms) => { +function PostsCategoryParameter(parameters, terms, apolloClient) { const cat = terms.cat || terms["cat[]"]; @@ -21,15 +22,18 @@ const PostsCategoryParameter = (parameters, terms) => { let categoriesIds = []; let selector = {}; + let slugs; if (typeof cat === "string") { // cat is a string selector = {slug: cat}; + slugs = [cat]; } else if (Array.isArray(cat)) { // cat is an array selector = {slug: {$in: cat}}; + slugs = cat; } // get all categories passed in terms - let categories = Categories.find(selector).fetch(); + const categories = Meteor.isClient ? _.filter(getCategories(apolloClient), category => _.contains(slugs, category.slug) ) : Categories.find(selector).fetch(); // for each category, add its ID and the IDs of its children to categoriesId array categories.forEach(function (category) { @@ -37,7 +41,7 @@ const PostsCategoryParameter = (parameters, terms) => { categoriesIds = categoriesIds.concat(_.pluck(Categories.getChildren(category), "_id")); }); - parameters.selector.categories = {$in: categoriesIds}; + parameters.selector = Meteor.isClient ? {'categories._id': {$in: categoriesIds}} : {categories: {$in: categoriesIds}}; } return parameters; diff --git a/packages/nova-categories/lib/schema.js b/packages/nova-categories/lib/schema.js index bafc6d85de..ffad50d91b 100644 --- a/packages/nova-categories/lib/schema.js +++ b/packages/nova-categories/lib/schema.js @@ -1,3 +1,28 @@ + +export function getCategories (apolloClient) { + + // get the current data of the store + const apolloData = apolloClient.store.getState().apollo.data; + + // filter these data based on their typename: we are interested in the categories data + const categories = _.filter(apolloData, (object, key) => { + return object.__typename === 'Category' + }); + + return categories; +} + +export function getCategoriesAsOptions (apolloClient) { + // give the form component (here: checkboxgroup) exploitable data + return getCategories(apolloClient).map(function (category) { + return { + value: category._id, + label: category.name, + // slug: category.slug, // note: it may be used to look up from prefilled props + }; + }); +} + // category schema const schema = { _id: { @@ -48,27 +73,19 @@ const schema = { editableBy: ['members'], publish: true }, - // parentId: { - // type: String, - // optional: true, - // viewableBy: ['guests'], - // insertableBy: ['members'], - // editableBy: ['members'], - // publish: true, - // resolveAs: 'parent: Category', - // form: { - // options: function () { - // // todo: get the collection from the options in form - // var categories = Categories.find().map(function (category) { - // return { - // value: category._id, - // label: category.name - // }; - // }); - // return categories; - // } - // } - // } + parentId: { + type: String, + optional: true, + control: "select", + viewableBy: ['guests'], + insertableBy: ['members'], + editableBy: ['members'], + publish: true, + resolveAs: 'parent: Category', + form: { + options: formProps => getCategoriesAsOptions(formProps.client) + } + } }; export default schema; diff --git a/packages/nova-categories/lib/server.js b/packages/nova-categories/lib/server.js index b556e2780a..274bc143ec 100644 --- a/packages/nova-categories/lib/server.js +++ b/packages/nova-categories/lib/server.js @@ -1,5 +1,6 @@ -import Categories from './modules.js'; +import Categories, { getCategories, getCategoriesAsOptions } from './modules.js'; import './server/load_categories.js'; -export default Categories; \ No newline at end of file +export { getCategories, getCategoriesAsOptions }; +export default Categories; diff --git a/packages/nova-categories/lib/server/publications.js b/packages/nova-categories/lib/server/publications.js deleted file mode 100644 index b3b6ed3084..0000000000 --- a/packages/nova-categories/lib/server/publications.js +++ /dev/null @@ -1,24 +0,0 @@ -// import Posts from "meteor/nova:posts"; -// import Users from 'meteor/nova:users'; -// import Categories from "../collection.js"; - -// Meteor.publish('categories', function() { - -// const currentUser = this.userId && Users.findOne(this.userId); - -// if(Users.canDo(currentUser, "posts.view.approved.all")){ - -// var categories = Categories.find({}, {fields: Categories.publishedFields.list}); -// var publication = this; - -// categories.forEach(function (category) { -// var childrenCategories = category.getChildren(); -// var categoryIds = [category._id].concat(_.pluck(childrenCategories, "_id")); -// var cursor = Posts.find({$and: [{categories: {$in: categoryIds}}, {status: Posts.config.STATUS_APPROVED}]}); -// // Counts.publish(publication, category.getCounterName(), cursor, { noReady: true }); -// }); - -// return categories; -// } -// return []; -// }); diff --git a/packages/nova-comments/lib/callbacks/callbacks_comments_edit.js b/packages/nova-comments/lib/callbacks/callbacks_comments_edit.js index a7c32d9a0b..291c9e1bc7 100644 --- a/packages/nova-comments/lib/callbacks/callbacks_comments_edit.js +++ b/packages/nova-comments/lib/callbacks/callbacks_comments_edit.js @@ -1,30 +1,6 @@ import marked from 'marked'; -import Posts from "meteor/nova:posts"; -import Users from 'meteor/nova:users'; import { addCallback, Utils } from 'meteor/nova:core'; -// ------------------------------------- comments.edit.validate -------------------------------- // - -function CommentsEditSubmittedPropertiesCheck (modifier, comment, user) { - const schema = Posts.simpleSchema()._schema; - // go over each field and throw an error if it's not editable - // loop over each operation ($set, $unset, etc.) - _.each(modifier, function (operation) { - // loop over each property being operated on - _.keys(operation).forEach(function (fieldName) { - - var field = schema[fieldName]; - if (!Users.canEditField(user, field, comment)) { - throw new Meteor.Error("disallowed_property", 'disallowed_property_detected' + ": " + fieldName); - } - - }); - }); - return modifier; -} -addCallback("comments.edit.validate", CommentsEditSubmittedPropertiesCheck); - - // ------------------------------------- comments.edit.sync -------------------------------- // function CommentsEditGenerateHTMLBody (modifier, comment, user) { diff --git a/packages/nova-comments/lib/callbacks/callbacks_comments_new.js b/packages/nova-comments/lib/callbacks/callbacks_comments_new.js index 025e13a347..7c6b0b2e17 100644 --- a/packages/nova-comments/lib/callbacks/callbacks_comments_new.js +++ b/packages/nova-comments/lib/callbacks/callbacks_comments_new.js @@ -7,84 +7,22 @@ import { addCallback, Utils, getSetting } from 'meteor/nova:core'; // ------------------------------------- comments.new.validate -------------------------------- // -// function CommentsNewUserCheck (comment, user) { -// // check that user can post -// if (!user || !Users.canDo(user, "comments.new")) -// throw new Meteor.Error(601, 'you_need_to_login_or_be_invited_to_post_new_comments'); -// return comment; -// } -// addCallback("comments.new.sync", CommentsNewUserCheck); - function CommentsNewRateLimit (comment, user) { if (!Users.isAdmin(user)) { const timeSinceLastComment = Users.timeSinceLast(user, Comments); const commentInterval = Math.abs(parseInt(getSetting('commentInterval',15))); + // check that user waits more than 15 seconds between comments if((timeSinceLastComment < commentInterval)) { - throw new Meteor.Error("CommentsNewRateLimit", "comments.rate_limit_error", commentInterval-timeSinceLastComment); + throw new Error(Utils.encodeIntlError({id: "comments.rate_limit_error", value: commentInterval-timeSinceLastComment})); } } return comment; } addCallback("comments.new.validate", CommentsNewRateLimit); -// function CommentsNewSubmittedPropertiesCheck (comment, user) { -// // admin-only properties -// // userId -// const schema = Comments.simpleSchema()._schema; - -// // clear restricted properties -// _.keys(comment).forEach(function (fieldName) { - -// // make an exception for postId, which should be setable but not modifiable -// if (fieldName === "postId") { -// // ok -// } else { -// var field = schema[fieldName]; -// if (!Users.canInsertField (user, field)) { -// throw new Meteor.Error("disallowed_property", 'disallowed_property_detected' + ": " + fieldName); -// } -// } - -// }); - -// // if no userId has been set, default to current user id -// if (!comment.userId) { -// comment.userId = user._id; -// } -// return comment; -// } -// addCallback("comments.new.validate", CommentsNewSubmittedPropertiesCheck); - // ------------------------------------- comments.new.sync -------------------------------- // -/** - * @summary Check for required properties - */ -// function CommentsNewRequiredPropertiesCheck (comment, user) { - -// var userId = comment.userId; // at this stage, a userId is expected - -// // Don't allow empty comments -// if (!comment.body) -// throw new Meteor.Error(704, 'your_comment_is_empty'); - -// var defaultProperties = { -// createdAt: new Date(), -// postedAt: new Date(), -// upvotes: 0, -// downvotes: 0, -// baseScore: 0, -// score: 0, -// author: Users.getDisplayNameById(userId) -// }; - -// comment = _.extend(defaultProperties, comment); - -// return comment; -// } -// addCallback("comments.new.sync", CommentsNewRequiredPropertiesCheck); - function CommentsNewGenerateHTMLBody (comment, user) { comment.htmlBody = Utils.sanitize(marked(comment.body)); return comment; diff --git a/packages/nova-comments/lib/callbacks/callbacks_comments_remove.js b/packages/nova-comments/lib/callbacks/callbacks_comments_remove.js index 0b07ca98cb..6d4cf58e3e 100644 --- a/packages/nova-comments/lib/callbacks/callbacks_comments_remove.js +++ b/packages/nova-comments/lib/callbacks/callbacks_comments_remove.js @@ -1,8 +1,7 @@ -import { removeMutation } from 'meteor/nova:core'; +import { removeMutation, addCallback } from 'meteor/nova:core'; import Posts from "meteor/nova:posts"; import Comments from '../collection.js'; import Users from 'meteor/nova:users'; -import { addCallback } from 'meteor/nova:core'; const CommentsRemovePostCommenters = (comment, currentUser) => { const { userId, postId } = comment; @@ -45,4 +44,4 @@ const CommentsRemoveChildrenComments = (comment, currentUser) => { return comment; }; -addCallback("comments.remove.async", CommentsRemoveChildrenComments); \ No newline at end of file +addCallback("comments.remove.async", CommentsRemoveChildrenComments); diff --git a/packages/nova-comments/lib/callbacks/callbacks_other.js b/packages/nova-comments/lib/callbacks/callbacks_other.js index ae21f57c20..9184f07198 100644 --- a/packages/nova-comments/lib/callbacks/callbacks_other.js +++ b/packages/nova-comments/lib/callbacks/callbacks_other.js @@ -3,7 +3,7 @@ import { addCallback } from 'meteor/nova:core'; function UsersRemoveDeleteComments (user, options) { if (options.deleteComments) { - var deletedComments = Comments.remove({userId: userId}); + Comments.remove({userId: user._id}); } else { // not sure if anything should be done in that scenario yet // Comments.update({userId: userId}, {$set: {author: "\[deleted\]"}}, {multi: true}); diff --git a/packages/nova-comments/lib/helpers.js b/packages/nova-comments/lib/helpers.js index fe5ae8b719..0012031dd0 100644 --- a/packages/nova-comments/lib/helpers.js +++ b/packages/nova-comments/lib/helpers.js @@ -1,4 +1,3 @@ -// import Telescope from 'meteor/nova:lib'; import Comments from './collection.js'; import Posts from 'meteor/nova:posts'; import Users from 'meteor/nova:users'; diff --git a/packages/nova-comments/lib/mutations.js b/packages/nova-comments/lib/mutations.js index 5a3a06625e..230d76a5dd 100644 --- a/packages/nova-comments/lib/mutations.js +++ b/packages/nova-comments/lib/mutations.js @@ -1,8 +1,8 @@ -import { newMutation, editMutation, removeMutation } from 'meteor/nova:core'; +import { newMutation, editMutation, removeMutation, Utils } from 'meteor/nova:core'; import Users from 'meteor/nova:users'; const performCheck = (mutation, user, document) => { - if (!mutation.check(user, document)) throw new Error(`Sorry, you don't have the rights to perform the mutation ${mutation.name} on document _id = ${document._id}`); + if (!mutation.check(user, document)) throw new Error(Utils.encodeIntlError({id: `app.mutation_not_allowed`, value: `"${mutation.name}" on _id "${document._id}"`})); }; const mutations = { @@ -85,4 +85,4 @@ const mutations = { }; -export default mutations; \ No newline at end of file +export default mutations; diff --git a/packages/nova-comments/lib/published_fields.js b/packages/nova-comments/lib/published_fields.js deleted file mode 100644 index c4064cf053..0000000000 --- a/packages/nova-comments/lib/published_fields.js +++ /dev/null @@ -1,29 +0,0 @@ -import Comments from './collection.js'; -import PublicationsUtils from 'meteor/utilities:smart-publications'; -// import Posts from "meteor/nova:posts"; - -Comments.publishedFields = {}; - -/** - * @summary Specify which fields should be published by the posts.list publication - * @array Posts.publishedFields.list - */ -Comments.publishedFields.list = PublicationsUtils.arrayToFields([ - "_id", - "parentCommentId", - "topLevelCommentId", - "postedAt", - "body", - "htmlBody", - "author", - "inactive", - "postId", - "userId", - "isDeleted" -]); - -/** - * @summary Specify which fields should be published by the posts.single publication - * @array Posts.publishedFields.single - */ -Comments.publishedFields.single = PublicationsUtils.arrayToFields(Comments.getPublishedFields()); diff --git a/packages/nova-comments/lib/server/publications.js b/packages/nova-comments/lib/server/publications.js deleted file mode 100644 index bf333eb164..0000000000 --- a/packages/nova-comments/lib/server/publications.js +++ /dev/null @@ -1,103 +0,0 @@ -import Posts from "meteor/nova:posts"; -import Users from 'meteor/nova:users'; -import Comments from '../collection.js'; - -Comments._ensureIndex({postId: 1}); -Comments._ensureIndex({parentCommentId: 1}); - -/** - * @summary Publish a list of comments, along with the posts and users corresponding to these comments - * @param {Object} terms - */ -// Meteor.publish('comments.list', function (terms) { -// -// const currentUser = this.userId && Users.findOne(this.userId); -// -// terms.currentUserId = this.userId; // add currentUserId to terms -// const {selector, options} = Comments.parameters.get(terms); -// -// // commenting this because of FR-SSR issue -// // Counts.publish(this, 'comments.list', Comments.find(selector, options)); -// -// options.fields = Comments.publishedFields.list; -// -// const comments = Comments.find(selector, options); -// const posts = Posts.find({_id: {$in: _.pluck(comments.fetch(), 'postId')}}, {fields: Posts.publishedFields.list}); -// const users = Users.find({_id: {$in: _.pluck(comments.fetch(), 'userId')}}, {fields: Users.publishedFields.list}); -// -// return Users.canDo(currentUser, "comments.view.all") ? [comments, posts, users] : []; -// -// }); - - - - - - -// /** -// * Publish a single comment, along with all relevant users -// * @param {Object} terms -// */ -// Meteor.publish('comments.single', function(terms) { - -// check(terms, {_id: String}); - -// - -// let commentIds = [terms._id]; -// const childCommentIds = _.pluck(Comments.find({parentCommentId: terms._id}, {fields: {_id: 1}}).fetch(), '_id'); -// commentIds = commentIds.concat(childCommentIds); - -// return Users.canView(currentUser) ? Comments.find({_id: {$in: commentIds}}, {sort: {score: -1, postedAt: -1}}) : []; - -// }); - - - - -// // Publish the post related to the current comment - -// Meteor.publish('commentPost', function(commentId) { - -// check(commentId, String); - -// - -// if(Users.canViewById(this.userId)){ -// var comment = Comments.findOne(commentId); -// return Posts.find({_id: comment && comment.postId}); -// } -// return []; -// }); - -// // Publish author of the current comment, and author of the post related to the current comment - -// Meteor.publish('commentUsers', function(commentId) { - -// check(commentId, String); - -// - -// var userIds = []; - -// if(Users.canViewById(this.userId)){ - -// var comment = Comments.findOne(commentId); - -// if (!!comment) { -// userIds.push(comment.userId); - -// var post = Posts.findOne(comment.postId); -// if (!!post) { -// userIds.push(post.userId); -// } - -// return Users.find({_id: {$in: userIds}}, {fields: Users.pubsub.publicProperties}); - -// } - -// } - -// return []; - -// }); diff --git a/packages/nova-core/lib/callbacks.js b/packages/nova-core/lib/callbacks.js new file mode 100644 index 0000000000..ced81ec459 --- /dev/null +++ b/packages/nova-core/lib/callbacks.js @@ -0,0 +1,21 @@ +import { addCallback, getActions } from 'meteor/nova:lib'; + +/* + + Core callbacks + +*/ + +/** + * @summary Clear flash messages marked as seen when the route changes + * @param {Object} Item needed by `runCallbacks` to iterate on, unused here + * @param {Object} Redux store reference instantiated on the current connected client + * @param {Object} Apollo Client reference instantiated on the current connected client + */ +function RouterClearMessages(unusedItem, store, apolloClient) { + store.dispatch(getActions().messages.clearSeen()); + + return unusedItem; +} + +addCallback('router.onUpdate', RouterClearMessages); diff --git a/packages/nova-core/lib/client.js b/packages/nova-core/lib/client.js index 32600f8216..4b291b1d04 100644 --- a/packages/nova-core/lib/client.js +++ b/packages/nova-core/lib/client.js @@ -1 +1,3 @@ -export * from './modules.js'; \ No newline at end of file +export * from './modules.js'; + +export { store } from 'meteor/nova:lib'; diff --git a/packages/nova-core/lib/components/ContextPasser.jsx b/packages/nova-core/lib/components/ContextPasser.jsx deleted file mode 100644 index 94549ad5f7..0000000000 --- a/packages/nova-core/lib/components/ContextPasser.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import { registerComponent } from 'meteor/nova:lib'; -import React, { PropTypes, Component } from 'react'; - -class ContextPasser extends Component { - - getChildContext() { - return { - closeCallback: this.props.closeCallback, - events: this.props.events, - messages: this.props.messages, - }; - } - - render() { - return this.props.children; - } -} - -ContextPasser.propTypes = { - closeCallback: React.PropTypes.func, - events: React.PropTypes.object, - messages: React.PropTypes.object, -}; - -ContextPasser.childContextTypes = { - closeCallback: React.PropTypes.func, - events: React.PropTypes.object, - messages: React.PropTypes.object, -}; - -registerComponent('ContextPasser', ContextPasser); - -export default ContextPasser; -module.exports = ContextPasser; \ No newline at end of file diff --git a/packages/nova-core/lib/components/Layout.jsx b/packages/nova-core/lib/components/Layout.jsx deleted file mode 100644 index b705d4443e..0000000000 --- a/packages/nova-core/lib/components/Layout.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { registerComponent } from 'meteor/nova:lib'; -import React from 'react'; - -const Layout = props => { - return ( -
{props.children}
- ); -} - -Layout.displayName = "Layout"; - -registerComponent('Layout', Layout); - -export default Layout; \ No newline at end of file diff --git a/packages/nova-core/lib/components/ModalTrigger.jsx b/packages/nova-core/lib/components/ModalTrigger.jsx index f29c3d5806..d0c57fd9a9 100644 --- a/packages/nova-core/lib/components/ModalTrigger.jsx +++ b/packages/nova-core/lib/components/ModalTrigger.jsx @@ -1,6 +1,5 @@ import { registerComponent } from 'meteor/nova:lib'; import React, { PropTypes, Component } from 'react'; -import ContextPasser from './ContextPasser.jsx' import { Modal } from 'react-bootstrap'; class ModalTrigger extends Component { @@ -58,4 +57,6 @@ ModalTrigger.defaultProps = { size: "large" } -export default registerComponent('ModalTrigger', ModalTrigger); +registerComponent('ModalTrigger', ModalTrigger); + +export default ModalTrigger; diff --git a/packages/nova-core/lib/components/ShowIf.jsx b/packages/nova-core/lib/components/ShowIf.jsx index 0e9a46864b..15ff63c4da 100644 --- a/packages/nova-core/lib/components/ShowIf.jsx +++ b/packages/nova-core/lib/components/ShowIf.jsx @@ -16,4 +16,6 @@ ShowIf.propTypes = { ShowIf.displayName = "ShowIf"; -export default registerComponent('ShowIf', ShowIf, withCurrentUser); \ No newline at end of file +registerComponent('ShowIf', ShowIf, withCurrentUser); + +export default withCurrentUser(ShowIf); \ No newline at end of file diff --git a/packages/nova-core/lib/containers/withApp.js b/packages/nova-core/lib/containers/withApp.js deleted file mode 100644 index ab188094d6..0000000000 --- a/packages/nova-core/lib/containers/withApp.js +++ /dev/null @@ -1,58 +0,0 @@ -// import Users from 'meteor/nova:users'; -// import React from 'react'; -// import { graphql } from 'react-apollo'; -// import gql from 'graphql-tag'; -// import hoistStatics from 'hoist-non-react-statics'; -// import { Utils } from 'meteor/nova:lib'; -// -// const withApp = WrappedComponent => { -// -// class WithApp extends React.Component { -// constructor(...args) { -// super(...args); -// -// this.preloadedFields = ['_id']; -// } -// -// componentWillMount() { -// this.preloadedFields = _.compact(_.map(Users.simpleSchema()._schema, (field, fieldName) => { -// return field.preload ? fieldName : undefined; -// })); -// } -// -// render() { -// -// const ComponentWithData = graphql( -// gql`query getCurrentUser { -// currentUser { -// ${this.preloadedFields.join('\n')} -// } -// } -// `, { -// options(ownProps) { -// return { -// variables: {}, -// // pollInterval: 20000, -// }; -// }, -// props(props) { -// const {data: {loading, currentUser}} = props; -// return { -// loading, -// currentUser, -// }; -// }, -// } -// )(WrappedComponent); -// -// return -// } -// } -// -// WithApp.displayName = `withApp(${Utils.getComponentDisplayName(WrappedComponent)})` -// WithApp.WrappedComponent = WrappedComponent -// -// return hoistStatics(WithApp, WrappedComponent); -// }; -// -// export default withApp; diff --git a/packages/nova-core/lib/containers/withDocument.js b/packages/nova-core/lib/containers/withDocument.js index 2ab0df83e8..cb97c28f9d 100644 --- a/packages/nova-core/lib/containers/withDocument.js +++ b/packages/nova-core/lib/containers/withDocument.js @@ -1,12 +1,13 @@ import React, { PropTypes, Component } from 'react'; import { graphql } from 'react-apollo'; import gql from 'graphql-tag'; -import { Utils } from 'meteor/nova:core'; +import { Utils, getFragment, getFragmentName } from 'meteor/nova:core'; export default function withDocument (options) { - const { queryName, collection, fragment, pollInterval = 20000 } = options, - fragmentName = fragment.definitions[0].name.value, + const { queryName, collection, pollInterval = 20000 } = options, + fragment = options.fragment || getFragment(options.fragmentName), + fragmentName = getFragmentName(fragment), singleResolverName = collection.options.resolvers.single.name; return graphql(gql` diff --git a/packages/nova-core/lib/containers/withList.js b/packages/nova-core/lib/containers/withList.js index 1470c001e3..fb1ece91d6 100644 --- a/packages/nova-core/lib/containers/withList.js +++ b/packages/nova-core/lib/containers/withList.js @@ -9,6 +9,7 @@ Options: - queryName: an arbitrary name for the query - collection: the collection to fetch the documents from - fragment: the fragment that defines which properties to fetch + - fragmentName: the name of the fragment, passed to getFragment - limit: the number of documents to show initially - pollInterval: how often the data should be updated, in ms (set to 0 to disable polling) @@ -36,19 +37,25 @@ import React, { PropTypes, Component } from 'react'; import { graphql } from 'react-apollo'; import gql from 'graphql-tag'; import update from 'immutability-helper'; -import { getSetting, Utils } from 'meteor/nova:core'; +import { getSetting, getFragment, getFragmentName } from 'meteor/nova:core'; import Mingo from 'mingo'; import { compose, withState } from 'recompose'; +import { withApollo } from 'react-apollo'; const withList = (options) => { - const { queryName, collection, fragment, limit = getSetting('postsPerPage', 10), pollInterval = 20000 } = options, - fragmentName = fragment.definitions[0].name.value, + const { queryName, collection, limit = getSetting('postsPerPage', 10), pollInterval = 20000 } = options, + fragment = options.fragment || getFragment(options.fragmentName), + fragmentName = getFragmentName(fragment), listResolverName = collection.options.resolvers.list.name, totalResolverName = collection.options.resolvers.total.name; + return compose( + // wrap component with Apollo HoC to give it access to the store + withApollo, + // wrap component with HoC that manages the terms object via its state withState('paginationTerms', 'setPaginationTerms', props => { @@ -79,7 +86,7 @@ const withList = (options) => { alias: 'withList', // graphql query options - options({terms, paginationTerms}) { + options({terms, paginationTerms, client: apolloClient}) { const mergedTerms = {...terms, ...paginationTerms}; return { variables: { @@ -90,7 +97,7 @@ const withList = (options) => { reducer: (previousResults, action) => { // see queryReducer function defined below - return queryReducer(previousResults, action, collection, mergedTerms, listResolverName, totalResolverName, queryName); + return queryReducer(previousResults, action, collection, mergedTerms, listResolverName, totalResolverName, queryName, apolloClient); }, }; @@ -157,7 +164,7 @@ const withList = (options) => { // define query reducer separately -const queryReducer = (previousResults, action, collection, mergedTerms, listResolverName, totalResolverName, queryName) => { +const queryReducer = (previousResults, action, collection, mergedTerms, listResolverName, totalResolverName, queryName, apolloClient) => { const newMutationName = `${collection._name}New`; const editMutationName = `${collection._name}Edit`; @@ -166,7 +173,7 @@ const queryReducer = (previousResults, action, collection, mergedTerms, listReso let newResults = previousResults; // get mongo selector and options objects based on current terms - const { selector, options } = collection.getParameters(mergedTerms); + const { selector, options } = collection.getParameters(mergedTerms, apolloClient); const mingoQuery = Mingo.Query(selector); // function to remove a document from a results object, used by edit and remove cases below diff --git a/packages/nova-core/lib/containers/withMessages.js b/packages/nova-core/lib/containers/withMessages.js index 0e9315ca93..9e46bfa5d8 100644 --- a/packages/nova-core/lib/containers/withMessages.js +++ b/packages/nova-core/lib/containers/withMessages.js @@ -4,11 +4,16 @@ HoC that provides access to flash messages stored in Redux state and actions to */ -import { Actions, addAction, addReducer } from 'meteor/nova:lib'; +import { getActions, addAction, addReducer } from 'meteor/nova:lib'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -// register messages actions +/* + + Messages actions + +*/ + addAction({ messages: { flash(content, flashType) { @@ -38,13 +43,19 @@ addAction({ } }); -// register messages reducer + +/* + + Messages reducers + +*/ + addReducer({ messages: (state = [], action) => { // default values const flashType = typeof action.flashType === 'undefined' ? 'error' : action.flashType; - const currentMsg = typeof action.i === 'undefined' ? {} : state[action.i]; - + const currentMsg = typeof action.i === 'undefined' ? {} : state[action.i]; + switch(action.type) { case 'FLASH': return [ @@ -57,7 +68,7 @@ addReducer({ { ...currentMsg, seen: true }, ...state.slice(action.i + 1), ]; - case 'CLEAR': + case 'CLEAR': return [ ...state.slice(0, action.i), { ...currentMsg, show: false }, @@ -71,8 +82,14 @@ addReducer({ }, }); +/* + + withMessages HOC + +*/ + const mapStateToProps = state => ({ messages: state.messages, }); -const mapDispatchToProps = dispatch => bindActionCreators(Actions.messages, dispatch); +const mapDispatchToProps = dispatch => bindActionCreators(getActions().messages, dispatch); const withMessages = component => connect(mapStateToProps, mapDispatchToProps)(component); diff --git a/packages/nova-core/lib/modules.js b/packages/nova-core/lib/modules.js index 035918710a..7b8cd7d12c 100644 --- a/packages/nova-core/lib/modules.js +++ b/packages/nova-core/lib/modules.js @@ -1,13 +1,13 @@ // import and re-export -export { Components, registerComponent, replaceComponent, getRawComponent, getComponent, copyHoCs, populateComponentsApp, createCollection, Callbacks, addCallback, removeCallback, runCallbacks, runCallbacksAsync, GraphQLSchema, Routes, addRoute, getRoute, populateRoutesApp, Utils, getSetting, Strings, addStrings, configureStore, Actions, addAction, Reducers, addReducer, Middleware, addMiddleware, Headtags } from 'meteor/nova:lib'; +export { Components, registerComponent, replaceComponent, getRawComponent, getComponent, copyHoCs, populateComponentsApp, createCollection, Callbacks, addCallback, removeCallback, runCallbacks, runCallbacksAsync, GraphQLSchema, Routes, addRoute, getRoute, populateRoutesApp, Utils, getSetting, Strings, addStrings, configureStore, getActions, addAction, getReducers, addReducer, getMiddlewares, addMiddleware, Headtags, Fragments, registerFragment, getFragment, getFragmentName, extendFragment } from 'meteor/nova:lib'; + +import './callbacks.js'; export { default as App } from "./components/App.jsx"; -export { default as Layout } from "./components/Layout.jsx"; export { default as Icon } from "./components/Icon.jsx"; export { default as Loading } from "./components/Loading.jsx"; export { default as ShowIf } from "./components/ShowIf.jsx"; export { default as ModalTrigger } from './components/ModalTrigger.jsx'; -export { default as ContextPasser } from './components/ContextPasser.jsx'; export { default as withMessages } from "./containers/withMessages.js"; export { default as withList } from './containers/withList.js'; export { default as withDocument } from './containers/withDocument.js'; diff --git a/packages/nova-debug/lib/components/Emails.jsx b/packages/nova-debug/lib/components/Emails.jsx index 329d8ea1b5..94a7b0fa9a 100644 --- a/packages/nova-debug/lib/components/Emails.jsx +++ b/packages/nova-debug/lib/components/Emails.jsx @@ -1,4 +1,4 @@ -import { Components, registerComponent } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import { Button } from 'react-bootstrap'; import NovaEmail from 'meteor/nova:email'; diff --git a/packages/nova-debug/lib/globals.js b/packages/nova-debug/lib/globals.js index 29ea6b18ce..8cccc83cf3 100644 --- a/packages/nova-debug/lib/globals.js +++ b/packages/nova-debug/lib/globals.js @@ -1,12 +1,10 @@ /* eslint-disable no-undef */ -import TelescopeImport from 'meteor/nova:lib'; import PostsImport from "meteor/nova:posts"; import CommentsImport from "meteor/nova:comments"; import UsersImport from "meteor/nova:users"; import CategoriesImport from "meteor/nova:categories"; -Telescope = TelescopeImport; Posts = PostsImport; Comments = CommentsImport; Users = UsersImport; diff --git a/packages/nova-debug/lib/server/methods.js b/packages/nova-debug/lib/server/methods.js index cbd7e1d507..700b847be2 100644 --- a/packages/nova-debug/lib/server/methods.js +++ b/packages/nova-debug/lib/server/methods.js @@ -1,6 +1,6 @@ import NovaEmail from 'meteor/nova:email'; import Users from 'meteor/nova:users'; -import { getSetting } from 'meteor/nova:core'; +import { getSetting, Utils } from 'meteor/nova:core'; Meteor.methods({ "email.test": function (emailName) { @@ -37,7 +37,7 @@ Meteor.methods({ return subject; } else { - throw new Meteor.Error("must_be_admin", "You must be an admin to send test emails"); + throw new Error(Utils.encodeIntlError({id: "app.noPermission"})); } } }); diff --git a/packages/nova-email/lib/server/email.js b/packages/nova-email/lib/server/email.js index b7ae0f12af..856af0a57b 100644 --- a/packages/nova-email/lib/server/email.js +++ b/packages/nova-email/lib/server/email.js @@ -2,7 +2,7 @@ import NovaEmail from '../namespace.js'; import Juice from 'juice'; import htmlToText from 'html-to-text'; import Handlebars from 'handlebars'; -import { Utils, getSetting } from 'meteor/nova:lib'; +import { Utils, getSetting } from 'meteor/nova:lib'; // import from nova:lib because nova:core is not loaded yet NovaEmail.templates = {}; diff --git a/packages/nova-embedly/lib/components/EmbedlyURL.jsx b/packages/nova-embedly/lib/components/EmbedlyURL.jsx index 5ce17bd911..3c9f301098 100644 --- a/packages/nova-embedly/lib/components/EmbedlyURL.jsx +++ b/packages/nova-embedly/lib/components/EmbedlyURL.jsx @@ -1,44 +1,76 @@ -import { Components } from 'meteor/nova:lib'; +import { Components, registerComponent } from 'meteor/nova:core'; import { withMutation } from 'meteor/nova:core'; import React, { PropTypes, Component } from 'react'; import FRC from 'formsy-react-components'; -import { graphql } from 'react-apollo'; -import gql from 'graphql-tag'; const Input = FRC.Input; class EmbedlyURL extends Component { - constructor() { - super(); + constructor(props) { + super(props); this.handleBlur = this.handleBlur.bind(this); + this.state = { - loading: false + loading: false, + value: props.value || '', + }; + } + + // clean the media property of the document if it exists: this field is handled server-side in an async callback + async componentDidMount() { + try { + if (this.props.document && !_.isEmpty(this.props.document.media)) { + await this.context.updateCurrentValues({media: {}}); + } + } catch(error) { + console.error('Error cleaning "media" property', error); // eslint-disable-line } } // called whenever the URL input field loses focus - handleBlur() { - - this.setState({loading: true}); - - const url = this.input.getValue(); - - if (url.length) { - // the URL has changed, get a new thumbnail - this.props.getEmbedlyData({url}).then(result => { - this.setState({loading: false}); - console.log('Embedly Data', result); - this.context.addToAutofilledValues({ - title: result.data.getEmbedlyData.title, - body: result.data.getEmbedlyData.description, - thumbnailUrl: result.data.getEmbedlyData.thumbnailUrl + async handleBlur() { + try { + // value from formsy input ref + const url = this.input.getValue(); + + // start the mutation only if the input has a value + if (url.length) { + + // notify the user that something happens + await this.setState({loading: true}); + + // the URL has changed, get new title, body, thumbnail & media for this url + const result = await this.props.getEmbedlyData({url}); + + // uncomment for debug + // console.log('Embedly Data', result); + + // extract the relevant data, for easier consumption + const { data: { getEmbedlyData: { title, description, thumbnailUrl } } } = result; + + // update the form + await this.context.updateCurrentValues({ + title: title || "", + body: description || "", + thumbnailUrl: thumbnailUrl || "", }); - }).catch(error => { - this.setState({loading: false}); - console.log(error) - this.context.throwError({content: error.message, type: "error"}); - }); + + // embedly component is done + await this.setState({loading: false}); + + // remove errors & keep the current values + await this.context.clearForm({clearErrors: true}); + } + } catch(error) { + + console.error(error); // eslint-disable-line + + // embedly component is done + await this.setState({loading: false}); + + // something bad happened + await this.context.throwError(error.message); } } @@ -58,7 +90,7 @@ class EmbedlyURL extends Component { loadingStyle.display = this.state.loading ? "block" : "none"; // see https://facebook.github.io/react/warnings/unknown-prop.html - const {document, updateCurrentValue, control, getEmbedlyData, ...rest} = this.props; + const {document, control, getEmbedlyData, ...rest} = this.props; // eslint-disable-line return (
@@ -77,36 +109,17 @@ class EmbedlyURL extends Component { } EmbedlyURL.propTypes = { - name: React.PropTypes.string, - value: React.PropTypes.any, - label: React.PropTypes.string + name: PropTypes.string, + value: PropTypes.any, + label: PropTypes.string } EmbedlyURL.contextTypes = { - addToAutofilledValues: React.PropTypes.func, - throwError: React.PropTypes.func, - actions: React.PropTypes.object, + updateCurrentValues: PropTypes.func, + throwError: PropTypes.func, + clearForm: PropTypes.func, } -// note: not used since we use `withMutation` and getEmbedlyData returns a `JSON` type -// function withGetEmbedlyData() { -// return graphql(gql` -// mutation getEmbedlyData($url: String) { -// getEmbedlyData(url: $url) { -// title -// media -// description -// thumbnailUrl -// sourceName -// sourceUrl -// } -// } -// `, { -// name: 'getEmbedlyData' -// }); -// } - - export default withMutation({ name: 'getEmbedlyData', args: {url: 'String'}, diff --git a/packages/nova-embedly/lib/components/ThumbnailURL.jsx b/packages/nova-embedly/lib/components/ThumbnailURL.jsx index 15bcefae39..c9df2cef7a 100644 --- a/packages/nova-embedly/lib/components/ThumbnailURL.jsx +++ b/packages/nova-embedly/lib/components/ThumbnailURL.jsx @@ -17,7 +17,7 @@ class ThumbnailURL extends Component { } clearThumbnail() { - this.context.updateCurrentValue("thumbnailUrl", ""); + this.context.updateCurrentValues({thumbnailUrl: ""}); } showInput() { @@ -69,7 +69,7 @@ ThumbnailURL.propTypes = { ThumbnailURL.contextTypes = { addToPrefilledValues: React.PropTypes.func, - updateCurrentValue: React.PropTypes.func, + updateCurrentValues: React.PropTypes.func, deleteValue: React.PropTypes.func } diff --git a/packages/nova-embedly/lib/custom_fields.js b/packages/nova-embedly/lib/custom_fields.js index bd0724db55..9d6b1fe9f5 100644 --- a/packages/nova-embedly/lib/custom_fields.js +++ b/packages/nova-embedly/lib/custom_fields.js @@ -6,14 +6,7 @@ Posts.addField([ { fieldName: 'url', fieldSchema: { - type: String, - optional: true, - max: 500, - insertableBy: ['members'], - editableBy: ['members'], - viewableBy: ['guests'], - control: EmbedlyURL, - publish: true + control: EmbedlyURL, // we are just extending the field url, not replacing it } }, { diff --git a/packages/nova-embedly/lib/server/get_embedly_data.js b/packages/nova-embedly/lib/server/get_embedly_data.js index 4ffc415da5..ab3854801c 100644 --- a/packages/nova-embedly/lib/server/get_embedly_data.js +++ b/packages/nova-embedly/lib/server/get_embedly_data.js @@ -44,7 +44,7 @@ function getEmbedlyData(url) { console.log(error); // eslint-disable-line // the first 13 characters of the Embedly errors are "failed [400] ", so remove them and parse the rest var errorObject = JSON.parse(error.message.substring(13)); - throw new Meteor.Error(errorObject.error_code, errorObject.error_message); + throw new Error(errorObject.error_code, errorObject.error_message); } } @@ -85,6 +85,9 @@ function updateMediaOnEdit (modifier, post) { var data = getEmbedlyData(newUrl); if(!!data) { if (!!data.media.html) { + if (modifier.$unset.media) { + delete modifier.$unset.media + } modifier.$set.media = data.media; } diff --git a/packages/nova-events/lib/callbacks.js b/packages/nova-events/lib/callbacks.js new file mode 100644 index 0000000000..7f95e92b2d --- /dev/null +++ b/packages/nova-events/lib/callbacks.js @@ -0,0 +1,86 @@ +import { addCallback } from 'meteor/nova:core'; +import Posts from 'meteor/nova:posts'; +import Events from './collection.js'; +import { sendGoogleAnalyticsRequest, requestAnalyticsAsync } from './helpers'; + +// add client-side callback: log a ga request on page view +addCallback('router.onUpdate', sendGoogleAnalyticsRequest); + + +// generate callbacks on collection for each common mutation +['users', 'posts', 'comments', 'categories'].map(collection => { + + return ['new', 'edit', 'remove'].map(mutation => { + + const hook = `${collection}.${mutation}`; + + addCallback(`${hook}.async`, function MutationAnalyticsTracking(...args) { + + // a note on what's happenning below: + // the first argument is always the document we are interested in + // the second to last argument is always the current user + // on edit.async, the argument on index 1 is always the previous document + // see nova:lib/mutations.js for more informations + + // remove unnecessary 'previousDocument' if operating on a collection.edit hook + if (hook.includes('edit')) { + args.splice(1,1); + } + + const [document, currentUser, ...rest] = args; // eslint-disable-line no-unused-vars + + return requestAnalyticsAsync(hook, document, currentUser); + }); + + // return the hook name, used for debug + return hook; + }); +}); + +// generate callbacks on voting operations +['upvote', 'cancelUpvote', 'downvote', 'cancelDownvote'].map(operation => { + + addCallback(`${operation}.async`, function OperationTracking(...args) { + + const [document, currentUser, ...rest] = args; // eslint-disable-line no-unused-vars + + return requestAnalyticsAsync(operation, document, currentUser); + }); +}); + +// identify profile completion +addCallback(`users.profileCompleted.async`, function ProfileCompletedTracking(user) { + return requestAnalyticsAsync('users.profileCompleted', user); +}); + + +// /** +// * @summary Increase the number of clicks on a post +// * @param {string} postId – the ID of the post being edited +// * @param {string} ip – the IP of the current user +// */ +Posts.increaseClicks = (postId, ip) => { + const clickEvent = { + name: 'click', + properties: { + postId: postId, + ip: ip + } + }; + + // make sure this IP hasn't previously clicked on this post + const existingClickEvent = Events.findOne({name: 'click', 'properties.postId': postId, 'properties.ip': ip}); + + if(!existingClickEvent) { + Events.log(clickEvent); + return Posts.update(postId, { $inc: { clickCount: 1 }}); + } +}; + +// track links clicked, locally in Events collection +// note: this event is not sent to segment cause we cannot access the current user +// in our server-side route /out -> sending an event would create a new anonymous +// user: the free limit of 1,000 unique users per month would be reached quickly +addCallback(`posts.click.async`, function PostsClickTracking(postId, ip) { + return Posts.increaseClicks(postId, ip); +}); diff --git a/packages/nova-events/lib/client.js b/packages/nova-events/lib/client.js index 3b4757489c..5f5e7d9855 100644 --- a/packages/nova-events/lib/client.js +++ b/packages/nova-events/lib/client.js @@ -1,5 +1,8 @@ import Events from './collection.js'; +import { initGoogleAnalytics } from './helpers.js'; +import './callbacks.js'; -import './client/analytics.js'; +// init google analytics on the client module +initGoogleAnalytics(); -export default Events; \ No newline at end of file +export default Events; diff --git a/packages/nova-events/lib/client/analytics.js b/packages/nova-events/lib/client/analytics.js deleted file mode 100644 index 875529d1f4..0000000000 --- a/packages/nova-events/lib/client/analytics.js +++ /dev/null @@ -1,40 +0,0 @@ -import Events from '../collection.js'; -import { addCallback, getSetting } from 'meteor/nova:core'; - -Events.analyticsRequest = function() { - // Google Analytics - if (typeof window.ga !== 'undefined'){ - window.ga('send', 'pageview', { - 'page': window.location.pathname - }); - } -}; - -Events.analyticsInit = function() { - - // Google Analytics - const googleAnalyticsId = getSetting("googleAnalyticsId"); - if (googleAnalyticsId) { - - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - - var cookieDomain = document.domain === "localhost" ? "none" : "auto"; - - window.ga('create', googleAnalyticsId, cookieDomain); - - } - - // trigger first request once analytics are initialized - Events.analyticsRequest(); - -}; - -Events.analyticsInit(); - -function analyticsRequest () { - Events.analyticsRequest(); -} -addCallback('router.onUpdate', analyticsRequest); diff --git a/packages/nova-events/lib/collection.js b/packages/nova-events/lib/collection.js index 40b2d739f2..7202fd8605 100644 --- a/packages/nova-events/lib/collection.js +++ b/packages/nova-events/lib/collection.js @@ -28,41 +28,6 @@ Events.schema = new SimpleSchema({ } }); -// Meteor.startup(function(){ -// // needs to happen after every fields are added -// Events.internationalize(); -// }); - Events.attachSchema(Events.schema); -if (Meteor.isServer) { - Events.log = function (event) { - - // if event is supposed to be unique, check if it has already been logged - if (!!event.unique && !!Events.findOne({name: event.name})) { - return; - } - - event.createdAt = new Date(); - - Events.insert(event); - - }; -} - -Events.track = function(event, properties){ - // console.log('trackevent: ', event, properties); - properties = properties || {}; - //TODO - // add event to an Events collection for logging and buffering purposes - // if(Meteor.isClient){ - // if(typeof mixpanel !== 'undefined' && typeof mixpanel.track !== 'undefined'){ - // mixpanel.track(event, properties); - // } - // if(typeof GoSquared !== 'undefined' && typeof GoSquared.DefaultTracker !== 'undefined'){ - // GoSquared.DefaultTracker.TrackEvent(event, JSON.stringify(properties)); - // } - // } -}; - export default Events; diff --git a/packages/nova-events/lib/helpers.js b/packages/nova-events/lib/helpers.js new file mode 100644 index 0000000000..95b09add04 --- /dev/null +++ b/packages/nova-events/lib/helpers.js @@ -0,0 +1,123 @@ +import Analytics from 'analytics-node'; +import { getSetting } from 'meteor/nova:core'; +import Events from './collection.js'; +/* + + We provide a special support for Google Analytics. + + If you want to enable GA page viewing / tracking, go to + your settings file and update the "public > googleAnalyticsId" + field with your GA unique identifier (UA-xxx...). + +*/ + +export const sendGoogleAnalyticsRequest = () => { + if (window && window.ga) { + window.ga('send', 'pageview', { + 'page': window.location.pathname + }); + } +}; + +export const initGoogleAnalytics = () => { + + // get the google analytics id from the settings + const googleAnalyticsId = getSetting("googleAnalyticsId"); + + // the google analytics id exists & isn't the placeholder from sample_settings.json + if (googleAnalyticsId && googleAnalyticsId !== "foo123") { + + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + + const cookieDomain = document.domain === "localhost" ? "none" : "auto"; + + window.ga('create', googleAnalyticsId, cookieDomain); + + // trigger first request once analytics are initialized + sendGoogleAnalyticsRequest(); + } +}; + +/* + + We provide a special support for Segment, using analytics-node + See https://segment.com/docs/sources/server/node/ + +*/ + +export const requestAnalyticsAsync = (hook, document, user) => { + + // get the segment write key from the settings + const useSegment = getSetting('useSegment'); + const writeKey = getSetting('segmentWriteKey'); + + // the settings obviously tells to use segment + // and segment write key is defined & isn't the placeholder from sample_settings.json + if (useSegment && writeKey && writeKey !== '456bar') { + + const analytics = new Analytics(writeKey); + + if (hook.includes('users')) { + // if the mutation is related to users, use analytics.identify + // see https://segment.com/docs/sources/server/node/#identify + + // note: on users.new.async, user is undefined + const userId = user ? user._id : document._id; + + if (document.services) { + if(document.services.password) { + delete document.services.password; + } + + if (document.services.resume) { + delete document.services.resume; + } + } + + + const data = { + userId, + traits: document, + }; + + // uncomment for debug + // console.log(`// dispatching identify on "${hook}" (user ${userId})`); + // console.log(data); + + analytics.identify(data); + + } else { + // else use analytics.track + // see https://segment.com/docs/sources/server/node/#track + + const data = { + userId: user._id, + event: hook, + properties: document, + }; + + // uncomment for debug + // console.log(`// dispatching track on "${hook}"`); + // console.log(data); + + analytics.track(data); + } + } +} + +// collection based logging +Events.log = function (event) { + + // if event is supposed to be unique, check if it has already been logged + if (!!event.unique && !!Events.findOne({name: event.name})) { + return; + } + + event.createdAt = new Date(); + + Events.insert(event); + +}; diff --git a/packages/nova-events/lib/mutations.js b/packages/nova-events/lib/mutations.js new file mode 100644 index 0000000000..7a9138488c --- /dev/null +++ b/packages/nova-events/lib/mutations.js @@ -0,0 +1,19 @@ +import { GraphQLSchema } from 'meteor/nova:core'; +// import Events from './collection.js'; +import { requestAnalyticsAsync } from './helpers.js'; + +GraphQLSchema.addMutation('eventTrack(eventName: String, properties: JSON): JSON'); + +const resolvers = { + Mutation: { + eventTrack: (root, { eventName, properties }, context) => { + const user = context.currentUser || {_id: 'anonymous'}; + + requestAnalyticsAsync(eventName, properties, user); + + return properties; + }, + }, +}; + +GraphQLSchema.addResolvers(resolvers); diff --git a/packages/nova-events/lib/server.js b/packages/nova-events/lib/server.js index 29c8986e57..a82b4b245d 100644 --- a/packages/nova-events/lib/server.js +++ b/packages/nova-events/lib/server.js @@ -1,3 +1,6 @@ import Events from './collection.js'; +import './helpers'; +import './callbacks.js'; +import './mutations.js'; -export default Events; \ No newline at end of file +export default Events; diff --git a/packages/nova-events/package.js b/packages/nova-events/package.js index ef7b72a6f2..43d5aa2e3b 100644 --- a/packages/nova-events/package.js +++ b/packages/nova-events/package.js @@ -10,7 +10,8 @@ Package.onUse(function(api) { api.versionsFrom("METEOR@1.0"); api.use([ - 'nova:lib@1.0.0' + 'nova:core@1.0.0', + 'nova:posts@1.0.0', // needed to track posts click ]); api.mainModule("lib/server.js", "server"); diff --git a/packages/nova-forms/README.md b/packages/nova-forms/README.md index dc5b2ea9a8..2c46bb2755 100644 --- a/packages/nova-forms/README.md +++ b/packages/nova-forms/README.md @@ -211,7 +211,7 @@ An object containing optional autofilled properties. A function that takes a property, and adds it to the `autofilledValues` object. -###### `throwError({content, type})` +###### `throwError(errorMessage)` A callback function that can be used to throw an error. diff --git a/packages/nova-forms/lib/DateTime.jsx b/packages/nova-forms/lib/DateTime.jsx index c945649073..2e03ff17fd 100644 --- a/packages/nova-forms/lib/DateTime.jsx +++ b/packages/nova-forms/lib/DateTime.jsx @@ -16,7 +16,7 @@ class DateTime extends Component { } updateDate(date) { - this.context.updateCurrentValue(this.props.name, date); + this.context.updateCurrentValues({[this.props.name]: date}); } render() { @@ -48,7 +48,7 @@ DateTime.propTypes = { DateTime.contextTypes = { addToAutofilledValues: React.PropTypes.func, - updateCurrentValue: React.PropTypes.func, + updateCurrentValues: React.PropTypes.func, }; export default DateTime; diff --git a/packages/nova-forms/lib/Form.jsx b/packages/nova-forms/lib/Form.jsx index 787649eb79..3c6ada1684 100644 --- a/packages/nova-forms/lib/Form.jsx +++ b/packages/nova-forms/lib/Form.jsx @@ -60,7 +60,7 @@ class Form extends Component { this.addToAutofilledValues = this.addToAutofilledValues.bind(this); this.throwError = this.throwError.bind(this); this.clearForm = this.clearForm.bind(this); - this.updateCurrentValue = this.updateCurrentValue.bind(this); + this.updateCurrentValues = this.updateCurrentValues.bind(this); this.formKeyDown = this.formKeyDown.bind(this); this.deleteDocument = this.deleteDocument.bind(this); // a debounced version of seState that only updates state every 500 ms (not used) @@ -246,17 +246,21 @@ class Form extends Component { delete e[key]; } }); - this.setState({ + this.setState(prevState => ({ currentValues: e - }); + })); } } - // manually update current value (i.e. on blur). See above for on change instead - updateCurrentValue(fieldName, fieldValue) { - const currentValues = this.state.currentValues; - currentValues[fieldName] = fieldValue; - this.setState({currentValues: currentValues}); + // manually update the current values of one or more fields(i.e. on blur). See above for on change instead + updateCurrentValues(newValues) { + // keep the previous ones and extend (with possible replacement) with new ones + this.setState(prevState => ({ + currentValues: { + ...prevState.currentValues, + ...newValues, + } + })); } // key down handler @@ -283,44 +287,69 @@ class Form extends Component { // render errors renderErrors() { - return
{this.state.errors.map(message => )}
+ return
{this.state.errors.map((message, index) => )}
} // --------------------------------------------------------------------- // // ------------------------------- Context ----------------------------- // // --------------------------------------------------------------------- // - // add error to state - throwError(error) { - this.setState({ - errors: [error] - }); + // add error to form state + // from "GraphQL Error: You have an error [error_code]" + // to { content: "You have an error", type: "error" } + throwError(errorMessage) { + + let strippedError = errorMessage; + + // strip the "GraphQL Error: message [error_code]" given by Apollo if present + const graphqlPrefixIsPresent = strippedError.match(/GraphQL error: (.*)/); + if (graphqlPrefixIsPresent) { + strippedError = graphqlPrefixIsPresent[1]; + } + + // strip the error code if present + const errorCodeIsPresent = strippedError.match(/(.*)\[(.*)\]/); + if (errorCodeIsPresent) { + strippedError = errorCodeIsPresent[1]; + } + + // internationalize the error if necessary + const intlError = Utils.decodeIntlError(strippedError); + if(typeof intlError === 'object') { + const { id, value = "" } = intlError; + strippedError = this.context.intl.formatMessage({id}, {value}); + } + + // build the error for the Flash component and only keep the interesting message + const error = { + content: strippedError, + type: 'error' + }; + + // update the state with unique errors messages + this.setState(prevState => ({ + errors: _.uniq([...prevState.errors, error]) + })); } // add something to prefilled values addToAutofilledValues(property) { - this.setState(function(state){ - return { - autofilledValues: { - ...state.autofilledValues, - ...property - } - }; - }); - } - - // clear value - clearValue(property) { - + this.setState(prevState => ({ + autofilledValues: { + ...prevState.autofilledValues, + ...property + } + })); } // pass on context to all child components getChildContext() { return { throwError: this.throwError, + clearForm: this.clearForm, autofilledValues: this.state.autofilledValues, addToAutofilledValues: this.addToAutofilledValues, - updateCurrentValue: this.updateCurrentValue, + updateCurrentValues: this.updateCurrentValues, getDocument: this.getDocument, }; } @@ -363,17 +392,14 @@ class Form extends Component { // catch graphql errors mutationErrorCallback(error) { - this.setState({disabled: false}); - - console.log("// graphQL Error"); - console.log(error); + this.setState(prevState => ({disabled: false})); + console.log("// graphQL Error"); // eslint-disable-line no-console + console.log(error); // eslint-disable-line no-console + if (!_.isEmpty(error)) { // add error to state - this.throwError({ - content: error.message, - type: "error" - }); + this.throwError(error.message); } // note: we don't have access to the document here :( maybe use redux-forms and get it from the store? @@ -383,12 +409,12 @@ class Form extends Component { // submit form handler submitForm(data) { - this.setState({disabled: true}); + this.setState(prevState => ({disabled: true})); // complete the data with values from custom components which are not being catched by Formsy mixin // note: it follows the same logic as SmartForm's getDocument method data = { - ...this.state.autofilledValues, // ex: can be values from EmbedlyURL or NewsletterSubscribe component + ...this.state.autofilledValues, // ex: can be values from NewsletterSubscribe component ...data, // original data generated thanks to Formsy ...this.state.currentValues, // ex: can be values from DateTime component }; @@ -466,7 +492,7 @@ class Form extends Component { ref="form" > {this.renderErrors()} - {fieldGroups.map(group => )} + {fieldGroups.map(group => )} {this.props.cancelCallback ? : null} @@ -491,30 +517,30 @@ class Form extends Component { Form.propTypes = { // main options - collection: React.PropTypes.object, - document: React.PropTypes.object, // if a document is passed, this will be an edit form - schema: React.PropTypes.object, // usually not needed + collection: PropTypes.object, + document: PropTypes.object, // if a document is passed, this will be an edit form + schema: PropTypes.object, // usually not needed // graphQL - newMutation: React.PropTypes.func, // the new mutation - editMutation: React.PropTypes.func, // the edit mutation - removeMutation: React.PropTypes.func, // the remove mutation + newMutation: PropTypes.func, // the new mutation + editMutation: PropTypes.func, // the edit mutation + removeMutation: PropTypes.func, // the remove mutation // form - prefilledProps: React.PropTypes.object, - layout: React.PropTypes.string, - fields: React.PropTypes.arrayOf(React.PropTypes.string), - showRemove: React.PropTypes.bool, + prefilledProps: PropTypes.object, + layout: PropTypes.string, + fields: PropTypes.arrayOf(PropTypes.string), + showRemove: PropTypes.bool, // callbacks - submitCallback: React.PropTypes.func, - successCallback: React.PropTypes.func, - removeSuccessCallback: React.PropTypes.func, - errorCallback: React.PropTypes.func, - cancelCallback: React.PropTypes.func, - - currentUser: React.PropTypes.object, - client: React.PropTypes.object, + submitCallback: PropTypes.func, + successCallback: PropTypes.func, + removeSuccessCallback: PropTypes.func, + errorCallback: PropTypes.func, + cancelCallback: PropTypes.func, + + currentUser: PropTypes.object, + client: PropTypes.object, } Form.defaultProps = { @@ -526,11 +552,12 @@ Form.contextTypes = { } Form.childContextTypes = { - autofilledValues: React.PropTypes.object, - addToAutofilledValues: React.PropTypes.func, - updateCurrentValue: React.PropTypes.func, - throwError: React.PropTypes.func, - getDocument: React.PropTypes.func + autofilledValues: PropTypes.object, + addToAutofilledValues: PropTypes.func, + updateCurrentValues: PropTypes.func, + throwError: PropTypes.func, + clearForm: PropTypes.func, + getDocument: PropTypes.func } module.exports = Form diff --git a/packages/nova-forms/lib/FormComponent.jsx b/packages/nova-forms/lib/FormComponent.jsx index 704dc1ef09..626447279f 100644 --- a/packages/nova-forms/lib/FormComponent.jsx +++ b/packages/nova-forms/lib/FormComponent.jsx @@ -1,7 +1,7 @@ import React, { PropTypes, Component } from 'react'; import Formsy from 'formsy-react'; import FRC from 'formsy-react-components'; - +import { intlShape } from 'react-intl'; import DateTime from './DateTime.jsx'; // import Utils from './utils.js'; @@ -23,14 +23,14 @@ class FormComponent extends Component { handleBlur() { // see https://facebook.github.io/react/docs/more-about-refs.html if (this.formControl !== null) { - this.props.updateCurrentValue(this.props.name, this.formControl.getValue()); + this.props.updateCurrentValues({[this.props.name]: this.formControl.getValue()}); } } renderComponent() { // see https://facebook.github.io/react/warnings/unknown-prop.html - const { control, group, updateCurrentValue, document, ...rest } = this.props; // eslint-disable-line + const { control, group, updateCurrentValues, document, beforeComponent, afterComponent, ...rest } = this.props; // eslint-disable-line const base = this.props.control === "function" ? this.props : rest; @@ -60,6 +60,7 @@ class FormComponent extends Component { case "radiogroup": return ; case "select": + properties.options = [{label: this.context.intl.formatMessage({id: "forms.select_option"}), disabled: true}, ...properties.options]; return