Merge branch 'devel'

# Conflicts:
#	.github/CONTRIBUTING.md
#	packages/vulcan-lib/lib/server/mutations.js
This commit is contained in:
SachaG 2017-10-06 08:54:18 +09:00
commit 5434356866
537 changed files with 8574 additions and 7673 deletions

View file

@ -1,3 +1,3 @@
Before starting on a new feature, please [check out the roadmap](https://trello.com/b/oLMMqjVL/telescope-roadmap) and come check-in in the [Vulcan Slack channel](http://slack.telescopeapp.org/).
Before starting on a new feature, please [check out the roadmap](https://trello.com/b/dwPR0LTz/vulcanjs-roadmap) and come check-in in the [Vulcan Slack channel](http://slack.telescopeapp.org/).
Also, all PRs should be made to the `devel` branch, not `master`.
Also, all PRs should be made to the `devel` branch, not `master`.

View file

@ -22,3 +22,6 @@ example-simple
# example-permissions
# example-membership
# example-interfaces
# example-reactions
vulcan:debug

View file

@ -1 +1 @@
METEOR@1.5.1
METEOR@1.5.2.2

View file

@ -1,28 +1,28 @@
accounts-base@1.3.1
accounts-base@1.3.4
accounts-password@1.4.0
allow-deny@1.0.6
allow-deny@1.0.9
autoupdate@1.3.12
babel-compiler@6.19.4
babel-compiler@6.20.0
babel-runtime@1.0.1
base64@1.0.10
binary-heap@1.0.10
boilerplate-generator@1.1.2
boilerplate-generator@1.2.0
buffer@0.0.0
caching-compiler@1.1.9
callback-hook@1.0.10
check@1.2.5
ddp@1.3.0
ddp-client@2.0.0
ddp@1.3.1
ddp-client@2.1.3
ddp-common@1.2.9
ddp-rate-limiter@1.0.7
ddp-server@2.0.0
ddp-server@2.0.2
diff-sequence@1.0.7
dynamic-import@0.1.1
ecmascript@0.8.2
dynamic-import@0.1.3
ecmascript@0.8.3
ecmascript-runtime@0.4.1
ecmascript-runtime-client@0.4.3
ecmascript-runtime-server@0.4.1
ejson@1.0.13
ejson@1.0.14
email@1.2.3
example-simple@0.0.0
fourseven:scss@4.5.4
@ -33,22 +33,23 @@ id-map@1.0.9
livedata@1.0.18
localstorage@1.1.1
logging@1.1.17
meteor@1.7.0
meteor@1.7.2
meteor-base@1.1.0
meteorhacks:inject-initial@1.0.4
meteorhacks:picker@1.0.3
minifier-css@1.2.16
minifier-js@2.1.1
minimongo@1.2.1
modules@0.9.2
minifier-js@2.1.4
minimongo@1.3.2
modules@0.10.0
modules-runtime@0.8.0
mongo@1.1.22
mongo@1.2.2
mongo-dev-server@1.0.1
mongo-id@1.0.6
npm-bcrypt@0.9.3
npm-mongo@2.2.30
ordered-dict@1.0.9
percolatestudio:synced-cron@1.1.0
promise@0.8.9
promise@0.9.0
random@1.0.10
rate-limit@1.0.8
reactive-dict@1.1.9
@ -61,19 +62,21 @@ session@1.1.7
sha@1.0.9
shell-server@0.2.4
srp@1.0.10
standard-minifier-css@1.3.4
standard-minifier-js@2.1.1
standard-minifier-css@1.3.5
standard-minifier-js@2.1.2
standard-minifiers@1.1.0
tracker@1.1.3
underscore@1.0.10
url@1.1.0
vulcan:accounts@1.7.0
vulcan:core@1.7.0
vulcan:forms@1.7.0
vulcan:i18n@1.7.0
vulcan:i18n-en-us@1.7.0
vulcan:lib@1.7.0
vulcan:routing@1.7.0
vulcan:users@1.7.0
webapp@1.3.17
vulcan:accounts@1.8.0
vulcan:core@1.8.0
vulcan:debug@1.8.0
vulcan:email@1.8.0
vulcan:forms@1.8.0
vulcan:i18n@1.8.0
vulcan:i18n-en-us@1.8.0
vulcan:lib@1.8.0
vulcan:routing@1.8.0
vulcan:users@1.8.0
webapp@1.3.19
webapp-hashing@1.0.9

21
package-lock.json generated Normal file
View file

@ -0,0 +1,21 @@
{
"name": "Vulcan",
"version": "1.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"flat": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/flat/-/flat-4.0.0.tgz",
"integrity": "sha512-ji/WMv2jdsE+LaznpkIF9Haax0sdpTBozrz/Dtg4qSRMfbs8oVg4ypJunIRYPiMLvH/ed6OflXbnbTIKJhtgeg==",
"requires": {
"is-buffer": "1.1.5"
}
},
"is-buffer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz",
"integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw="
}
}
}

View file

@ -18,11 +18,13 @@
"body-parser": "^1.15.2",
"classnames": "^2.2.3",
"cookie-parser": "^1.4.3",
"cross-fetch": "^0.0.8",
"crypto-js": "^3.1.9-1",
"dataloader": "^1.3.0",
"deepmerge": "^1.2.0",
"escape-string-regexp": "^1.0.5",
"express": "^4.14.0",
"flat": "^4.0.0",
"formsy-react": "^0.19.5",
"formsy-react-components": "^0.10.1",
"graphql": "^0.9.6",
@ -40,7 +42,6 @@
"import": "0.0.6",
"intl": "^1.2.4",
"intl-locales-supported": "^1.0.0",
"isomorphic-fetch": "^2.2.1",
"juice": "^1.11.0",
"mailchimp": "^1.1.6",
"marked": "^0.3.5",
@ -73,7 +74,7 @@
"rss": "^1.2.1",
"sanitize-html": "^1.11.4",
"sendy-api": "^0.1.0",
"simpl-schema": "^0.2.3",
"simpl-schema": "^0.3.2",
"speakingurl": "^9.0.0",
"stripe": "^4.23.1",
"styled-components": "^2.1.1",

View file

