This commit is contained in:
Dominic Tracey 2017-03-29 22:09:50 -04:00
commit bc306ba703
79 changed files with 633 additions and 912 deletions

View file

@ -1,38 +1,11 @@
# see http://docs.vulcanjs.org/packages # see http://docs.vulcanjs.org/packages
############ Core Packages ############ vulcan:core
vulcan:core # core components and wrappers
vulcan:forms # auto-generated forms
vulcan:routing # routing and server-side rendering
vulcan:users # user management and permissions
############ Features Packages ############
# vulcan:email
# vulcan:posts
# vulcan:comments
# vulcan:newsletter
# vulcan:notifications
# vulcan:getting-started
# vulcan:categories
# vulcan:voting
# vulcan:events
# vulcan:embedly
# vulcan:api
# vulcan:rss
# vulcan:subscribe
############ Theme Packages ############
# vulcan:base-components # default ui components
# vulcan:base-styles # default styling
# vulcan:email-templates # default email templates for notifications
############ Language Packages ############ ############ Language Packages ############
vulcan:i18n-en-us # default language translation vulcan:i18n-en-us
############ Accounts Packages ############ ############ Accounts Packages ############
@ -43,5 +16,6 @@ accounts-password@1.3.4
############ Your Packages ############ ############ Your Packages ############
example-movies example-movies
# example-movies-full # example-instagram
# example-forum
# example-customization # example-customization

View file