@ -1,7 +1,7 @@
Package.describe({
name: "boilerplate-generator",
summary: "Generates the boilerplate html from program's manifest",
version: '1.1.2'
version: '1.2.0'
});
Package.onUse(api => {

View file

@ -2,7 +2,7 @@ import { Components, getRawComponent, replaceComponent } from 'meteor/vulcan:cor
import React from 'react';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import { Link } from 'react-router';
import Posts from "meteor/vulcan:posts";
import { Posts } from 'meteor/example-forum';
import moment from 'moment';
class CustomPostsItem extends getRawComponent('PostsItem') {

View file

@ -1,4 +1,4 @@
import Posts from "meteor/vulcan:posts";
import { Posts } from 'meteor/example-forum';
/*
Let's assign a color to each post (why? cause we want to, that's why).

View file

@ -10,11 +10,9 @@ VulcanEmail.addEmails({
customEmail: {
template: "customEmail",
path: "/email/custom-email",
getProperties() {return {};},
subject() {
return "My awesome new email";
},
getTestObject() {return {};}
}
}
});

View file

@ -1,18 +1,18 @@
<span class="heading">
<a href="{{profileUrl}}">{{postAuthorName}}</a>
<a href="{{PostsSingle.user.pageUrl}}">{{PostsSingle.user.displayName}}</a>
has created a new AWESOME post:
{{#if url}}
<a href="{{linkUrl}}" class="action-link">{{postTitle}}</a>
{{#if PostsSingle.url}}
<a href="{{PostsSingle.linkUrl}}" class="action-link">{{PostsSingle.title}}</a>
{{else}}
{{postTitle}}
{{PostsSingle.title}}
{{/if}}
</span><br><br>
{{#if htmlBody}}
{{#if PostsSingle.htmlBody}}
<div class="post-body">
{{{htmlBody}}}
{{{PostsSingle.htmlBody}}}
</div>
<br>
{{/if}}
<a href="{{postUrl}}">Discuss</a><br><br>
<a href="{{PostsSingle.pageUrl}}">Discuss</a><br><br>

View file

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1 @@
export * from '../modules/index.js';

View file

@ -1,5 +1,5 @@
import React from 'react';
import Posts from 'meteor/vulcan:posts';
import { Posts } from '../../modules/posts/index.js';
import { Link } from 'react-router';
const AdminUsersPosts = ({ document: user }) =>

View file

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import { Components, registerComponent, getFragment, withMessages } from 'meteor/vulcan:core';
import Categories from "meteor/vulcan:categories";
import { Categories } from '../../modules/categories/index.js';
const CategoriesEditForm = (props, context) => {

View file

@ -7,7 +7,7 @@ import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import { withRouter } from 'react-router'
import { LinkContainer } from 'react-router-bootstrap';
import Categories from 'meteor/vulcan:categories';
import { Categories } from '../../modules/categories/index.js';
import { withApollo } from 'react-apollo';
class CategoriesList extends PureComponent {
@ -30,7 +30,7 @@ class CategoriesList extends PureComponent {
getCategoryLink(slug) {
const categories = this.getCurrentCategoriesArray();
return {
pathname: '/',
pathname: Utils.getRoutePath('posts.list'),
query: {
...this.props.location.query,
cat: categories.includes(slug) ? _.without(categories, slug) : categories.concat([slug])

View file

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import { Components, registerComponent, getFragment, withMessages } from 'meteor/vulcan:core';
import Categories from "meteor/vulcan:categories";
import { Categories } from '../../modules/categories/index.js';
const CategoriesNewForm = (props, context) => {

View file

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { LinkContainer } from 'react-router-bootstrap';
import MenuItem from 'react-bootstrap/lib/MenuItem'
import { withRouter } from 'react-router'
import Categories from 'meteor/vulcan:categories';
import { Categories } from '../../modules/categories/index.js';
class Category extends PureComponent {

View file

@ -1,7 +1,7 @@
import { Components, registerComponent, getFragment, withMessages } from 'meteor/vulcan:core';
import React from 'react';
import PropTypes from 'prop-types';
import Comments from "meteor/vulcan:comments";
import { Comments } from '../../modules/comments/index.js';
const CommentsEditForm = (props, context) => {
return (

View file

@ -2,7 +2,7 @@ import { Components, registerComponent, withMessages } from 'meteor/vulcan:core'
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { intlShape, FormattedMessage } from 'meteor/vulcan:i18n';
import Comments from 'meteor/vulcan:comments';
import { Comments } from '../../modules/comments/index.js';
import moment from 'moment';
class CommentsItem extends PureComponent {

View file

@ -1,7 +1,7 @@
import { Components, registerComponent, getFragment, withMessages } from 'meteor/vulcan:core';
import React from 'react';
import PropTypes from 'prop-types';
import Comments from "meteor/vulcan:comments";
import { Comments } from '../../modules/comments/index.js';
import { FormattedMessage } from 'meteor/vulcan:i18n';
const CommentsNewForm = (props, context) => {

View file

@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withCurrentUser, getSetting, Components, registerComponent } from 'meteor/vulcan:core';
import { withCurrentUser, getSetting, registerSetting, Components, registerComponent } from 'meteor/vulcan:core';
const Header = (props, context) => {
const logoUrl = getSetting("logoUrl");
const siteTitle = getSetting("title", "My App");
const tagline = getSetting("tagline");
const logoUrl = getSetting('logoUrl');
const siteTitle = getSetting('title', 'My App');
const tagline = getSetting('tagline');
return (
<div className="header-wrapper">

View file

@ -11,7 +11,9 @@ const Layout = ({currentUser, children, currentRoute}) =>
<link name="bootstrap" rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css"/>
<link name="font-awesome" rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"/>
</Helmet>
<Components.HeadTags />
{currentUser ? <Components.UsersProfileCheck currentUser={currentUser} documentId={currentUser._id} /> : null}
<Components.Header />

View file

@ -1,4 +1,4 @@
import { registerComponent, Components } from 'meteor/vulcan:core';
import { registerComponent, Components, Utils } from 'meteor/vulcan:core';
import React, { PropTypes, Component } from 'react';
import { intlShape } from 'meteor/vulcan:i18n';
import Formsy from 'formsy-react';
@ -42,7 +42,7 @@ class SearchForm extends Component{
const query = data.searchQuery === '' ? routerQuery : {...routerQuery, query: data.searchQuery};
delay(() => {
router.push({pathname: "/", query: query});
router.push({pathname: Utils.getRoutePath('posts.list'), query: query});
}, 700 );
}

View file

@ -0,0 +1,71 @@
import { Components, registerComponent, withMessages } from 'meteor/vulcan:core';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { withVote, hasVotedClient } from 'meteor/vulcan:voting';
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
class Vote extends PureComponent {
constructor() {
super();
this.vote = this.vote.bind(this);
this.getActionClass = this.getActionClass.bind(this);
this.hasVoted = this.hasVoted.bind(this);
}
vote(e) {
e.preventDefault();
const document = this.props.document;
const collection = this.props.collection;
const user = this.props.currentUser;
if(!user){
this.props.flash(this.context.intl.formatMessage({id: 'users.please_log_in'}));
} else {
this.props.vote({document, voteType: 'upvote', collection, currentUser: this.props.currentUser});
}
}
hasVoted() {
return hasVotedClient({document: this.props.document, voteType: 'upvote'})
}
getActionClass() {
const actionsClass = classNames(
'vote-button',
{upvoted: this.hasVoted()},
);
return actionsClass;
}
render() {
return (
<div className={this.getActionClass()}>
<a className="upvote-button" onClick={this.vote}>
<Components.Icon name="upvote" />
<div className="sr-only"><FormattedMessage id="voting.upvote"/></div>
<div className="vote-count">{this.props.document.baseScore || 0}</div>
</a>
</div>
)
}
}
Vote.propTypes = {
document: PropTypes.object.isRequired, // the document to upvote
collection: PropTypes.object.isRequired, // the collection containing the document
vote: PropTypes.func.isRequired, // mutate function with callback inside
currentUser: PropTypes.object, // user might not be logged in, so don't make it required
};
Vote.contextTypes = {
intl: intlShape
};
registerComponent('Vote', Vote, withMessages, withVote);

View file

@ -1,7 +1,7 @@
import { Components, registerComponent } from 'meteor/vulcan:core';
import React from 'react';
import { Link } from 'react-router';
import Posts from "meteor/vulcan:posts";
import { Posts } from '../../modules/posts/index.js';
const PostsCommenters = ({post}) => {
return (

View file

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import { withList, withCurrentUser, Components, registerComponent, Utils } from 'meteor/vulcan:core';
import Comments from 'meteor/vulcan:comments';
import { Comments } from '../../modules/comments/index.js';
const PostsCommentsThread = (props, /* context*/) => {

View file

@ -0,0 +1,21 @@
import { Components, registerComponent, getSetting, registerSetting } from 'meteor/vulcan:core';
import React, { PropTypes, Component } from 'react';
import moment from 'moment';
registerSetting('forum.numberOfDays', 5, 'Number of days to display in Daily view');
const PostsDaily = props => {
// const terms = props.location && props.location.query;
const numberOfDays = getSetting('forum.numberOfDays', 5);
const terms = {
view: 'top',
after: moment().subtract(numberOfDays - 1, 'days').format('YYYY-MM-DD'),
before: moment().format('YYYY-MM-DD'),
};
return <Components.PostsDailyList terms={terms}/>
};
PostsDaily.displayName = 'PostsDaily';
registerComponent('PostsDaily', PostsDaily);

View file

@ -2,8 +2,8 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import Posts from 'meteor/vulcan:posts';
import { withCurrentUser, withList, getSetting, Components, getRawComponent, registerComponent } from 'meteor/vulcan:core';
import { Posts } from '../../modules/posts/index.js';
import { withCurrentUser, withList, getSetting, registerSetting, Components, getRawComponent, registerComponent } from 'meteor/vulcan:core';
class PostsDailyList extends PureComponent {
@ -51,7 +51,7 @@ class PostsDailyList extends PureComponent {
// variant 1: reload everything each time (works with polling)
loadMoreDays(e) {
e.preventDefault();
const numberOfDays = getSetting('numberOfDays', 5);
const numberOfDays = getSetting('forum.numberOfDays', 5);
const loadMoreAfter = moment(this.state.after, 'YYYY-MM-DD').subtract(numberOfDays, 'days').format('YYYY-MM-DD');
this.props.loadMore({
@ -68,7 +68,7 @@ class PostsDailyList extends PureComponent {
// variant 2: only load new data (need to disable polling)
loadMoreDaysInc(e) {
e.preventDefault();
const numberOfDays = getSetting('numberOfDays', 5);
const numberOfDays = getSetting('forum.numberOfDays', 5);
const loadMoreAfter = moment(this.state.after, 'YYYY-MM-DD').subtract(numberOfDays, 'days').format('YYYY-MM-DD');
const loadMoreBefore = moment(this.state.after, 'YYYY-MM-DD').subtract(1, 'days').format('YYYY-MM-DD');
@ -105,8 +105,8 @@ PostsDailyList.propTypes = {
};
PostsDailyList.defaultProps = {
days: getSetting('numberOfDays', 5),
increment: getSetting('numberOfDays', 5)
days: getSetting('forum.numberOfDays', 5),
increment: getSetting('forum.numberOfDays', 5)
};
const options = {

View file

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent, getFragment, withMessages, withCurrentUser } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import Posts from "meteor/vulcan:posts";
import { Posts } from '../../modules/posts/index.js';
import Users from "meteor/vulcan:users";
import { withRouter } from 'react-router'

View file

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import { Link } from 'react-router';
import Posts from "meteor/vulcan:posts";
import { Posts } from '../../modules/posts/index.js';
import moment from 'moment';
class PostsItem extends PureComponent {
@ -37,7 +37,7 @@ class PostsItem extends PureComponent {
<div className={postClass}>
<div className="posts-item-vote">
<Components.Vote collection={Posts} document={post} currentUser={this.props.currentUser}/>
<Components.Vote collection={Posts} document={post} currentUser={this.props.currentUser} />
</div>
{post.thumbnailUrl ? <Components.PostsThumbnail post={post}/> : null}

View file

@ -1,7 +1,7 @@
import { Components, registerComponent, withList, withCurrentUser, Utils } from 'meteor/vulcan:core';
import React from 'react';
import PropTypes from 'prop-types';
import Posts from 'meteor/vulcan:posts';
import { Posts } from '../../modules/posts/index.js';
import Alert from 'react-bootstrap/lib/Alert'
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
import classNames from 'classnames';
@ -17,7 +17,7 @@ const PostsList = ({className, results, loading, count, totalCount, loadMore, sh
const hasMore = totalCount > results.length;
return (
<div className={classNames(className, 'posts-list')}>
<div className={classNames(className, 'posts-list', `posts-list-${terms.view}`)}>
{showHeader ? <Components.PostsListHeader/> : null}
{error ? <Error error={Utils.decodeIntlError(error)} /> : null }
<div className="posts-list-content">

View file

@ -1,5 +1,5 @@
import { Components, registerComponent, getRawComponent, getFragment, withMessages } from 'meteor/vulcan:core';
import Posts from "meteor/vulcan:posts";
import { Posts } from '../../modules/posts/index.js';
import React from 'react';
import PropTypes from 'prop-types';
import { intlShape, FormattedMessage } from 'meteor/vulcan:i18n';

View file

@ -1,5 +1,5 @@
import { Components, registerComponent, withDocument, withCurrentUser, getActions, withMutation } from 'meteor/vulcan:core';
import Posts from 'meteor/vulcan:posts';
import { Posts } from '../../modules/posts/index.js';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

View file

@ -5,10 +5,10 @@ const PostsStats = ({post}) => {
return (
<div className="posts-stats">
{post.score ? <span className="posts-stats-item" title="Score"><Components.Icon name="score"/> {Math.floor(post.score*10000)/10000} <span className="sr-only">Score</span></span> : ""}
<span className="posts-stats-item" title="Upvotes"><Components.Icon name="upvote"/> {post.upvotes} <span className="sr-only">Upvotes</span></span>
<span className="posts-stats-item" title="Clicks"><Components.Icon name="clicks"/> {post.clickCount} <span className="sr-only">Clicks</span></span>
<span className="posts-stats-item" title="Views"><Components.Icon name="views"/> {post.viewCount} <span className="sr-only">Views</span></span>
{post.score ? <span className="posts-stats-item" title="Score"><Components.Icon name="score"/> {Math.floor((post.score || 0)*10000)/10000} <span className="sr-only">Score</span></span> : ""}
<span className="posts-stats-item" title="Upvotes"><Components.Icon name="upvote"/> {post.baseScore || 0} <span className="sr-only">Upvotes</span></span>
<span className="posts-stats-item" title="Clicks"><Components.Icon name="clicks"/> {post.clickCount || 0} <span className="sr-only">Clicks</span></span>
<span className="posts-stats-item" title="Views"><Components.Icon name="views"/> {post.viewCount || 0} <span className="sr-only">Views</span></span>
</div>
)
}

View file

@ -1,6 +1,6 @@
import { registerComponent } from 'meteor/vulcan:core';
import React from 'react';
import Posts from "meteor/vulcan:posts";
import { Posts } from '../../modules/posts/index.js';
const PostsThumbnail = ({post}) =>
<a className="posts-thumbnail" href={Posts.getLink(post)} target={Posts.getLinkTarget(post)}>

View file

@ -1,4 +1,4 @@
import { registerComponent, withCurrentUser } from 'meteor/vulcan:core';
import { registerComponent, withCurrentUser, Utils } from 'meteor/vulcan:core';
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
@ -10,10 +10,10 @@ import Users from 'meteor/vulcan:users';
const PostsViews = (props, context) => {
let views = ["top", "new", "best"];
const adminViews = ["pending", "rejected", "scheduled"];
let views = ['top', 'new', 'best'];
const adminViews = ['pending', 'rejected', 'scheduled'];
if (Users.canDo(props.currentUser, "posts.edit.all")) {
if (Users.canDo(props.currentUser, 'posts.edit.all')) {
views = views.concat(adminViews);
}
@ -24,18 +24,18 @@ const PostsViews = (props, context) => {
<DropdownButton
bsStyle="default"
className="views btn-secondary"
title={context.intl.formatMessage({id: "posts.view"})}
title={context.intl.formatMessage({id: 'posts.view'})}
id="views-dropdown"
>
{views.map(view =>
<LinkContainer key={view} to={{pathname: "/", query: {...query, view: view}}} className="dropdown-item">
<LinkContainer key={view} to={{pathname: Utils.getRoutePath('posts.list'), query: {...query, view: view}}} className="dropdown-item">
<MenuItem>
<FormattedMessage id={"posts."+view}/>
</MenuItem>
</LinkContainer>
)}
<LinkContainer to={"/daily"} className="dropdown-item">
<MenuItem className={"bar"}>
<LinkContainer to="/daily" className="dropdown-item">
<MenuItem className="bar">
<FormattedMessage id="posts.daily"/>
</MenuItem>
</LinkContainer>
@ -50,7 +50,7 @@ PostsViews.propTypes = {
};
PostsViews.defaultProps = {
defaultView: "top"
defaultView: 'top'
};
PostsViews.contextTypes = {
@ -58,6 +58,6 @@ PostsViews.contextTypes = {
intl: intlShape
};
PostsViews.displayName = "PostsViews";
PostsViews.displayName = 'PostsViews';
registerComponent('PostsViews', PostsViews, withCurrentUser, withRouter);

View file

@ -0,0 +1,26 @@
/*
The Categories collection
*/
import { createCollection, getDefaultResolvers, getDefaultMutations } from 'meteor/vulcan:core';
import schema from './schema.js';
/**
* @summary The global namespace for Categories.
* @namespace Categories
*/
export const Categories = createCollection({
collectionName: 'Categories',
typeName: 'Category',
schema,
resolvers: getDefaultResolvers('Categories'),
mutations: getDefaultMutations('Categories'),
});

View file

@ -1,4 +1,10 @@
import Posts from "meteor/vulcan:posts";
/*
Custom fields on Posts collection
*/
import { Posts } from '../../modules/posts/index.js';
import { getCategoriesAsOptions } from './schema.js';
Posts.addField([
@ -6,14 +12,14 @@ Posts.addField([
fieldName: 'categories',
fieldSchema: {
type: Array,
control: "checkboxgroup",
control: 'checkboxgroup',
optional: true,
insertableBy: ['members'],
editableBy: ['members'],
viewableBy: ['guests'],
form: {
noselect: true,
type: "bootstrap-category",
type: 'bootstrap-category',
order: 50,
options: formProps => getCategoriesAsOptions(formProps.client),
},

View file

@ -0,0 +1,25 @@
import { registerFragment } from 'meteor/vulcan:core';
// note: fragment used by default on CategoriesList & PostsList fragments
registerFragment(`
fragment CategoriesMinimumInfo on Category {
# vulcan:categories
_id
name
slug
}
`);
registerFragment(`
fragment CategoriesList on Category {
# vulcan:categories
...CategoriesMinimumInfo
description
order
image
parentId
parent {
...CategoriesMinimumInfo
}
}
`);

View file

@ -1,5 +1,5 @@
import Posts from "meteor/vulcan:posts";
import Categories from "./collection.js";
import { Posts } from '../posts/index.js';
import { Categories } from './collection.js';
import { Utils } from 'meteor/vulcan:core';
/**
@ -30,7 +30,7 @@ Categories.getChildren = function (category) {
var categoriesArray = [];
var getChildren = function recurse (categories) {
var children = Categories.find({parentId: {$in: _.pluck(categories, "_id")}}).fetch()
var children = Categories.find({parentId: {$in: _.pluck(categories, '_id')}}).fetch()
if (children.length > 0) {
categoriesArray = categoriesArray.concat(children);
recurse(children);
@ -52,9 +52,9 @@ Posts.getCategories = function (post) {
* @param {Object} category
*/
Categories.getUrl = function (category, isAbsolute) {
isAbsolute = typeof isAbsolute === "undefined" ? false : isAbsolute; // default to false
const prefix = isAbsolute ? Utils.getSiteUrl().slice(0,-1) : "";
// return prefix + FlowRouter.path("postsCategory", category);
isAbsolute = typeof isAbsolute === 'undefined' ? false : isAbsolute; // default to false
const prefix = isAbsolute ? Utils.getSiteUrl().slice(0,-1) : '';
// return prefix + FlowRouter.path('postsCategory', category);
return `${prefix}/?cat=${category.slug}`;
};
/**
@ -62,6 +62,6 @@ Categories.getUrl = function (category, isAbsolute) {
* @param {Object} category
*/
Categories.getCounterName = function (category) {
return category._id + "-postsCount";
return category._id + '-postsCount';
}

View file

@ -0,0 +1,8 @@
export * from './collection.js';
import './fragments.js';
import './views.js';
import './custom_fields.js';
import './helpers.js';
import './permissions.js';
import './parameters.js';

View file

@ -0,0 +1,61 @@
/*
Categories parameter
*/
import { addCallback, getSetting, registerSetting, getFragment, runQuery } from 'meteor/vulcan:core';
import gql from 'graphql-tag';
import { Categories } from './collection.js';
registerSetting('forum.categoriesFilter', 'union', 'Display posts belonging to all (“intersection”) or at least one of (“union”) the selected categories');
// Category Posts Parameters
// Add a 'categories' property to terms which can be used to filter *all* existing Posts views.
function PostsCategoryParameter(parameters, terms, apolloClient) {
// get category slugs
const cat = terms.cat || terms['cat[]'];
const categoriesSlugs = Array.isArray(cat) ? cat : [cat];
let allCategories = [];
if (cat && cat.length) {
// get all categories
// note: specify all arguments, see https://github.com/apollographql/apollo-client/issues/2051
const query = `
query GetCategories($terms: JSON) {
CategoriesList(terms: $terms) {
_id
slug
}
}
`
if (Meteor.isClient) {
// get categories from Redux store
allCategories = apolloClient.readQuery({
query: gql`${query}`,
variables: {terms: {limit: 0, itemsPerPage: 0}}
}).CategoriesList;
} else {
// TODO: figure out how to make this async without messing up withList on the client
// get categories through GraphQL API using runQuery
// const results = await runQuery(query);
// allCategories = results.data.CategoriesList;
allCategories = Categories.find().fetch();
}
// get corresponding category ids
const categoriesIds = _.pluck(_.filter(allCategories, category => _.contains(categoriesSlugs, category.slug)), '_id');
const operator = getSetting('forum.categoriesFilter', 'union') === 'union' ? '$in' : '$all';
// parameters.selector = Meteor.isClient ? {...parameters.selector, 'categories._id': {$in: categoriesIds}} : {...parameters.selector, categories: {[operator]: categoriesIds}};
parameters.selector = {...parameters.selector, categories: {[operator]: categoriesIds}};
}
return parameters;
}
addCallback('posts.parameters', PostsCategoryParameter);

View file

@ -1,19 +1,25 @@
/*
Categories permissions
*/
import Users from 'meteor/vulcan:users';
const guestsActions = [
"categories.view"
'categories.view'
];
Users.groups.guests.can(guestsActions);
const membersActions = [
"categories.view"
'categories.view'
];
Users.groups.members.can(membersActions);
const adminActions = [
"categories.view",
"categories.new",
"categories.edit.all",
"categories.remove.all"
'categories.view',
'categories.new',
'categories.edit.all',
'categories.remove.all'
];
Users.groups.admins.can(adminActions);

View file

@ -1,4 +1,11 @@
/*
Categories schema
*/
import { Utils } from 'meteor/vulcan:core';
import { Categories } from './collection.js';
export function getCategories (apolloClient) {
@ -78,6 +85,18 @@ const schema = {
viewableBy: ['guests'],
insertableBy: ['members'],
editableBy: ['members'],
onInsert: category => {
// if no slug has been provided, generate one
const slug = category.slug || Utils.slugify(category.name);
return Utils.getUnusedSlug(Categories, slug);
},
onEdit: (modifier, category) => {
// if slug is changing
if (modifier.$set && modifier.$set.slug && modifier.$set.slug !== category.slug) {
const slug = modifier.$set.slug;
return Utils.getUnusedSlug(Categories, slug);
}
}
},
image: {
type: String,

View file

@ -0,0 +1,15 @@
/*
Default sort
*/
import { Categories } from './collection.js';
Categories.addDefaultView(terms => ({
options: {
sort: {
order: 1
}
}
}));

View file

@ -0,0 +1,37 @@
/*
Comments collection
*/
import schema from './schema.js';
import { createCollection, getDefaultResolvers, getDefaultMutations } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users';
/**
* @summary The global namespace for Comments.
* @namespace Comments
*/
export const Comments = createCollection({
collectionName: 'Comments',
typeName: 'Comment',
schema,
resolvers: getDefaultResolvers('Comments'),
mutations: getDefaultMutations('Comments'),
});
Comments.checkAccess = (currentUser, comment) => {
if (Users.isAdmin(currentUser) || Users.owns(currentUser, comment)) { // admins can always see everything, users can always see their own posts
return true;
} else if (comment.isDeleted) {
return false;
} else {
return true;
}
}

View file

@ -1,12 +1,12 @@
import Posts from "meteor/vulcan:posts";
import Users from "meteor/vulcan:users";
import { Posts } from '../posts/index.js';
import Users from 'meteor/vulcan:users';
Users.addField([
/**
Count of the user's comments
*/
{
fieldName: "commentCount",
fieldName: 'commentCount',
fieldSchema: {
type: Number,
optional: true,
@ -21,7 +21,7 @@ Posts.addField([
Count of the post's comments
*/
{
fieldName: "commentCount",
fieldName: 'commentCount',
fieldSchema: {
type: Number,
optional: true,
@ -33,7 +33,7 @@ Posts.addField([
An array containing the `_id`s of commenters
*/
{
fieldName: "commenters",
fieldName: 'commenters',
fieldSchema: {
type: Array,
optional: true,
@ -50,7 +50,7 @@ Posts.addField([
}
},
{
fieldName: "commenters.$",
fieldName: 'commenters.$',
fieldSchema: {
type: String,
optional: true

View file

@ -0,0 +1,30 @@
import { registerFragment } from 'meteor/vulcan:core';
// ----------------------------- Comments ------------------------------ //
registerFragment(`
fragment CommentsList on Comment {
# vulcan:comments
_id
postId
parentCommentId
topLevelCommentId
body
htmlBody
postedAt
# vulcan:users
userId
user {
...UsersMinimumInfo
}
# vulcan:posts
post {
_id
commentCount
commenters {
...UsersMinimumInfo
}
}
# vulcan:voting
}
`);

View file

@ -1,5 +1,11 @@
import Comments from './collection.js';
import Posts from 'meteor/vulcan:posts';
/*
Comments helpers
*/
import { Comments } from './index.js';
import { Posts } from '../posts/index.js';
import Users from 'meteor/vulcan:users';
//////////////////

View file

@ -0,0 +1,7 @@
export * from './collection.js';
import './fragments.js';
import './custom_fields.js';
import './helpers.js';
import './permissions.js';
import './views.js';

View file

@ -0,0 +1,30 @@
/*
Comments permissions
*/
import Users from 'meteor/vulcan:users';
const guestsActions = [
'comments.view'
];
Users.groups.guests.can(guestsActions);
const membersActions = [
'comments.view',
'comments.new',
'comments.edit.own',
'comments.remove.own',
'comments.upvote',
'comments.cancelUpvote',
'comments.downvote',
'comments.cancelDownvote'
];
Users.groups.members.can(membersActions);
const adminActions = [
'comments.edit.all',
'comments.remove.all'
];
Users.groups.admins.can(adminActions);

View file

@ -1,4 +1,12 @@
/*
Comments schema
*/
import Users from 'meteor/vulcan:users';
import marked from 'marked';
import { Utils } from 'meteor/vulcan:core';
/**
* @summary Comments schema
@ -97,6 +105,16 @@ const schema = {
type: String,
optional: true,
viewableBy: ['guests'],
onInsert: (comment) => {
if (comment.body) {
return Utils.sanitize(marked(comment.body));
}
},
onEdit: (modifier, comment) => {
if (modifier.$set.body) {
return Utils.sanitize(marked(modifier.$set.body));
}
}
},
/**
The comment author's name
@ -112,14 +130,6 @@ const schema = {
}
}
},
/**
Whether the comment is inactive. Inactive comments' scores gets recalculated less often
*/
inactive: {
type: Boolean,
optional: true,
viewableBy: ['guests'],
},
/**
The post's `_id`
*/
@ -184,7 +194,21 @@ const schema = {
type: String,
optional: true,
viewableBy: ['admins'],
}
},
// GraphQL only fields
pageUrl: {
type: String,
optional: true,
resolveAs: {
fieldName: 'pageUrl',
type: 'String',
resolver: (comment, args, context) => {
return context.Comments.getPageUrl(comment, true);
},
}
},
};
export default schema;

View file

@ -0,0 +1,21 @@
/*
Comments views
*/
import { Comments } from './index.js';
Comments.addView('postComments', function (terms) {
return {
selector: {postId: terms.postId},
options: {sort: {postedAt: -1}}
};
});
Comments.addView('userComments', function (terms) {
return {
selector: {userId: terms.userId},
options: {sort: {postedAt: -1}}
};
});

View file

@ -0,0 +1,65 @@
// common
import '../components/common/Footer.jsx';
import '../components/common/Header.jsx';
import '../components/common/Layout.jsx';
import '../components/common/Logo.jsx';
import '../components/common/Newsletter.jsx';
import '../components/common/NewsletterButton.jsx';
import '../components/common/SearchForm.jsx';
import '../components/common/Vote.jsx';
// posts
import '../components/posts/PostsHome.jsx';
import '../components/posts/PostsSingle.jsx';
import '../components/posts/PostsNewButton.jsx';
import '../components/posts/PostsLoadMore.jsx';
import '../components/posts/PostsNoMore.jsx';
import '../components/posts/PostsNoResults.jsx';
import '../components/posts/PostsItem.jsx';
import '../components/posts/PostsLoading.jsx';
import '../components/posts/PostsViews.jsx';
import '../components/posts/PostsList.jsx';
import '../components/posts/PostsListHeader.jsx';
import '../components/posts/PostsCategories.jsx';
import '../components/posts/PostsCommenters.jsx';
import '../components/posts/PostsPage.jsx';
import '../components/posts/PostsStats.jsx';
import '../components/posts/PostsDaily.jsx';
import '../components/posts/PostsDailyList.jsx';
import '../components/posts/PostsDay.jsx';
import '../components/posts/PostsThumbnail.jsx';
import '../components/posts/PostsEditForm.jsx';
import '../components/posts/PostsNewForm.jsx';
import '../components/posts/PostsCommentsThread.jsx';
// comments
import '../components/comments/CommentsItem.jsx';
import '../components/comments/CommentsList.jsx';
import '../components/comments/CommentsNode.jsx';
import '../components/comments/CommentsNewForm.jsx';
import '../components/comments/CommentsEditForm.jsx';
import '../components/comments/CommentsLoadMore.jsx';
// categories
import '../components/categories/CategoriesList.jsx';
import '../components/categories/CategoriesNode.jsx';
import '../components/categories/Category.jsx';
import '../components/categories/CategoriesEditForm.jsx';
import '../components/categories/CategoriesNewForm.jsx';
// users
import '../components/users/UsersSingle.jsx';
import '../components/users/UsersAccount.jsx';
import '../components/users/UsersEditForm.jsx';
import '../components/users/UsersProfile.jsx';
import '../components/users/UsersProfileCheck.jsx';
import '../components/users/UsersAvatar.jsx';
import '../components/users/UsersName.jsx';
import '../components/users/UsersMenu.jsx';
import '../components/users/UsersAccountMenu.jsx';

View file

@ -0,0 +1,6 @@
import Users from 'meteor/vulcan:users';
Users.avatar.setOptions({
'gravatarDefault': 'mm',
'defaultImageUrl': 'http://www.gravatar.com/avatar/00000000000000000000000000000000?d=mm&f=y'
});

View file

@ -0,0 +1,48 @@
import { registerFragment } from 'meteor/vulcan:core';
// ------------------------------ Vote ------------------------------ //
// note: fragment used by default on the UsersProfile fragment
registerFragment(`
fragment VotedItem on Vote {
# vulcan:voting
documentId
power
votedAt
}
`);
// ------------------------------ Users ------------------------------ //
// note: fragment used by default on UsersProfile, PostsList & CommentsList fragments
registerFragment(`
fragment UsersMinimumInfo on User {
# vulcan:users
_id
slug
username
displayName
emailHash
avatarUrl
}
`);
registerFragment(`
fragment UsersProfile on User {
# vulcan:users
...UsersMinimumInfo
createdAt
isAdmin
bio
htmlBio
twitterUsername
website
groups
karma
# vulcan:posts
postCount
# vulcan:comments
commentCount
}
`);

View file

@ -0,0 +1,88 @@
import { addStrings } from 'meteor/vulcan:core';
addStrings('en', {
'posts.new_post': 'New Post',
'posts.edit': 'Edit',
'posts.edit_success': 'Post “{title}” edited.',
'posts.delete': 'Delete',
'posts.delete_confirm': 'Delete post “{title}”?',
'posts.delete_success': 'Post “{title}” deleted.',
'posts.title': 'Title',
'posts.url': 'URL',
'posts.body': 'Body',
'posts.categories': 'Categories',
'posts.thumbnailUrl': 'Thumbnail URL',
'posts.status': 'Status',
'posts.sticky': 'Sticky',
'posts.load_more': 'Load More',
'posts.load_more_days': 'Load More Days',
'posts.no_more': 'No more posts.',
'posts.no_results': 'No posts to display.',
'posts.search': 'Search',
'posts.view': 'View',
'posts.top': 'Top',
'posts.new': 'New',
'posts.best': 'Best',
'posts.pending': 'Pending',
'posts.rejected': 'Rejected',
'posts.scheduled': 'Scheduled',
'posts.daily': 'Daily',
'posts.clear_thumbnail': 'Clear Thumbnail',
'posts.clear_thumbnail?': 'Clear thumbnail?',
'posts.enter_thumbnail_url': 'Enter URL',
'posts.created_message': 'Post created.',
'posts.rate_limit_error': 'Please wait {value} seconds before posting again.',
'posts.sign_up_or_log_in_first': 'Please sign up or log in first.',
'posts.postedAt': 'Posted at',
'posts.dateNotDefined': 'Date not defined',
'posts.subscribe': 'Subscribe',
'posts.unsubscribe': 'Unsubscribe',
'posts.subscribed': 'You have subscribed to “{name}” comments.',
'posts.unsubscribed': 'You have unsubscribed from “{name}” comments.',
'posts.subscribed_posts' : 'Posts subscribed to',
'posts.link_already_posted': 'This link has already been posted.',
'posts.max_per_day': 'Sorry you cannot submit more than {value} posts per day.',
'posts.like': 'Like',
'comments.comments': 'Comments',
'comments.count': '{count, plural, =0 {No comments} one {# comment} other {# comments}}',
'comments.count_0': 'No comments',
'comments.count_1': '1 comment',
'comments.count_2': '{count} comments',
'comments.new': 'New Comment',
'comments.no_comments': 'No comments to display.',
'comments.reply': 'Reply',
'comments.edit': 'Edit',
'comments.delete': 'Delete',
'comments.delete_confirm': 'Delete this comment?',
'comments.delete_success': 'Comment deleted.',
'comments.please_log_in': 'Please log in to comment.',
'comments.parentCommentId': 'Parent Comment ID',
'comments.topLevelCommentId': 'Top Level Comment ID',
'comments.body': 'Body',
'comments.rate_limit_error': 'Please wait {value} seconds before commenting again.',
'categories': 'Categories',
'categories.all': 'All Categories',
'categories.edit': 'Edit Category',
'categories.edit_success': 'Category “{name}” edited.',
'categories.new': 'New Category',
'categories.new_success': 'Category “{name}” created.',
'categories.delete': 'Delete Category',
'categories.name': 'Name',
'categories.description': 'Description',
'categories.order': 'Order',
'categories.slug': 'Slug',
'categories.image': 'Image',
'categories.parentId': 'Parent ID',
'categories.subscribe': 'Subscribe to this category\'s posts',
'categories.unsubscribe': 'Unsubscribe to this category\'s posts',
'categories.subscribed': 'You have subscribed to “{name}” posts.',
'categories.unsubscribed': 'You have unsubscribed from “{name}” posts.',
'categories.subscribed_categories' : 'Categories subscribed to',
'categories.delete_confirm': 'Delete category “{title}”?',
'categories.delete_success': 'Category “{name}” deleted.',
'categories.invalid': 'Invalid category',
});

View file

@ -0,0 +1,13 @@
import './voting.js';
import './fragments.js';
import './components.js';
import './config.js';
import './routes.js';
import './headtags.js';
import './i18n.js';
export { Categories } from './categories/index.js';
export { Comments } from './comments/index.js';
export { Posts } from './posts/index.js';
import './notifications/index.js';

View file

@ -0,0 +1,66 @@
import Users from 'meteor/vulcan:users';
const notificationsGroup = {
name: "notifications",
order: 2
};
// Add notifications options to user profile settings
Users.addField([
{
fieldName: 'notifications_users',
fieldSchema: {
label: 'New users',
type: Boolean,
optional: true,
defaultValue: false,
control: "checkbox",
viewableBy: ['guests'],
insertableBy: ['admins'],
editableBy: ['admins'],
group: notificationsGroup,
}
},
{
fieldName: 'notifications_posts',
fieldSchema: {
label: 'New posts',
type: Boolean,
optional: true,
defaultValue: false,
control: "checkbox",
viewableBy: ['guests'],
insertableBy: ['members'],
editableBy: ['members'],
group: notificationsGroup,
}
},
{
fieldName: 'notifications_comments',
fieldSchema: {
label: 'Comments on my posts',
type: Boolean,
optional: true,
defaultValue: false,
control: "checkbox",
viewableBy: ['guests'],
insertableBy: ['members'],
editableBy: ['members'],
group: notificationsGroup,
}
},
{
fieldName: 'notifications_replies',
fieldSchema: {
label: 'Replies to my comments',
type: Boolean,
optional: true,
defaultValue: false,
control: "checkbox",
viewableBy: ['guests'],
insertableBy: ['members'],
editableBy: ['members'],
group: notificationsGroup,
}
}
]);

View file

@ -0,0 +1,192 @@
/*
Emails
*/
import VulcanEmail from 'meteor/vulcan:email';
/*
Test
*/
VulcanEmail.addEmails({
test: {
template: "test",
path: "/email/test",
data() {
return {date: new Date()};
},
subject() {
return "This is a test";
},
}
});
/*
Users
*/
VulcanEmail.addEmails({
newUser: {
template: "newUser",
path: "/email/new-user/:_id?",
subject() {
return "A new user has been created";
},
query: `
query UsersSingleQuery($documentId: String){
UsersSingle(documentId: $documentId){
displayName
pageUrl
}
}
`
},
accountApproved: {
template: "accountApproved",
path: "/email/account-approved/:_id?",
subject() {
return "Your account has been approved.";
},
query: `
query UsersSingleQuery($documentId: String){
UsersSingle(documentId: $documentId){
displayName
}
SiteData{
title
url
}
}
`
}
});
/*
Posts
*/
const postsQuery = `
query PostsSingleQuery($documentId: String){
PostsSingle(documentId: $documentId){
title
url
pageUrl
linkUrl
htmlBody
thumbnailUrl
user{
pageUrl
displayName
}
}
}
`
const dummyPost = {title: '[title]', user: {displayName: '[user]'}};
VulcanEmail.addEmails({
newPost: {
template: "newPost",
path: "/email/new-post/:_id?",
subject(data) {
const post = _.isEmpty(data) ? dummyPost : data.PostsSingle;
return post.user.displayName+' has created a new post: '+post.title;
},
query: postsQuery
},
newPendingPost: {
template: "newPendingPost",
path: "/email/new-pending-post/:_id?",
subject(data) {
const post = _.isEmpty(data) ? dummyPost : data.PostsSingle;
return post.user.displayName+' has a new post pending approval: '+post.title;
},
query: postsQuery
},
postApproved: {
template: "postApproved",
path: "/email/post-approved/:_id?",
subject(data) {
const post = _.isEmpty(data) ? dummyPost : data.PostsSingle;
return 'Your post “'+post.title+'” has been approved';
},
query: postsQuery
}
});
/*
Comments
*/
const commentsQuery = `
query CommentsSingleQuery($documentId: String){
CommentsSingle(documentId: $documentId){
pageUrl
htmlBody
post{
pageUrl
title
}
user{
pageUrl
displayName
}
}
}
`
const dummyComment = {post: {title: '[title]'}, user: {displayName: '[user]'}};
VulcanEmail.addEmails({
newComment: {
template: "newComment",
path: "/email/new-comment/:_id?",
subject(data) {
const comment = _.isEmpty(data) ? dummyComment : data.CommentsSingle;
return comment.user.displayName+' left a new comment on your post "' + comment.post.title + '"';
},
query: commentsQuery
},
newReply: {
template: "newReply",
path: "/email/new-reply/:_id?",
subject(data) {
const comment = _.isEmpty(data) ? dummyComment : data.CommentsSingle;
return comment.user.displayName+' replied to your comment on "'+comment.post.title+'"';
},
query: commentsQuery
},
newCommentSubscribed: {
template: "newComment",
path: "/email/new-comment-subscribed/:_id?",
subject(data) {
const comment = _.isEmpty(data) ? dummyComment : data.CommentsSingle;
return comment.user.displayName+' left a new comment on "' + comment.post.title + '"';
},
query: commentsQuery
}
});

View file

@ -0,0 +1,2 @@
import './custom_fields.js';
import './emails.js';

View file

@ -0,0 +1,25 @@
/*
Admin dashboard extension
*/
import { extendFragment, addAdminColumn, addStrings } from 'meteor/vulcan:core';
import AdminUsersPosts from '../../components/admin/AdminUsersPosts';
extendFragment('UsersAdmin', `
posts(limit: 5){
...PostsPage
}
`);
addAdminColumn({
name: 'posts',
order: 50,
component: AdminUsersPosts
});
addStrings('en', {
'admin.users.posts': 'Posts',
});

View file

@ -1,15 +1,18 @@
/*
Posts collection
*/
import schema from './schema.js';
import mutations from './mutations.js';
import resolvers from './resolvers.js';
// import views from './views.js';
import { createCollection } from 'meteor/vulcan:core';
import { createCollection, getDefaultResolvers, getDefaultMutations } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users';
/**
* @summary The global namespace for Posts.
* @namespace Posts
*/
const Posts = createCollection({
export const Posts = createCollection({
collectionName: 'Posts',
@ -17,9 +20,9 @@ const Posts = createCollection({
schema,
resolvers,
resolvers: getDefaultResolvers('Posts'),
mutations,
mutations: getDefaultMutations('Posts'),
});
@ -69,6 +72,4 @@ Posts.checkAccess = (currentUser, post) => {
const status = _.findWhere(Posts.statuses, {value: post.status});
return Users.canDo(currentUser, `posts.view.${status.label}`);
}
}
export default Posts;
}

View file

@ -1,11 +1,17 @@
import Users from "meteor/vulcan:users";
/*
Custom fields on Users collection
*/
import Users from 'meteor/vulcan:users';
Users.addField([
/**
Count of the user's posts
*/
{
fieldName: "postCount",
fieldName: 'postCount',
fieldSchema: {
type: Number,
optional: true,
@ -17,13 +23,12 @@ Users.addField([
The user's associated posts (GraphQL only)
*/
{
fieldName: "posts",
fieldName: 'posts',
fieldSchema: {
type: Array,
optional: true,
viewableBy: ['guests'],
resolveAs: {
fieldName: 'posts',
arguments: 'limit: Int = 5',
type: '[Post]',
resolver: (user, { limit }, { currentUser, Users, Posts }) => {

View file

@ -1,11 +1,10 @@
import EmbedlyURL from '../components/EmbedlyURL.jsx';
import Posts from "meteor/vulcan:posts";
import { Posts } from '../posts/index.js';
Posts.addField([
{
fieldName: 'url',
fieldSchema: {
control: EmbedlyURL, // we are just extending the field url, not replacing it
control: 'EmbedURL', // we are just extending the field url, not replacing it
}
},
{
@ -44,4 +43,4 @@ Posts.addField([
viewableBy: ['guests'],
}
}
]);
]);

Some files were not shown because too many files have changed in this diff Show more