@ -11,7 +11,7 @@
}, },
"dependencies": { "dependencies": {
"analytics-node": "^2.1.1", "analytics-node": "^2.1.1",
"apollo-client": "^1.0.0-rc.5", "apollo-client": "^1.0.0-rc.6",
"babel-runtime": "^6.18.0", "babel-runtime": "^6.18.0",
"bcrypt": "^0.8.7", "bcrypt": "^0.8.7",
"body-parser": "^1.15.2", "body-parser": "^1.15.2",
@ -26,12 +26,11 @@
"graphql-anywhere": "^3.0.1", "graphql-anywhere": "^3.0.1",
"graphql-date": "^1.0.2", "graphql-date": "^1.0.2",
"graphql-server-express": "^0.6.0", "graphql-server-express": "^0.6.0",
"graphql-tag": "^1.3.2", "graphql-tag": "^2.0.0",
"graphql-tools": "^0.10.1", "graphql-tools": "^0.10.1",
"graphql-type-json": "^0.1.4", "graphql-type-json": "^0.1.4",
"handlebars": "^4.0.5", "handlebars": "^4.0.5",
"history": "^3.0.0", "history": "^3.0.0",
"hoist-non-react-statics": "^1.2.0",
"html-to-text": "^2.1.0", "html-to-text": "^2.1.0",
"immutability-helper": "^2.0.0", "immutability-helper": "^2.0.0",
"intl": "^1.2.4", "intl": "^1.2.4",

View file

@ -5,12 +5,9 @@ Package.describe({
Package.onUse( function(api) { Package.onUse( function(api) {
api.use([ api.use([
'fourseven:scss', 'fourseven:scss@3.8.0',
'vulcan:core', 'example-forum',
'vulcan:base-components',
'vulcan:posts',
'vulcan:users'
]); ]);
api.mainModule('server.js', 'server'); api.mainModule('server.js', 'server');

View file

@ -0,0 +1 @@
Vulcan forum example package.

View file

@ -0,0 +1,43 @@
Package.describe({
name: "example-forum",
summary: "Telescope forum package",
version: '1.3.0',
git: "https://github.com/TelescopeJS/Telescope.git"
});
Package.onUse(function (api) {
api.versionsFrom(['METEOR@1.0']);
api.use([
// vulcan core
'vulcan:core@1.3.0',
// vulcan packages
'vulcan:posts@1.3.0',
'vulcan:comments@1.3.0',
'vulcan:voting@1.3.0',
'vulcan:accounts@1.3.0',
'vulcan:email',
'vulcan:forms',
'vulcan:newsletter',
'vulcan:notifications',
'vulcan:getting-started',
'vulcan:categories',
'vulcan:events',
'vulcan:embedly',
'vulcan:api',
'vulcan:rss',
'vulcan:subscribe',
'vulcan:base-components',
'vulcan:base-styles',
'vulcan:email-templates',
]);
// api.mainModule("lib/server.js", "server");
// api.mainModule("lib/client.js", "client");
});

View file

@ -1,7 +1,17 @@
/* /*
A component to configure the "edit comment" form. A component to configure the "edit comment" form.
Wrapped with the "withDocument" container.
Components.SmartForm props:
- collection: the collection in which to edit a document
- documentId: the id of the document to edit
- mutationFragment: the GraphQL fragment defining the data returned by the mutation
- showRemove: whether to show the "delete document" action in the form
- successCallback: what to do after the mutation succeeds
Note: `closeModal` is available as a prop because this form will be opened
in a modal popup.
*/ */

View file

@ -1,7 +1,11 @@
/* /*
An item in the comments list. An item in the comments list.
Wrapped with the "withCurrentUser" container.
Note: Comments.options.mutations.edit.check is defined in
modules/comments/mutations.js and is used both on the server when
performing the mutation, and here to check if the form link
should be displayed.
*/ */

View file

@ -3,6 +3,8 @@
List of comments. List of comments.
Wrapped with the "withList" and "withCurrentUser" containers. Wrapped with the "withList" and "withCurrentUser" containers.
All props except currentUser are passed by the withList container.
*/ */
import React, { PropTypes, Component } from 'react'; import React, { PropTypes, Component } from 'react';

View file

@ -2,14 +2,19 @@
A component to configure the "new comment" form. A component to configure the "new comment" form.
The "prefilledProps" option lets you prefill specific form fields
(in this case "picId"). This works even if the field is not actually
displayed in the form, as is the case here
(picId's "hidden" property is set to true in the Comments schema)
*/ */
import React, { PropTypes, Component } from 'react'; import React, { PropTypes, Component } from 'react';
import { Components, registerComponent, withCurrentUser, getFragment } from 'meteor/vulcan:core'; import { Components, registerComponent, getFragment } from 'meteor/vulcan:core';
import Comments from '../../modules/comments/collection.js'; import Comments from '../../modules/comments/collection.js';
const CommentsNewForm = ({currentUser, picId}) => const CommentsNewForm = ({picId}) =>
<div className="comments-new-form"> <div className="comments-new-form">
@ -21,4 +26,4 @@ const CommentsNewForm = ({currentUser, picId}) =>
</div> </div>
export default withCurrentUser(CommentsNewForm); export default CommentsNewForm;

View file

@ -1,9 +1,21 @@
/*
The Header component.
Components.ModalTrigger is a built-in Vulcan component that displays
its children in a popup triggered by either a text link, or a cusotm
component (if the "component" prop is specified).
*/
import React, { PropTypes, Component } from 'react'; import React, { PropTypes, Component } from 'react';
import { Components, withCurrentUser } from 'meteor/vulcan:core'; import { Components, withCurrentUser } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users'; import Users from 'meteor/vulcan:users';
import PicsNewForm from '../pics/PicsNewForm'; import PicsNewForm from '../pics/PicsNewForm';
const HeaderLoggedIn = ({currentUser}) => // navigation bar component when the user is logged in
const NavLoggedIn = ({currentUser}) =>
<div className="header-nav header-logged-in"> <div className="header-nav header-logged-in">
@ -26,7 +38,9 @@ const HeaderLoggedIn = ({currentUser}) =>
</div> </div>
const HeaderLoggedOut = ({currentUser}) => // navigation bar component when the user is logged out
const NavLoggedOut = ({currentUser}) =>
<div className="header-nav header-logged-out"> <div className="header-nav header-logged-out">
@ -36,6 +50,8 @@ const HeaderLoggedOut = ({currentUser}) =>
</div> </div>
// Header component
const Header = ({currentUser}) => const Header = ({currentUser}) =>
<div className="header-wrapper"> <div className="header-wrapper">
@ -47,8 +63,8 @@ const Header = ({currentUser}) =>
</h1> </h1>
{currentUser ? {currentUser ?
<HeaderLoggedIn currentUser={currentUser}/> : <NavLoggedIn currentUser={currentUser}/> :
<HeaderLoggedOut currentUser={currentUser}/> <NavLoggedOut currentUser={currentUser}/>
} }
</div> </div>

View file

@ -1,11 +1,40 @@
/*
The Layout component.
In other words, the template used to display every page in the app.
Specific pages will be displayed in place of the "children" property.
Note: the Helmet library is used to insert meta tags and link tags in the <head>
*/
import React, { PropTypes, Component } from 'react'; import React, { PropTypes, Component } from 'react';
import Helmet from 'react-helmet';
import Header from './Header.jsx'; import Header from './Header.jsx';
const links = [
// note: modal popups won't work with anything above alpha.5.
// see https://github.com/twbs/bootstrap/issues/21876#issuecomment-276181539
{
rel: 'stylesheet',
type: 'text/css',
href: 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css'
},
{
rel: 'stylesheet',
type: 'text/css',
href: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'
}
];
const Layout = ({children}) => const Layout = ({children}) =>
<div className="wrapper" id="wrapper"> <div className="wrapper" id="wrapper">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" /> <Helmet title="Vulcanstagram" link={links} />
<link />
<Header/> <Header/>

View file

@ -1,6 +1,6 @@
/* /*
A component that shows a detailed view of a single movie. A component that shows a detailed view of a single picture.
Wrapped with the "withDocument" container. Wrapped with the "withDocument" container.
*/ */

View file

@ -1,7 +1,6 @@
/* /*
A component to configure the "edit pic" form. A component to configure the "edit pic" form.
Wrapped with the "withDocument" container.
*/ */

View file

@ -1,11 +0,0 @@
import React, { PropTypes, Component } from 'react';
const PicsImage = ({imageUrl, onClick}) =>
<div className="pics-image" onClick={onClick}>
<img src={imageUrl}/>
</div>
export default PicsImage;

View file

@ -1,7 +1,6 @@
/* /*
An item in the pics list. An item in the pics list.
Wrapped with the "withCurrentUser" container.
*/ */
@ -9,15 +8,12 @@ import React, { PropTypes, Component } from 'react';
import { Components, registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
import PicsDetail from './PicsDetails.jsx'; import PicsDetail from './PicsDetails.jsx';
import PicsImage from './PicsImage.jsx';
const PicsItem = ({pic, currentUser}) => const PicsItem = ({pic, currentUser}) =>
<div className="pics-item"> <div className="pics-item">
{/* document properties */} <Components.ModalTrigger className="pics-details-modal" component={<div className="pics-image"><img src={pic.imageUrl}/></div>}>
<Components.ModalTrigger className="pics-details-modal" component={<PicsImage imageUrl={pic.imageUrl} />}>
<PicsDetail documentId={pic._id} currentUser={currentUser} /> <PicsDetail documentId={pic._id} currentUser={currentUser} />
</Components.ModalTrigger> </Components.ModalTrigger>

View file

@ -2,6 +2,9 @@
A component to configure the "new pic" form. A component to configure the "new pic" form.
We're using Pics.options.mutations.new.check (defined in modules/pics/mutations.js)
to check if the user has the proper permissions to actually insert a new picture.
*/ */
import React, { PropTypes, Component } from 'react'; import React, { PropTypes, Component } from 'react';
@ -14,13 +17,11 @@ const PicsNewForm = ({currentUser, closeModal}) =>
<div> <div>
{Pics.options.mutations.new.check(currentUser) ? {Pics.options.mutations.new.check(currentUser) ?
<div style={{marginBottom: '20px', paddingBottom: '20px', borderBottom: '1px solid #ccc'}}> <Components.SmartForm
<Components.SmartForm collection={Pics}
collection={Pics} mutationFragment={getFragment('PicsItemFragment')}
mutationFragment={getFragment('PicsItemFragment')} successCallback={closeModal}
successCallback={closeModal} /> :
/>
</div> :
null null
} }

View file

@ -13,7 +13,10 @@ import './permissions.js';
const Comments = createCollection({ const Comments = createCollection({
collectionName: 'comments', collectionName: 'Comments',
// avoid conflicts with 'comments' collection in vulcan:comments
dbCollectionName: 'commentsInstagram',
typeName: 'Comment', typeName: 'Comment',
@ -25,7 +28,16 @@ const Comments = createCollection({
}); });
Comments.addView('picComments', function (terms) { /*
Set a default results view whenever the Comments collection is queried:
- Comments are limited to those corresponding to the current picture
- They're sorted by their createdAt timestamp in ascending order
*/
Comments.addDefaultView(terms => {
return { return {
selector: {picId: terms.picId}, selector: {picId: terms.picId},
options: {sort: {createdAt: 1}} options: {sort: {createdAt: 1}}

View file

@ -1,3 +1,9 @@
/*
Declare permissions for the comments collection.
*/
import Users from 'meteor/vulcan:users'; import Users from 'meteor/vulcan:users';
const membersActions = [ const membersActions = [

View file

@ -16,13 +16,14 @@ const schema = {
type: Date, type: Date,
viewableBy: ['guests'], viewableBy: ['guests'],
autoValue: (documentOrModifier) => { autoValue: (documentOrModifier) => {
if (documentOrModifier && !documentOrModifier.$set) return new Date() // if this is an insert, set createdAt to current timestamp // if this is an insert, set createdAt to current timestamp
if (documentOrModifier && !documentOrModifier.$set) return new Date()
} }
}, },
userId: { userId: {
type: String, type: String,
viewableBy: ['guests'], viewableBy: ['guests'],
resolveAs: 'user: User', resolveAs: 'user: User', // resolve as "user" on the client
}, },
// custom properties // custom properties
@ -40,7 +41,7 @@ const schema = {
type: String, type: String,
viewableBy: ['guests'], viewableBy: ['guests'],
insertableBy: ['members'], insertableBy: ['members'],
hidden: true, hidden: true, // never show this in forms
}, },
}; };

View file

@ -10,11 +10,10 @@ import resolvers from './resolvers.js';
import './fragments.js'; import './fragments.js';
import mutations from './mutations.js'; import mutations from './mutations.js';
import './permissions.js'; import './permissions.js';
import './parameters.js';
const Pics = createCollection({ const Pics = createCollection({
collectionName: 'pics', collectionName: 'Pics',
typeName: 'Pic', typeName: 'Pic',
@ -26,4 +25,18 @@ const Pics = createCollection({
}); });
/*
Set a default results view whenever the Pics collection is queried:
- Pics are sorted by their createdAt timestamp in descending order
*/
Pics.addDefaultView(terms => {
return {
options: {sort: {createdAt: -1}}
};
});
export default Pics; export default Pics;

View file

@ -1,10 +0,0 @@
import { addCallback } from 'meteor/vulcan:core';
function sortByCreatedAt (parameters, terms) {
return {
selector: parameters.selector,
options: {...parameters.options, sort: {createdAt: -1}}
};
}
addCallback('pics.parameters', sortByCreatedAt);

View file

@ -1,3 +1,9 @@
/*
Declare permissions for the comments collection.
*/
import Users from 'meteor/vulcan:users'; import Users from 'meteor/vulcan:users';
const membersActions = [ const membersActions = [

View file

@ -4,8 +4,7 @@ A SimpleSchema-compatible JSON schema
*/ */
import { getSetting } from 'meteor/vulcan:core'; import FormsUpload from 'meteor/vulcan:forms-upload';
import Upload from 'meteor/vulcan:forms-upload';
const schema = { const schema = {
@ -19,13 +18,14 @@ const schema = {
type: Date, type: Date,
viewableBy: ['guests'], viewableBy: ['guests'],
autoValue: (documentOrModifier) => { autoValue: (documentOrModifier) => {
if (documentOrModifier && !documentOrModifier.$set) return new Date() // if this is an insert, set createdAt to current timestamp // if this is an insert, set createdAt to current timestamp
if (documentOrModifier && !documentOrModifier.$set) return new Date()
} }
}, },
userId: { userId: {
type: String, type: String,
viewableBy: ['guests'], viewableBy: ['guests'],
resolveAs: 'user: User', resolveAs: 'user: User', // resolve this field as "user" on the client
}, },
// custom properties // custom properties
@ -37,10 +37,10 @@ const schema = {
viewableBy: ['guests'], viewableBy: ['guests'],
insertableBy: ['members'], insertableBy: ['members'],
editableBy: ['members'], editableBy: ['members'],
control: Upload, control: FormsUpload, // use the FormsUpload form component
form: { form: {
options: { options: {
preset: getSetting('cloudinaryPresets').pics preset: 'vulcanstagram'
}, },
} }
}, },
@ -48,7 +48,7 @@ const schema = {
label: 'Body', label: 'Body',
type: String, type: String,
optional: true, optional: true,
control: 'textarea', control: 'textarea', // use a textarea form component
viewableBy: ['guests'], viewableBy: ['guests'],
insertableBy: ['members'], insertableBy: ['members'],
editableBy: ['members'] editableBy: ['members']
@ -61,7 +61,7 @@ const schema = {
optional: true, optional: true,
viewableBy: ['guests'], viewableBy: ['guests'],
hidden: true, hidden: true,
resolveAs: 'commentsCount: Float' resolveAs: 'commentsCount: Float' // resolve as commentCount on the client
} }
}; };

File diff suppressed because one or more lines are too long

View file

@ -5,16 +5,20 @@ Package.describe({
Package.onUse(function (api) { Package.onUse(function (api) {
api.use([ api.use([
// vulcan core
'vulcan:core', 'vulcan:core',
// vulcan packages
'vulcan:forms', 'vulcan:forms',
'vulcan:routing',
'vulcan:accounts', 'vulcan:accounts',
'vulcan:forms-upload', 'vulcan:forms-upload',
'fourseven:scss', // third-party packages
'fourseven:scss@3.8.0',
]); ]);
api.addFiles('lib/stylesheets/bootstrap.min.css');
api.addFiles('lib/stylesheets/style.scss'); api.addFiles('lib/stylesheets/style.scss');
api.addAssets([ api.addAssets([

View file

@ -1 +0,0 @@
Vulcan demo package.

View file

@ -1 +0,0 @@
import '../modules/index.js';

View file

@ -1,34 +0,0 @@
/*
A component that shows a detailed view of a single movie.
Wrapped with the "withDocument" container.
*/
import React, { PropTypes, Component } from 'react';
import Movies from '../../modules/movies/collection.js';
import { withDocument, registerComponent } from 'meteor/vulcan:core';
const MoviesDetails = props => {
const movie = props.document;
if (props.loading) {
return <p>Loading</p>
} else {
return (
<div>
<h2>{movie.name} ({movie.year})</h2>
<p>Reviewed by <strong>{movie.user && movie.user.displayName}</strong> on {movie.createdAt}</p>
<p>{movie.review}</p>
{movie.privateComments ? <p><strong>PRIVATE</strong>: {movie.privateComments}</p>: null}
</div>
)
}
}
const options = {
collection: Movies,
queryName: 'moviesSingleQuery',
fragmentName: 'MoviesDetailsFragment',
};
registerComponent('MoviesDetails', MoviesDetails, withDocument(options));

View file

@ -1,23 +0,0 @@
/*
A component to configure the "edit movie" form.
Wrapped with the "withDocument" container.
*/
import React, { PropTypes, Component } from 'react';
import { Components, registerComponent, getFragment } from "meteor/vulcan:core";
import Movies from '../../modules/movies/collection.js';
const MoviesEditForm = props =>
<Components.SmartForm
collection={Movies}
documentId={props.documentId}
mutationFragment={getFragment('MoviesDetailsFragment')}
showRemove={true}
successCallback={document => {
props.closeModal();
}}
/>
registerComponent('MoviesEditForm', MoviesEditForm);

View file

@ -1,61 +0,0 @@
/*
An item in the movies list.
Wrapped with the "withCurrentUser" container.
*/
import React, { PropTypes, Component } from 'react';
import { Button } from 'react-bootstrap';
import { Components, registerComponent, ModalTrigger } from 'meteor/vulcan:core';
import Movies from '../../modules/movies/collection.js';
class MoviesItem extends Component {
renderDetails() {
const movie = this.props.movie;
return (
<div style={{display: 'inline-block', marginRight: '5px'}}>
<ModalTrigger label="View Details">
<Components.MoviesDetails documentId={movie._id}/>
</ModalTrigger>
</div>
)
}
renderEdit() {
const movie = this.props.movie;
return (
<div style={{display: 'inline-block', marginRight: '5px'}}>
<ModalTrigger label="Edit Movie" >
<Components.MoviesEditForm currentUser={this.props.currentUser} documentId={movie._id} />
</ModalTrigger>
</div>
)
}
render() {
const movie = this.props.movie;
return (
<div key={movie.name} style={{paddingBottom: "15px",marginBottom: "15px", borderBottom: "1px solid #ccc"}}>
<h2>{movie.name} ({movie.year})</h2>
<p>By <strong>{movie.user && movie.user.displayName}</strong></p>
<div className="item-actions">
{this.renderDetails()}
&nbsp;|&nbsp;
{Movies.options.mutations.edit.check(this.props.currentUser, movie) ? this.renderEdit() : null}
</div>
</div>
)
}
}
registerComponent('MoviesItem', MoviesItem);

View file

@ -1,61 +0,0 @@
/*
List of movies.
Wrapped with the "withList" and "withCurrentUser" containers.
*/
import React, { PropTypes, Component } from 'react';
import { Button } from 'react-bootstrap';
import Movies from '../../modules/movies/collection.js';
import { Components, registerComponent, ModalTrigger, withList, withCurrentUser } from 'meteor/vulcan:core';
const LoadMore = props => <a href="#" className="load-more button button--primary" onClick={e => {e.preventDefault(); props.loadMore();}}>Load More ({props.count}/{props.totalCount})</a>
class MoviesList extends Component {
renderNew() {
const component = (
<div className="add-movie">
<ModalTrigger
title="Add Movie"
component={<Button bsStyle="primary">Add Movie</Button>}
>
<Components.MoviesNewForm />
</ModalTrigger>
<hr/>
</div>
)
return !!this.props.currentUser ? component : null;
}
render() {
const canCreateNewMovie = Movies.options.mutations.new.check(this.props.currentUser);
if (this.props.loading) {
return <Components.Loading />
} else {
const hasMore = this.props.totalCount > this.props.results.length;
return (
<div className="movies">
{canCreateNewMovie ? this.renderNew() : null}
{this.props.results.map(movie => <Components.MoviesItem key={movie._id} movie={movie} currentUser={this.props.currentUser} />)}
{hasMore ? <LoadMore {...this.props}/> : <p>No more movies</p>}
</div>
)
}
}
}
const options = {
collection: Movies,
queryName: 'moviesListQuery',
fragmentName: 'MoviesItemFragment',
limit: 5,
};
registerComponent('MoviesList', MoviesList, withList(options), withCurrentUser);

View file

@ -1,20 +0,0 @@
/*
A component to configure the "new movie" form.
*/
import React, { PropTypes, Component } from 'react';
import Movies from '../../modules/movies/collection.js';
import { Components, registerComponent, withMessages, getFragment } from 'meteor/vulcan:core';
const MoviesNewForm = props =>
<Components.SmartForm
collection={Movies}
mutationFragment={getFragment('MoviesItemFragment')}
successCallback={document => {
props.closeModal();
}}
/>
registerComponent('MoviesNewForm', MoviesNewForm, withMessages);

View file

@ -1,23 +0,0 @@
/*
Wrapper for the Movies components
*/
import React, { PropTypes, Component } from 'react';
import { Components, registerComponent } from 'meteor/vulcan:core';
const MoviesWrapper = () =>
<div className="wrapper framework-demo" style={{maxWidth: '500px', margin: 'auto'}}>
<div className="header" style={{padding: '20px 0', marginBottom: '20px', borderBottom: '1px solid #ccc'}}>
<Components.AccountsLoginForm />
</div>
<div className="main">
<Components.MoviesList />
</div>
</div>
registerComponent('MoviesWrapper', MoviesWrapper);

View file

@ -1,6 +0,0 @@
import '../components/movies/MoviesDetails.jsx';
import '../components/movies/MoviesEditForm.jsx';
import '../components/movies/MoviesItem.jsx';
import '../components/movies/MoviesList.jsx';
import '../components/movies/MoviesNewForm.jsx';
import '../components/movies/MoviesWrapper.jsx';

View file

@ -1,12 +0,0 @@
/*
Add strings for internationalization.
*/
import { addStrings } from 'meteor/vulcan:core';
addStrings('en', {
'movies.delete': "Delete Movie",
'movies.delete_confirm': "Delete Movie?"
});

View file

@ -1,15 +0,0 @@
// The main Movies collection
import MoviesImport from './movies/collection.js';
// Text strings used in the UI
import './i18n.js';
// React components
import './components.js';
// Routes
import './routes.js';
// Add Movies collection to global Meteor namespace (optional)
Movies = MoviesImport;

View file

@ -1,35 +0,0 @@
/*
The main Movies collection definition file.
*/
import { createCollection } from 'meteor/vulcan:core';
import schema from './schema.js';
import resolvers from './resolvers.js';
import mutations from './mutations.js';
// Groups & user permissions
import './permissions.js';
// GraphQL fragments used to query for data
import './fragments.js';
// Sorting & filtering parameters
import './parameters.js';
const Movies = createCollection({
collectionName: 'movies',
typeName: 'Movie',
schema,
resolvers,
mutations,
});
export default Movies;

View file

@ -1,35 +0,0 @@
/*
Register the two GraphQL fragments used to query for data
*/
import { registerFragment } from 'meteor/vulcan:core';
registerFragment(`
fragment MoviesItemFragment on Movie {
_id
name
year
createdAt
userId
user {
displayName
}
}
`);
registerFragment(`
fragment MoviesDetailsFragment on Movie {
_id
name
createdAt
year
review
privateComments
userId
user {
displayName
}
}
`);

View file

@ -1,104 +0,0 @@
/*
Define the three default mutations:
- new (e.g.: moviesNew(document: moviesInput) : Movie )
- edit (e.g.: moviesEdit(documentId: String, set: moviesInput, unset: moviesUnset) : Movie )
- remove (e.g.: moviesRemove(documentId: String) : Movie )
Each mutation has:
- A name
- A check function that takes the current user and (optionally) the document affected
- The actual mutation
*/
import { newMutation, editMutation, removeMutation, Utils } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users';
const performCheck = (mutation, user, document) => {
if (!mutation.check(user, document)) throw new Error(Utils.encodeIntlError({id: `app.mutation_not_allowed`, value: `"${mutation.name}" on _id "${document._id}"`}));
}
const mutations = {
new: {
name: 'moviesNew',
check(user) {
if (!user) return false;
return Users.canDo(user, 'movies.new');
},
mutation(root, {document}, context) {
performCheck(this, context.currentUser, document);
return newMutation({
collection: context.Movies,
document: document,
currentUser: context.currentUser,
validate: true,
context,
});
},
},
edit: {
name: 'moviesEdit',
check(user, document) {
if (!user || !document) return false;
return Users.owns(user, document) ? Users.canDo(user, 'movies.edit.own') : Users.canDo(user, `movies.edit.all`);
},
mutation(root, {documentId, set, unset}, context) {
const document = context.Movies.findOne(documentId);
performCheck(this, context.currentUser, document);
return editMutation({
collection: context.Movies,
documentId: documentId,
set: set,
unset: unset,
currentUser: context.currentUser,
validate: true,
context,
});
},
},
remove: {
name: 'moviesRemove',
check(user, document) {
if (!user || !document) return false;
return Users.owns(user, document) ? Users.canDo(user, 'movies.remove.own') : Users.canDo(user, `movies.remove.all`);
},
mutation(root, {documentId}, context) {
const document = context.Movies.findOne(documentId);
performCheck(this, context.currentUser, document);
return removeMutation({
collection: context.Movies,
documentId: documentId,
currentUser: context.currentUser,
validate: true,
context,
});
},
},
};
export default mutations;

View file

@ -1,19 +0,0 @@
/*
Add a new parameter callback that sorts movies by 'createdAt' property.
We use a callback instead of defining the sort in the resolver so that
the same sort can be used on the client, too.
*/
import { addCallback } from 'meteor/vulcan:core';
function sortByCreatedAt (parameters, terms) {
return {
selector: parameters.selector,
options: {...parameters.options, sort: {createdAt: -1}}
};
}
addCallback("movies.parameters", sortByCreatedAt);

View file

@ -1,14 +0,0 @@
import Users from 'meteor/vulcan:users';
const membersActions = [
'movies.new',
'movies.edit.own',
'movies.remove.own',
];
Users.groups.members.can(membersActions);
const adminActions = [
'movies.edit.all',
'movies.remove.all'
];
Users.groups.admins.can(adminActions);

View file

@ -1,62 +0,0 @@
/*
Three resolvers are defined:
- list (e.g.: moviesList(terms: JSON, offset: Int, limit: Int) )
- single (e.g.: moviesSingle(_id: String) )
- listTotal (e.g.: moviesTotal )
*/
import { GraphQLSchema } from 'meteor/vulcan:core';
// add the "user" resolver for the Movie type separately
const movieResolver = {
Movie: {
user(movie, args, context) {
return context.Users.findOne({ _id: movie.userId }, { fields: context.getViewableFields(context.currentUser, context.Users) });
},
},
};
GraphQLSchema.addResolvers(movieResolver);
// basic list, single, and total query resolvers
const resolvers = {
list: {
name: 'moviesList',
resolver(root, {terms = {}}, context, info) {
let {selector, options} = context.Movies.getParameters(terms);
options.limit = (terms.limit < 1 || terms.limit > 100) ? 100 : terms.limit;
options.fields = context.getViewableFields(context.currentUser, context.Movies);
return context.Movies.find(selector, options).fetch();
},
},
single: {
name: 'moviesSingle',
resolver(root, {documentId}, context) {
const document = context.Movies.findOne({_id: documentId});
return _.pick(document, _.keys(context.getViewableFields(context.currentUser, context.Movies, document)));
},
},
total: {
name: 'moviesTotal',
resolver(root, {terms = {}}, context) {
let {selector, options} = context.Movies.getParameters(terms);
return context.Movies.find(selector, options).count();
},
}
};
export default resolvers;

View file

@ -1,63 +0,0 @@
/*
A SimpleSchema-compatible JSON schema
*/
import Users from 'meteor/vulcan:users';
// define schema
const schema = {
_id: {
type: String,
optional: true,
viewableBy: ['guests'],
},
name: {
label: 'Name',
type: String,
viewableBy: ['guests'],
insertableBy: ['members'],
editableBy: ['members'],
},
createdAt: {
type: Date,
viewableBy: ['guests'],
autoValue: (documentOrModifier) => {
if (documentOrModifier && !documentOrModifier.$set) return new Date() // if this is an insert, set createdAt to current timestamp
}
},
year: {
label: 'Year',
type: String,
optional: true,
viewableBy: ['guests'],
insertableBy: ['members'],
editableBy: ['members'],
},
review: {
label: 'Review',
type: String,
control: 'textarea',
viewableBy: ['guests'],
insertableBy: ['members'],
editableBy: ['members']
},
privateComments: {
label: 'Private Comments',
type: String,
optional: true,
control: 'textarea',
viewableBy: Users.owns,
insertableBy: ['members'],
editableBy: ['members']
},
userId: {
type: String,
optional: true,
viewableBy: ['guests'],
resolveAs: 'user: User',
}
};
export default schema;

View file

@ -1,4 +0,0 @@
import { addRoute, getComponent } from 'meteor/vulcan:core';
// add new "/movies" route that loads the MoviesWrapper component
addRoute({ name: 'movies', path: '/', componentName: 'MoviesWrapper' });

View file

@ -1,2 +0,0 @@
import '../modules/index.js';
import './seed.js';

View file

@ -1,80 +0,0 @@
/*
Seed the database with some dummy content.
*/
import Movies from '../modules/movies/collection.js';
import Users from 'meteor/vulcan:users';
import { newMutation } from 'meteor/vulcan:core';
const seedData = [
{
name: 'Star Wars',
year: '1973',
review: `A classic.`,
privateComments: `Actually, I don't really like Star Wars…`
},
{
name: 'Die Hard',
year: '1987',
review: `A must-see if you like action movies.`,
privateComments: `I love Bruce Willis so much!`
},
{
name: 'Terminator',
year: '1983',
review: `Once again, Schwarzenegger shows why he's the boss.`,
privateComments: `Terminator is my favorite movie ever. `
},
{
name: 'Jaws',
year: '1971',
review: 'The original blockbuster.',
privateComments: `I'm scared of sharks…`
},
{
name: 'Die Hard II',
year: '1991',
review: `Another classic.`
},
{
name: 'Rush Hour',
year: '1993',
review: `Jackie Chan at his best.`,
},
{
name: 'Citizen Kane',
year: '1943',
review: `A disappointing lack of action sequences.`,
},
{
name: 'Commando',
year: '1983',
review: 'A good contender for highest kill count ever.',
},
];
Meteor.startup(function () {
if (Users.find().fetch().length === 0) {
Accounts.createUser({
username: 'DemoUser',
email: 'dummyuser@telescopeapp.org',
profile: {
isDummy: true
}
});
}
const currentUser = Users.findOne();
if (Movies.find().fetch().length === 0) {
seedData.forEach(document => {
newMutation({
action: 'movies.new',
collection: Movies,
document: document,
currentUser: currentUser,
validate: false
});
});
}
});

File diff suppressed because one or more lines are too long

View file

@ -1,23 +0,0 @@
Package.describe({
name: 'example-movies-full',
});
Package.onUse(function (api) {
api.use([
'vulcan:core',
'vulcan:forms',
'vulcan:routing',
'vulcan:accounts',
]);
api.addFiles('lib/stylesheets/bootstrap.min.css', 'client');
api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client');
api.export([
'Movies',
], ['client', 'server']);
});

View file

@ -1,7 +1,6 @@
/* /*
A component to configure the "edit movie" form. A component to configure the "edit movie" form.
Wrapped with the "withDocument" container.
*/ */

View file

@ -5,10 +5,14 @@ Package.describe({
Package.onUse(function (api) { Package.onUse(function (api) {
api.use([ api.use([
// vulcan core
'vulcan:core', 'vulcan:core',
// vulcan packages
'vulcan:forms', 'vulcan:forms',
'vulcan:routing',
'vulcan:accounts', 'vulcan:accounts',
]); ]);
api.addFiles('lib/stylesheets/bootstrap.min.css'); api.addFiles('lib/stylesheets/bootstrap.min.css');

View file

@ -8,6 +8,9 @@ Package.describe({
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.3'); api.versionsFrom('1.3');
api.use('vulcan:core@1.3.0');
api.use('ecmascript'); api.use('ecmascript');
api.use('tracker'); api.use('tracker');
api.use('underscore'); api.use('underscore');

View file

@ -44,7 +44,9 @@ class HeadTags extends Component {
// add <link /> markup specific to the page rendered // add <link /> markup specific to the page rendered
const link = Headtags.link.concat([ const link = Headtags.link.concat([
{ rel: "canonical", href: Utils.getSiteUrl() }, { rel: "canonical", href: Utils.getSiteUrl() },
{ rel: "shortcut icon", href: getSetting("faviconUrl", "/img/favicon.ico") } { rel: "shortcut icon", href: getSetting("faviconUrl", "/img/favicon.ico") },
{ rel: 'stylesheet', type: 'text/css', href: 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css' },
{ rel: 'stylesheet', type: 'text/css', href: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css' },
]); ]);
return ( return (

View file

@ -4,11 +4,10 @@ import React, { PropTypes, Component } from 'react';
import { intlShape } from 'react-intl'; import { intlShape } from 'react-intl';
import { withRouter } from 'react-router' import { withRouter } from 'react-router'
const PostsNewForm = (props, context) => { const PostsNewForm = (props, context) =>
return ( <Components.ShowIf
<Components.ShowIf
check={Posts.options.mutations.new.check} check={Posts.options.mutations.new.check}
failureComponent={<Components.UsersAccountForm />} failureComponent={<Components.AccountsLoginForm />}
> >
<div className="posts-new-form"> <div className="posts-new-form">
<Components.SmartForm <Components.SmartForm
@ -22,8 +21,6 @@ const PostsNewForm = (props, context) => {
/> />
</div> </div>
</Components.ShowIf> </Components.ShowIf>
);
};
PostsNewForm.propTypes = { PostsNewForm.propTypes = {
closeModal: React.PropTypes.func, closeModal: React.PropTypes.func,

View file

@ -16,9 +16,6 @@ Package.onUse(function (api) {
'vulcan:comments@1.3.0', 'vulcan:comments@1.3.0',
'vulcan:voting@1.3.0', 'vulcan:voting@1.3.0',
'vulcan:accounts@1.3.0', 'vulcan:accounts@1.3.0',
// third-party packages
'fortawesome:fontawesome@4.5.0',
]); ]);
api.mainModule("lib/server.js", "server"); api.mainModule("lib/server.js", "server");

View file

@ -11,11 +11,11 @@ Package.onUse(function (api) {
api.use([ api.use([
'vulcan:core@1.3.0', 'vulcan:core@1.3.0',
'fourseven:scss', 'fourseven:scss@3.8.0',
]); ]);
api.addFiles([ api.addFiles([
'lib/stylesheets/bootstrap.css', // 'lib/stylesheets/bootstrap.css',
'lib/stylesheets/main.scss' 'lib/stylesheets/main.scss'
], ['client']); ], ['client']);

View file

@ -13,7 +13,7 @@ export {
// fragments // fragments
Fragments, registerFragment, getFragment, getFragmentName, extendFragment, Fragments, registerFragment, getFragment, getFragmentName, extendFragment,
// graphql // graphql
GraphQLSchema, addGraphQLSchema, addGraphQLQuery, addGraphQLMutation, addGraphQLResolvers, addToGraphQLContext, GraphQLSchema, addGraphQLSchema, addGraphQLQuery, addGraphQLMutation, addGraphQLResolvers, removeGraphQLResolver, addToGraphQLContext,
// headtags // headtags
Headtags, Headtags,
// inject data // inject data

View file

@ -12,6 +12,7 @@ Package.onUse(function(api) {
api.use([ api.use([
'vulcan:lib@1.3.0', 'vulcan:lib@1.3.0',
'vulcan:users@1.3.0', 'vulcan:users@1.3.0',
'vulcan:routing@1.3.0'
]); ]);
api.imply([ api.imply([

View file

@ -11,7 +11,7 @@ Package.onUse(function (api) {
api.use([ api.use([
'fourseven:scss', 'fourseven:scss@3.8.0',
// Vulcan packages // Vulcan packages

View file

@ -10,7 +10,7 @@ Package.onUse(function (api) {
api.versionsFrom(['METEOR@1.0']); api.versionsFrom(['METEOR@1.0']);
api.use([ api.use([
'vulcan:lib@1.3.0' 'vulcan:core@1.3.0'
]); ]);
api.mainModule("lib/server.js", "server"); api.mainModule("lib/server.js", "server");

View file

@ -12,7 +12,7 @@ Package.onUse( function(api) {
api.use([ api.use([
'vulcan:core@1.3.0', 'vulcan:core@1.3.0',
'vulcan:posts@1.3.0', 'vulcan:posts@1.3.0',
'fourseven:scss' 'fourseven:scss@3.8.0'
]); ]);
api.addFiles([ api.addFiles([

View file

@ -0,0 +1,153 @@
# nova-upload
🏖🔭 Vulcan package extending `vulcan:forms` to upload images to Cloudinary from a drop zone.
![Screenshot](https://res.cloudinary.com/xavcz/image/upload/v1471534203/Capture_d_e%CC%81cran_2016-08-17_14.22.14_ehwv0d.png)
Want to add this to your Vulcan instance? Read below:
# Installation
### 1. Meteor package
I would recommend that you clone this repo in your vulcan's `/packages` folder.
Then, open the `.meteor/packages` file and add at the end of the **Optional packages** section:
`xavcz:nova-forms-upload`
> **Note:** This is the version for Nova 1.0.0, running with GraphQL. *If you are looking for a version compatible with Nova "classic", you'll need to change the package's branch, like below. Then, refer to [the README for `nova-forms-upload` on Nova Classic](https://github.com/xavcz/nova-forms-upload/blob/nova-classic/README.md#installation)*
```bash
# only for Nova classic users (v0.27.5)
cd nova-forms-upload
git checkout nova-classic
```
### 2. NPM dependency
This package depends on the awesome `react-dropzone` ([repo](https://github.com/okonet/react-dropzone)), you need to install the dependency:
```
npm install react-dropzone isomorphic-fetch
```
### 3. Cloudinary account
Create a [Cloudinary account](https://cloudinary.com) if you don't have one.
The upload to Cloudinary relies on **unsigned upload**:
> Unsigned upload is an option for performing upload directly from a browser or mobile application with no authentication signature, and without going through your servers at all. However, for security reasons, not all upload parameters can be specified directly when performing unsigned upload calls.
Unsigned upload options are controlled by [an upload preset](http://cloudinary.com/documentation/upload_images#upload_presets), so in order to use this feature you first need to enable unsigned uploading for your Cloudinary account from the [Upload Settings](https://cloudinary.com/console/settings/upload) page.
When creating your **preset**, you can define image transformations. I recommend to set something like 200px width & height, fill mode and auto quality. Once created, you will get a preset id.
It may look like this:
![Screenshot-Cloudinary](https://res.cloudinary.com/xavcz/image/upload/v1471534183/Capture_d_e%CC%81cran_2016-08-18_17.07.52_tr9uoh.png)
### 4. Nova Settings
Edit your `settings.json` and add inside the `public: { ... }` block the following entries with your own credentials:
```json
public: {
"cloudinaryCloudName": "YOUR_APP_NAME",
"cloudinaryPresets": {
"avatar": "YOUR_PRESET_ID",
"posts": "THE_SAME_OR_ANOTHER_PRESET_ID"
}
}
```
Picture upload in Nova is now enabled! Easy-peasy, right? 👯
### 5. Your custom package & custom fields
Make your custom package depends on this package: open `package.js` in your custom package and add `xavcz:nova-forms-upload` as a dependency, near by the other `nova:xxx` packages.
You can now use the `Upload` component as a classic form extension with [custom fields](https://www.youtube.com/watch?v=1yTT48xaSy8) like `nova:forms-tags` or `nova:embedly`.
**⚠️ Note:** Don't forget to update your query fragments wherever needed after defining your custom fields, else they will never be available!
## Image for posts
Let's say you want to enhance your posts with a custom image. In your custom package, your new custom field could look like this:
```js
// ... your imports
import { getComponent, getSetting } from 'meteor/nova:lib';
import Posts from 'meteor/nova:posts';
// extends Posts schema with a new field: 'image' 🏖
Posts.addField({
fieldName: 'image',
fieldSchema: {
type: String,
optional: true,
control: getComponent('Upload'),
insertableBy: ['members'],
editableBy: ['members'],
viewableBy: ['guests'],
form: {
options: {
preset: getSetting('cloudinaryPresets').posts // this setting refers to the transformation you want to apply to the image
},
}
}
});
```
## Avatar for users
Let's say you want to enable your users to upload their own avatar. In your custom package, your new custom field could look like this:
```js
// ... your imports
import { getComponent, getSetting } from 'meteor/nova:lib';
import Users from 'meteor/nova:users';
// extends Users schema with a new field: 'avatar' 👁
Users.addField({
fieldName: 'avatar',
fieldSchema: {
type: String,
optional: true,
control: getComponent('Upload'),
insertableBy: ['members'],
editableBy: ['members'],
viewableBy: ['guests'],
preload: true, // ⚠️ will preload the field for the current user!
form: {
options: {
preset: getSetting('cloudinaryPresets').avatar // this setting refers to the transformation you want to apply to the image
},
}
}
});
```
Adding the opportunity to upload an avatar comes with a trade-off: you also need to extend the behavior of the `Users.avatar` methods. You can do this by adding this snippet, in `custom_fields.js` for instance:
```js
const originalAvatarConstructor = Users.avatar;
// extends the Users.avatar function
Users.avatar = {
...originalAvatarConstructor,
getUrl(user) {
url = originalAvatarConstructor.getUrl(user);
return !!user && user.avatar ? user.avatar : url;
},
};
```
Now, you also need to update the query fragments related to `User` when you want the custom avatar to show up :)
## S3? Google Cloud?
Feel free to contribute to add new features and flexibility to this package :)
You are welcome to come chat about it [on the Nova Slack chatroom](http://slack.telescopeapp.org)
## What about `nova:cloudinary` ?
This package and `nova:cloudinary` share a settings in common: `cloudinaryCloudName`. They are fully compatible.
Happy hacking! 🚀

View file

@ -0,0 +1,123 @@
import { Components, getSetting, registerComponent } from 'meteor/vulcan:lib';
import React, { PropTypes, Component } from 'react';
import Dropzone from 'react-dropzone';
import 'isomorphic-fetch'; // patch for browser which don't have fetch implemented
class Upload extends Component {
constructor(props) {
super(props);
this.onDrop = this.onDrop.bind(this);
this.clearImage = this.clearImage.bind(this);
this.state = {
preview: '',
uploading: false,
value: props.value || '',
}
}
componentWillMount() {
this.context.addToAutofilledValues({[this.props.name]: this.props.value || ''});
}
onDrop(files) {
console.log(this)
// set the component in upload mode with the preview
this.setState({
preview: files[0].preview,
uploading: true,
value: '',
});
// request url to cloudinary
const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${getSetting("cloudinaryCloudName")}/upload`;
// request body
const body = new FormData();
body.append("file", files[0]);
body.append("upload_preset", this.props.options.preset);
// post request to cloudinary
fetch(cloudinaryUrl, {
method: "POST",
body,
})
.then(res => res.json()) // json-ify the readable strem
.then(body => {
// use the https:// url given by cloudinary
const avatarUrl = body.secure_url;
// set the uploading status to false
this.setState({
preview: '',
uploading: false,
value: avatarUrl,
});
// tell vulcanForm to catch the value
this.context.addToAutofilledValues({[this.props.name]: avatarUrl});
})
.catch(err => console.log("err", err));
}
clearImage(e) {
e.preventDefault();
this.context.addToAutofilledValues({[this.props.name]: ''});
this.setState({
preview: '',
value: '',
});
}
render() {
const { uploading, preview, value } = this.state;
// show the actual uploaded image or the preview
const image = preview || value;
return (
<div className="form-group row">
<label className="control-label col-sm-3">{this.props.label}</label>
<div className="col-sm-9">
<div className="upload-field">
<Dropzone ref="dropzone"
multiple={false}
onDrop={this.onDrop}
accept="image/*"
className="dropzone-base"
activeClassName="dropzone-active"
rejectClassName="dropzone-reject"
>
<div>Drop an image here, or click to select an image to upload.</div>
</Dropzone>
{image ?
<div className="upload-state">
{uploading ? <span>Uploading... Preview:</span> : null}
{value ? <a onClick={this.clearImage}><Components.Icon name="close"/> Remove image</a> : null}
<img style={{height: 120}} src={image} />
</div>
: null}
</div>
</div>
</div>
);
}
}
Upload.propTypes = {
name: React.PropTypes.string,
value: React.PropTypes.any,
label: React.PropTypes.string
};
Upload.contextTypes = {
addToAutofilledValues: React.PropTypes.func,
}
registerComponent('Upload', Upload);
export default Upload;

View file

@ -0,0 +1,30 @@
.upload-field {
display: flex;
align-items: center;
justify-content: flex-start;
}
.dropzone-base {
border: 4px dashed #ccc;
padding: 30px;
transition: "all 0.5s";
width: 300px;
cursor: pointer;
color: #ccc;
}
.dropzone-active {
border: #4FC47F 4px solid;
}
.dropzone-reject {
border: #DD3A0A 4px solid;
}
.upload-state {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
flex-basis: 300px;
}

View file

@ -0,0 +1,3 @@
import Upload from "./Upload.jsx";
export default Upload;

View file

@ -0,0 +1,24 @@
Package.describe({
name: "vulcan:forms-upload",
summary: "Vulcan package extending vulcan:forms to upload images to Cloudinary from a drop zone.",
version: "1.3.0",
git: 'https://github.com/xavcz/nova-forms-upload.git'
});
Package.onUse( function(api) {
api.versionsFrom("METEOR@1.0");
api.use([
'vulcan:core@1.3.0',
'vulcan:forms@1.3.0',
'fourseven:scss@3.8.0'
]);
api.addFiles([
"lib/Upload.scss"
], "client");
api.mainModule("lib/modules.js", ["client", "server"]);
});

View file

@ -1,40 +0,0 @@
import Posts from "meteor/vulcan:posts";
import Comments from "meteor/vulcan:comments";
import Users from 'meteor/vulcan:users';
import { addCallback } from 'meteor/vulcan:core';
Users.addField({
fieldName: 'isDummy',
fieldSchema: {
type: Boolean,
optional: true,
hidden: true // never show this
}
});
Posts.addField({
fieldName: 'dummySlug',
fieldSchema: {
type: String,
optional: true,
hidden: true // never show this
}
});
Posts.addField({
fieldName: 'isDummy',
fieldSchema: {
type: Boolean,
optional: true,
hidden: true // never show this
}
});
Comments.addField({
fieldName: 'isDummy',
fieldSchema: {
type: Boolean,
optional: true,
hidden: true // never show this
}
});

View file

@ -5,6 +5,27 @@ import Comments from "meteor/vulcan:comments";
import Users from 'meteor/vulcan:users'; import Users from 'meteor/vulcan:users';
import Events from "meteor/vulcan:events"; import Events from "meteor/vulcan:events";
const dummyFlag = {
fieldName: 'isDummy',
fieldSchema: {
type: Boolean,
optional: true,
hidden: true
}
}
Users.addField(dummyFlag);
Posts.addField(dummyFlag);
Comments.addField(dummyFlag);
Posts.addField({
fieldName: 'dummySlug',
fieldSchema: {
type: String,
optional: true,
hidden: true // never show this
}
});
var toTitleCase = function (str) { var toTitleCase = function (str) {
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}; };
@ -58,6 +79,7 @@ var createComment = function (slug, username, body, parentBody) {
}; };
var createDummyUsers = function () { var createDummyUsers = function () {
console.log('// inserting dummy users…');
Accounts.createUser({ Accounts.createUser({
username: 'Bruce', username: 'Bruce',
email: 'dummyuser1@telescopeapp.org', email: 'dummyuser1@telescopeapp.org',
@ -82,6 +104,7 @@ var createDummyUsers = function () {
}; };
var createDummyPosts = function () { var createDummyPosts = function () {
console.log('// inserting dummy posts');
createPost("read_this_first", moment().toDate(), "Bruce", "telescope.png"); createPost("read_this_first", moment().toDate(), "Bruce", "telescope.png");
@ -96,6 +119,7 @@ var createDummyPosts = function () {
}; };
var createDummyComments = function () { var createDummyComments = function () {
console.log('// inserting dummy comments…');
createComment("read_this_first", "Bruce", "What an awesome app!"); createComment("read_this_first", "Bruce", "What an awesome app!");
@ -109,32 +133,21 @@ var createDummyComments = function () {
}; };
var deleteDummyContent = function () { const deleteDummyContent = function () {
Users.remove({'profile.isDummy': true}); Users.remove({'profile.isDummy': true});
Posts.remove({isDummy: true}); Posts.remove({isDummy: true});
Comments.remove({isDummy: true}); Comments.remove({isDummy: true});
}; };
Meteor.methods({
addGettingStartedContent: function () {
if (Users.isAdmin(Meteor.user())) {
createDummyUsers();
createDummyPosts();
createDummyComments();
}
},
removeGettingStartedContent: function () {
if (Users.isAdmin(Meteor.user()))
deleteDummyContent();
}
});
Meteor.startup(function () { Meteor.startup(function () {
// insert dummy content only if createDummyContent hasn't happened and there aren't any posts or users in the db // insert dummy content only if createDummyContent hasn't happened and there aren't any posts or users in the db
if (!Users.find().count() && !Events.findOne({name: 'createDummyContent'}) && !Posts.find().count()) { if (!Users.find().count()) {
createDummyUsers(); createDummyUsers();
}
if (!Posts.find().count()) {
createDummyPosts(); createDummyPosts();
}
if (!Comments.find().count()) {
createDummyComments(); createDummyComments();
Events.log({name: 'createDummyContent', unique: true, important: true});
} }
}); });

View file

@ -20,12 +20,6 @@ Package.onUse(function (api) {
'vulcan:events@1.3.0', 'vulcan:events@1.3.0',
]); ]);
// both
api.addFiles([
'lib/getting_started.js'
], ['client', 'server']);
// client // client
api.addAssets([ api.addAssets([
@ -36,7 +30,7 @@ Package.onUse(function (api) {
// server // server
api.addFiles([ api.addFiles([
'lib/server/dummy_content.js' 'lib/server/seed.js'
], ['server']); ], ['server']);
api.addAssets('content/read_this_first.md', 'server'); api.addAssets('content/read_this_first.md', 'server');

View file

@ -89,10 +89,10 @@ Mongo.Collection.prototype.helpers = function(helpers) {
export const createCollection = options => { export const createCollection = options => {
const {collectionName, typeName, schema, resolvers, mutations, generateGraphQLSchema = true } = options; const {collectionName, typeName, schema, resolvers, mutations, generateGraphQLSchema = true, dbCollectionName } = options;
// initialize new Mongo collection // initialize new Mongo collection
const collection = collectionName === 'users' ? Meteor.users : new Mongo.Collection(collectionName); const collection = collectionName === 'users' ? Meteor.users : new Mongo.Collection(dbCollectionName ? dbCollectionName : collectionName.toLowerCase());
// decorate collection with options // decorate collection with options
collection.options = options; collection.options = options;

View file

@ -85,6 +85,9 @@ export const GraphQLSchema = {
addResolvers(resolvers) { addResolvers(resolvers) {
this.resolvers = deepmerge(this.resolvers, resolvers); this.resolvers = deepmerge(this.resolvers, resolvers);
}, },
removeResolver(typeName, resolverName) {
delete this.resolvers[typeName][resolverName];
},
// add objects to context // add objects to context
context: {}, context: {},
@ -95,7 +98,8 @@ export const GraphQLSchema = {
// generate a GraphQL schema corresponding to a given collection // generate a GraphQL schema corresponding to a given collection
generateSchema(collection) { generateSchema(collection) {
const collectionName = collection._name; const collectionName = collection.options.collectionName;
const mainTypeName = collection.typeName ? collection.typeName : Utils.camelToSpaces(_.initial(collectionName).join('')); // default to posts -> Post const mainTypeName = collection.typeName ? collection.typeName : Utils.camelToSpaces(_.initial(collectionName).join('')); // default to posts -> Post
// backward-compatibility code: we do not want user.telescope fields in the graphql schema // backward-compatibility code: we do not want user.telescope fields in the graphql schema
@ -160,4 +164,5 @@ export const addGraphQLSchema = GraphQLSchema.addSchema.bind(GraphQLSchema);
export const addGraphQLQuery = GraphQLSchema.addQuery.bind(GraphQLSchema); export const addGraphQLQuery = GraphQLSchema.addQuery.bind(GraphQLSchema);
export const addGraphQLMutation = GraphQLSchema.addMutation.bind(GraphQLSchema); export const addGraphQLMutation = GraphQLSchema.addMutation.bind(GraphQLSchema);
export const addGraphQLResolvers = GraphQLSchema.addResolvers.bind(GraphQLSchema); export const addGraphQLResolvers = GraphQLSchema.addResolvers.bind(GraphQLSchema);
export const removeGraphQLResolver = GraphQLSchema.removeResolver.bind(GraphQLSchema);
export const addToGraphQLContext = GraphQLSchema.addToContext.bind(GraphQLSchema); export const addToGraphQLContext = GraphQLSchema.addToContext.bind(GraphQLSchema);

View file

@ -14,7 +14,7 @@ import './mongo_redux.js';
export { Components, registerComponent, replaceComponent, getRawComponent, getComponent, copyHoCs, populateComponentsApp } from './components.js'; export { Components, registerComponent, replaceComponent, getRawComponent, getComponent, copyHoCs, populateComponentsApp } from './components.js';
export { createCollection } from './collections.js'; export { createCollection } from './collections.js';
export { Callbacks, addCallback, removeCallback, runCallbacks, runCallbacksAsync } from './callbacks.js'; export { Callbacks, addCallback, removeCallback, runCallbacks, runCallbacksAsync } from './callbacks.js';
export { GraphQLSchema, addGraphQLSchema, addGraphQLQuery, addGraphQLMutation, addGraphQLResolvers, addToGraphQLContext } from './graphql.js'; export { GraphQLSchema, addGraphQLSchema, addGraphQLQuery, addGraphQLMutation, addGraphQLResolvers, removeGraphQLResolver, addToGraphQLContext } from './graphql.js';
export { Routes, addRoute, getRoute, populateRoutesApp } from './routes.js'; export { Routes, addRoute, getRoute, populateRoutesApp } from './routes.js';
export { Utils } from './utils.js'; export { Utils } from './utils.js';
export { getSetting } from './settings.js'; export { getSetting } from './settings.js';

View file

@ -23,7 +23,6 @@ Package.onUse(function (api) {
'check', 'check',
'http', 'http',
'email', 'email',
'tracker',
'ecmascript', 'ecmascript',
'service-configuration', 'service-configuration',
'shell-server', 'shell-server',

View file

@ -4,7 +4,7 @@ import { Router, browserHistory } from 'react-router';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { InjectData } from 'meteor/vulcan:core'; import { InjectData } from 'meteor/vulcan:lib';
export const RouterClient = { export const RouterClient = {
run(routes, options) { run(routes, options) {

View file

@ -11,7 +11,7 @@ import {
addReducer, addMiddleware, addReducer, addMiddleware,
Routes, populateComponentsApp, populateRoutesApp, runCallbacks, Routes, populateComponentsApp, populateRoutesApp, runCallbacks,
getRenderContext, getRenderContext,
} from 'meteor/vulcan:core'; } from 'meteor/vulcan:lib';
import { RouterClient } from './router.jsx'; import { RouterClient } from './router.jsx';

View file

@ -4,7 +4,7 @@ import ReactDOMServer from 'react-dom/server';
import { RoutePolicy } from 'meteor/routepolicy'; import { RoutePolicy } from 'meteor/routepolicy';
import { withRenderContextEnvironment, InjectData } from 'meteor/vulcan:core'; import { withRenderContextEnvironment, InjectData } from 'meteor/vulcan:lib';
function isAppUrl(req) { function isAppUrl(req) {
const url = req.url; const url = req.url;

View file

@ -10,7 +10,7 @@ import {
addRoute, addRoute,
Routes, populateComponentsApp, populateRoutesApp, Routes, populateComponentsApp, populateRoutesApp,
getRenderContext, getRenderContext,
} from 'meteor/vulcan:core'; } from 'meteor/vulcan:lib';
import { RouterServer } from './router.jsx'; import { RouterServer } from './router.jsx';

View file

@ -10,7 +10,7 @@ Package.onUse(function (api) {
api.versionsFrom(['METEOR@1.0']); api.versionsFrom(['METEOR@1.0']);
api.use([ api.use([
'vulcan:core@1.3.0', 'vulcan:lib@1.3.0',
]); ]);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');

View file

@ -18,11 +18,11 @@ echo "Telescope requires Meteor but it's not installed. Trying to Install..." >&
if [ "$(uname)" == "Darwin" ]; then if [ "$(uname)" == "Darwin" ]; then
# Mac OS platform # Mac OS platform
echo "🔭 ${bold}${purple}Good news you have a Mac and we will install it now! ${reset}"; echo "🔭 ${bold}${purple}Good news you have a Mac and we will install it now! ${reset}";
curl https://install.meteor.com/ | sh; curl https://install.meteor.com/ | bash;
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
# GNU/Linux platform # GNU/Linux platform
echo "🔭 ${bold}${purple}Good news you are on GNU/Linux platform and we will install Meteor now! ${reset}"; echo "🔭 ${bold}${purple}Good news you are on GNU/Linux platform and we will install Meteor now! ${reset}";
curl https://install.meteor.com/ | sh; curl https://install.meteor.com/ | bash;
elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then
# Windows NT platform # Windows NT platform
echo "🔭 ${bold}${purple}Oh no! you are on a Windows platform and you will need to install Meteor Manually! ${reset}"; echo "🔭 ${bold}${purple}Oh no! you are on a Windows platform and you will need to install Meteor Manually! ${reset}";

View file

@ -23,6 +23,10 @@
version "0.8.6" version "0.8.6"
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.8.6.tgz#b34fb880493ba835b0c067024ee70130d6f9bb68" resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.8.6.tgz#b34fb880493ba835b0c067024ee70130d6f9bb68"
"@types/graphql@^0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.0.tgz#fccf859f0d2817687f210737dc3be48a18b1d754"
"@types/isomorphic-fetch@0.0.33": "@types/isomorphic-fetch@0.0.33":
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.33.tgz#3ea1b86f8b73e6a7430d01d4dbd5b1f63fd72718" resolved "https://registry.yarnpkg.com/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.33.tgz#3ea1b86f8b73e6a7430d01d4dbd5b1f63fd72718"
@ -130,7 +134,7 @@ ansi-styles@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
apollo-client@^1.0.0-rc.2, apollo-client@^1.0.0-rc.5: apollo-client@^1.0.0-rc.2:
version "1.0.0-rc.5" version "1.0.0-rc.5"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-1.0.0-rc.5.tgz#862bbd72a267a998627009665b5581ed13e26010" resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-1.0.0-rc.5.tgz#862bbd72a267a998627009665b5581ed13e26010"
dependencies: dependencies:
@ -144,6 +148,21 @@ apollo-client@^1.0.0-rc.2, apollo-client@^1.0.0-rc.5:
"@types/graphql" "^0.8.0" "@types/graphql" "^0.8.0"
"@types/isomorphic-fetch" "0.0.33" "@types/isomorphic-fetch" "0.0.33"
apollo-client@^1.0.0-rc.6:
version "1.0.0-rc.8"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-1.0.0-rc.8.tgz#30105eb33f59ecab63f493177416d720ba0a8fd9"
dependencies:
graphql "^0.9.1"
graphql-anywhere "^3.0.1"
graphql-tag "^2.0.0"
redux "^3.4.0"
symbol-observable "^1.0.2"
whatwg-fetch "^2.0.0"
optionalDependencies:
"@types/async" "^2.0.31"
"@types/graphql" "^0.9.0"
"@types/isomorphic-fetch" "0.0.33"
argparse@^1.0.7: argparse@^1.0.7:
version "1.0.9" version "1.0.9"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
@ -240,6 +259,10 @@ asynckit@^0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
attr-accept@^1.0.3:
version "1.1.0"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.0.tgz#b5cd35227f163935a8f1de10ed3eba16941f6be6"
autoprefixer@^6.3.6: autoprefixer@^6.3.6:
version "6.6.1" version "6.6.1"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.6.1.tgz#11a4077abb4b313253ec2f6e1adb91ad84253519" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.6.1.tgz#11a4077abb4b313253ec2f6e1adb91ad84253519"
@ -1682,13 +1705,13 @@ graphql-server-module-graphiql@^0.6.0:
resolved "https://registry.yarnpkg.com/graphql-server-module-graphiql/-/graphql-server-module-graphiql-0.6.0.tgz#e37634b05f000731981e8ed13103f9a5861e5da0" resolved "https://registry.yarnpkg.com/graphql-server-module-graphiql/-/graphql-server-module-graphiql-0.6.0.tgz#e37634b05f000731981e8ed13103f9a5861e5da0"
graphql-tag@^1.3.1: graphql-tag@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-1.3.1.tgz#16cdf13635f10bbc968c6f2c6265ffe883a906da"
graphql-tag@^1.3.2:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-1.3.2.tgz#7abb3a8fd9f3415d07163314ed237061c785b759" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-1.3.2.tgz#7abb3a8fd9f3415d07163314ed237061c785b759"
graphql-tag@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.0.0.tgz#f3efe3b4d64f33bfe8479ae06a461c9d72f2a6fe"
graphql-tools@^0.10.1: graphql-tools@^0.10.1:
version "0.10.1" version "0.10.1"
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-0.10.1.tgz#274aa338d50b1c0b3ed6936eafd8ed3a19ed1828" resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-0.10.1.tgz#274aa338d50b1c0b3ed6936eafd8ed3a19ed1828"
@ -2861,6 +2884,12 @@ react-datetime@^2.3.2:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.0" object-assign "^4.1.0"
react-dropzone@^3.12.2:
version "3.12.2"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-3.12.2.tgz#7e66a37322a80cf26a205d749ecf8cad0d90aa6f"
dependencies:
attr-accept "^1.0.3"
react-helmet@^3.1.0: react-helmet@^3.1.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-3.3.0.tgz#419933e7ce5a75d04aab3fefe77169eed8e55646" resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-3.3.0.tgz#419933e7ce5a75d04aab3fefe77169eed8e55646"