mirror of
https://github.com/vale981/Vulcan
synced 2025-03-08 19:11:38 -05:00
Merge branch 'devel'
# Conflicts: # README.md # packages/nova-voting/lib/callbacks.js # packages/nova-voting/lib/client.js # packages/nova-voting/lib/modules.js # packages/nova-voting/lib/server.js # packages/nova-voting/package.js
This commit is contained in:
commit
9193de08e3
818 changed files with 12406 additions and 14272 deletions
|
@ -20,7 +20,8 @@
|
|||
"MailChimpAPI",
|
||||
"Juice",
|
||||
"Run",
|
||||
"AppComposer"
|
||||
"AppComposer",
|
||||
"Query",
|
||||
]
|
||||
}],
|
||||
"babel/array-bracket-spacing": 0,
|
||||
|
@ -39,7 +40,8 @@
|
|||
}],
|
||||
"no-console": 1,
|
||||
"react/prop-types": 0,
|
||||
"meteor/audit-argument-checks": 0
|
||||
"meteor/audit-argument-checks": 0,
|
||||
"no-case-declarations": 0
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -46,3 +46,4 @@ bundle.tar.gz
|
|||
|
||||
jsdoc-conf.json
|
||||
jsdoc.json
|
||||
packages/nova-router/.npm
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
############ Nova Core ############
|
||||
############ Core Packages ############
|
||||
|
||||
nova:core # do not remove!
|
||||
nova:core # core components and wrappers
|
||||
nova:forms # auto-generated forms
|
||||
nova:apollo # data layer
|
||||
nova:routing # routing and server-side rendering
|
||||
nova:email # email
|
||||
nova:users # user management and permissions
|
||||
|
||||
############ Optional Packages ############
|
||||
############ Features Packages ############
|
||||
|
||||
nova:settings
|
||||
nova:users
|
||||
nova:events
|
||||
nova:posts
|
||||
nova:comments
|
||||
nova:newsletter
|
||||
|
@ -14,31 +18,29 @@ nova:notifications
|
|||
nova:getting-started
|
||||
nova:categories
|
||||
nova:voting
|
||||
nova:forms
|
||||
nova:embedly
|
||||
nova:api
|
||||
nova:email
|
||||
nova:rss
|
||||
# nova:subscribe
|
||||
# nova:cloudinary
|
||||
|
||||
############ Customizable Packages ############
|
||||
|
||||
nova:base-components
|
||||
nova:base-styles
|
||||
nova:base-routes
|
||||
nova:email-templates
|
||||
nova:i18n-en-us
|
||||
|
||||
accounts-password@1.3.3
|
||||
# accounts-twitter
|
||||
# accounts-facebook
|
||||
nova:subscribe
|
||||
|
||||
############ Debug Packages ############
|
||||
|
||||
nova:debug
|
||||
# nova:kadira
|
||||
|
||||
############ Theme Packages ############
|
||||
|
||||
nova:base-components # default ui components
|
||||
nova:base-styles # default styling
|
||||
|
||||
nova:email-templates # default email templates for notifications
|
||||
nova:i18n-en-us # default language translation
|
||||
|
||||
############ Your Packages ############
|
||||
|
||||
# my-custom-package
|
||||
accounts-password@1.3.3
|
||||
# accounts-twitter
|
||||
# accounts-facebook
|
||||
|
||||
# customization-demo
|
||||
# framework-demo
|
||||
|
|
|
@ -19,6 +19,7 @@ callback-hook@1.0.10
|
|||
check@1.2.4
|
||||
chuangbo:cookie@1.1.0
|
||||
coffeescript@1.11.1_4
|
||||
customization-demo@0.0.0
|
||||
dburles:collection-helpers@1.1.0
|
||||
ddp@1.2.5
|
||||
ddp-client@1.3.2
|
||||
|
@ -31,8 +32,9 @@ ecmascript@0.6.1
|
|||
ecmascript-runtime@0.3.15
|
||||
ejson@1.0.13
|
||||
email@1.1.18
|
||||
fortawesome:fontawesome@4.7.0
|
||||
fortawesome:fontawesome@4.5.0
|
||||
fourseven:scss@3.10.1
|
||||
framework-demo@0.0.0
|
||||
geojson-utils@1.0.10
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.0.11
|
||||
|
@ -46,17 +48,15 @@ jquery@1.11.10
|
|||
livedata@1.0.18
|
||||
localstorage@1.0.12
|
||||
logging@1.1.16
|
||||
matb33:collection-hooks@0.8.4
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.6.0
|
||||
meteor-base@1.0.4
|
||||
meteorhacks:fast-render@2.16.0
|
||||
meteorhacks:inject-data@2.0.1-nova-patch
|
||||
meteorhacks:inject-data@2.0.1-patch
|
||||
meteorhacks:inject-initial@1.0.4
|
||||
meteorhacks:meteorx@1.4.1
|
||||
meteorhacks:picker@1.0.3
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
meteorhacks:unblock@1.1.0
|
||||
minifier-css@1.2.15
|
||||
minifier-js@1.2.15
|
||||
minimongo@1.0.19
|
||||
|
@ -64,39 +64,35 @@ modules@0.7.7
|
|||
modules-runtime@0.7.7
|
||||
mongo@1.1.14
|
||||
mongo-id@1.0.6
|
||||
nova:api@0.27.5-nova
|
||||
nova:base-components@0.27.5-nova
|
||||
nova:base-routes@0.27.5-nova
|
||||
nova:base-styles@0.27.5-nova
|
||||
nova:categories@0.27.5-nova
|
||||
nova:comments@0.27.5-nova
|
||||
nova:core@0.27.5-nova
|
||||
nova:debug@0.27.5-nova
|
||||
nova:email@0.27.5-nova
|
||||
nova:email-templates@0.27.5-nova
|
||||
nova:embedly@0.27.5-nova
|
||||
nova:events@0.27.5-nova
|
||||
nova:forms@0.27.5-nova
|
||||
nova:getting-started@0.27.5-nova
|
||||
nova:i18n-en-us@0.27.5-nova
|
||||
nova:lib@0.27.5-nova
|
||||
nova:newsletter@0.27.5-nova
|
||||
nova:notifications@0.27.5-nova
|
||||
nova:posts@0.27.5-nova
|
||||
nova:rss@0.27.5-nova
|
||||
nova:search@0.27.5-nova
|
||||
nova:settings@0.27.5-nova
|
||||
nova:users@0.27.5-nova
|
||||
nova:voting@0.27.5-nova
|
||||
nova:api@1.0.0
|
||||
nova:apollo@1.0.0
|
||||
nova:base-components@1.0.0
|
||||
nova:base-styles@1.0.0
|
||||
nova:categories@1.0.0
|
||||
nova:comments@1.0.0
|
||||
nova:core@1.0.0
|
||||
nova:debug@1.0.0
|
||||
nova:email@1.0.0
|
||||
nova:email-templates@1.0.0
|
||||
nova:embedly@1.0.0
|
||||
nova:events@1.0.0
|
||||
nova:forms@1.0.0
|
||||
nova:getting-started@1.0.0
|
||||
nova:i18n-en-us@1.0.0
|
||||
nova:lib@1.0.0
|
||||
nova:newsletter@1.0.0
|
||||
nova:notifications@1.0.0
|
||||
nova:posts@1.0.0
|
||||
nova:routing@1.0.0
|
||||
nova:rss@1.0.0
|
||||
nova:search@1.0.0
|
||||
nova:subscribe@1.0.0
|
||||
nova:users@1.0.0
|
||||
nova:voting@1.0.0
|
||||
npm-bcrypt@0.9.2
|
||||
npm-mongo@2.2.11_2
|
||||
observe-sequence@1.0.14
|
||||
ordered-dict@1.0.9
|
||||
peerlibrary:assert@0.2.5
|
||||
peerlibrary:fiber-utils@0.6.0
|
||||
peerlibrary:reactive-mongo@0.1.1
|
||||
peerlibrary:reactive-publish@0.3.0
|
||||
peerlibrary:server-autorun@0.5.2
|
||||
percolatestudio:synced-cron@1.1.0
|
||||
promise@0.8.8
|
||||
raix:eventemitter@0.1.3
|
||||
|
@ -105,7 +101,7 @@ rate-limit@1.0.6
|
|||
react-meteor-data@0.2.9
|
||||
reactive-dict@1.1.8
|
||||
reactive-var@1.0.11
|
||||
reactrouter:react-router-ssr@3.1.6-nova-patch
|
||||
reactrouter:react-router-ssr@3.1.6-patch
|
||||
reload@1.1.11
|
||||
retry@1.0.9
|
||||
routepolicy@1.0.12
|
||||
|
@ -120,15 +116,12 @@ srp@1.0.10
|
|||
standard-minifier-css@1.3.2
|
||||
standard-minifier-js@1.2.1
|
||||
standard-minifiers@1.0.6
|
||||
std:accounts-ui@1.2.9
|
||||
std:accounts-ui@1.2.17
|
||||
tmeasday:check-npm-versions@0.3.1
|
||||
tmeasday:publish-counts@0.8.0
|
||||
tracker@1.1.1
|
||||
ui@1.0.12
|
||||
underscore@1.0.10
|
||||
url@1.0.11
|
||||
utilities:react-list-container@0.1.14
|
||||
utilities:smart-methods@0.1.5
|
||||
utilities:smart-publications@0.1.4
|
||||
webapp@1.3.12
|
||||
webapp-hashing@1.0.9
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
## v1.0.0
|
||||
- A lot of breaking changes: this is the Apollo/GraphQL version of Telescope Nova. [You can find the documentation here](http://nova-docs.telescopeapp.org/).
|
||||
|
||||
## v0.27.5
|
||||
|
||||
- Nova is now powered by Meteor 1.4.2.3.
|
||||
|
@ -110,4 +113,4 @@
|
|||
|
||||
## v0.25.7
|
||||
|
||||
First Nova version.
|
||||
First Nova version.
|
||||
|
|
745
README.md
745
README.md
|
@ -1,746 +1,11 @@
|
|||
# Telescope Nova
|
||||
|
||||
### Looking for the Apollo/GraphQL version? Check out the [devel](https://github.com/TelescopeJS/Telescope/tree/devel) branch.
|
||||
[Version 1.0.0](https://github.com/TelescopeJS/Telescope/releases)
|
||||
|
||||
There are currently two distinct versions of Telescope: **Nova** and **Legacy**.
|
||||
This is the Apollo/GraphQL version of Telescope Nova. [You can find the documentation here](http://nova-docs.telescopeapp.org/).
|
||||
|
||||
**Nova** is the new, React-based version and all development will happen on this version going forward. It's used by the [master](https://github.com/TelescopeJS/Telescope/tree/master) and [devel](https://github.com/TelescopeJS/Telescope/tree/devel) branches.
|
||||
### Other Versions
|
||||
|
||||
Note that as of December 2016, the devel branch now uses [GraphQL](http://graphql.org) as its data layer while the master branch is still on the previous, non-GraphQL version. It is recommended you use the devel branch for any new projects if possible.
|
||||
You can find the older, non-Apollo version of Telescope Nova on the [nova-classic](https://github.com/TelescopeJS/Telescope/tree/nova-classic) branch.
|
||||
|
||||
**Legacy** is the old, Blaze-powered version of Telescope and you can find it on the [legacy](https://github.com/TelescopeJS/Telescope/tree/legacy) and [legacy-devel](https://github.com/TelescopeJS/Telescope/tree/legacy-devel) branches.
|
||||
|
||||
Note that both versions use the same data format, so you can go back and forth between them on the same app and the same database.
|
||||
|
||||
## Table Of Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Updating](#updating)
|
||||
- [Resources](#resources)
|
||||
- [Settings](#settings)
|
||||
- [Deployment](#deployment)
|
||||
- [Categories](#categories)
|
||||
- [Social Login](#social-login)
|
||||
- [Packages](#packages)
|
||||
- [Application Structure](#application-structure)
|
||||
- [Files](#files)
|
||||
- [Customizing Components](#customizing-components)
|
||||
- [Customizing Emails](#customizing-emails)
|
||||
- [Custom Fields](#custom-fields)
|
||||
- [Publishing Data](#publishing-data)
|
||||
- [Subscribing](#subscribing)
|
||||
- [Loading Data](#loading-data)
|
||||
- [Callbacks](#callbacks)
|
||||
- [Posts Parameters](#posts-parameters)
|
||||
- [Forms](#forms)
|
||||
- [Methods](#methods)
|
||||
- [Routes](#routes)
|
||||
- [Groups & Permissions](#groups--permissions)
|
||||
- [Internationalization](#internationalization)
|
||||
- [Cheatsheet](#cheatsheet)
|
||||
- [Third-Party packages](#third-party-packages)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### First Steps
|
||||
|
||||
Install the latest version of Node and NPM. We recommend the usage of [NVM](http://nvm.sh).
|
||||
|
||||
[Install Meteor](https://www.meteor.com/install):
|
||||
|
||||
```sh
|
||||
curl https://install.meteor.com/ | sh
|
||||
```
|
||||
|
||||
Clone this repository locally:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:TelescopeJS/Telescope.git
|
||||
```
|
||||
|
||||
(or `https://github.com/TelescopeJS/Telescope.git`)
|
||||
|
||||
Install the necessary NPM packages:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
Then run the app with:
|
||||
|
||||
```sh
|
||||
meteor
|
||||
```
|
||||
|
||||
You'll then be able to access it on [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
### Creating An Admin Account
|
||||
|
||||
The first account you create (via Log In > Register) will automatically be given admin rights.
|
||||
|
||||
### Deleting Dummy Content
|
||||
|
||||
On its first run, Nova seeds the site with a few dummy posts. You can remove them by opening the browser console and calling `Meteor.call('removeGettingStartedContent')` while logged in as admin.
|
||||
|
||||
### Example Custom Package
|
||||
|
||||
This repo also includes an example of how to customize Nova using a custom package. To enable the custom package, simply uncomment the line `# my-custom-package` in `.meteor/packages` (remove the `#`).
|
||||
|
||||
For more information on customizing Nova, refer to specific sections in this documentation. Note that **you should never customize core files directly** (files in `nova:*` packages).
|
||||
|
||||
Instead, either extend the object you want to customize from your own package, or disable the initial package, clone it, and modify your own copy.
|
||||
|
||||
## Updating
|
||||
|
||||
#### Updating with Git
|
||||
|
||||
If you've cloned this repo and are using **local packages** (i.e. `nova:core`, `nova:posts`, etc. are in your `/packages` directory) you'll have to pull in the changes from this repo with `git pull origin master`.
|
||||
|
||||
#### Updating with Meteor
|
||||
|
||||
Alternatively, if Meteor can't find a package in your local `/packages` directory it will look for it in the [Atmosphere](http://atmospherejs.com) package directory. This means you can also update the app by following these steps:
|
||||
|
||||
- Delete all `nova:*` packages from `/packages` to force Meteor to use remote versions instead.
|
||||
- Run `meteor update`.
|
||||
- If that didn't work, delete the `.meteor/versions` file to force an update.
|
||||
|
||||
If you're comfortable with Git workflows the first method is recommended, if not you can use the `meteor update` technique instead.
|
||||
|
||||
#### Upgrading From Older Versions
|
||||
|
||||
To update to Nova from an earlier version of Telescope, I suggest you create a new repo and start from scratch. That being said you can use the same database seamlessly since Nova uses the same database schema.
|
||||
|
||||
For local development, an easy way to do that is to simply copy the `.meteor/local` directory which contains your local database to your new repo.
|
||||
|
||||
## Resources
|
||||
|
||||
The best ways to get support are [Telescope Meta](http://meta.telescopeapp.org) and the [Telescope Slack Chatroom](http://slack.telescopeapp.org).
|
||||
|
||||
## Settings
|
||||
|
||||
Settings can be configured in your `settings.json` file. For legacy compatibility reasons, settings can also be specified in your database, but note that settings specified in `settings.json` take priority over those stored in the database.
|
||||
|
||||
Settings can be public (meaning they will be published to the client) or private (they will be kept on the server). Public settings should be set on the `public` object. You can find a full example in `sample_settings.json`.
|
||||
|
||||
To use your `settings.json` file:
|
||||
|
||||
- Development: `meteor --settings settings.json`
|
||||
- Production: specify the path to `settings.json` in the tool you use to deploy (i.e. `mup deploy --settings settings.json`, see below)
|
||||
|
||||
## Deployment
|
||||
|
||||
The recommended way to deploy Nova is by using [Mup](https://github.com/kadirahq/meteor-up/), at least v1.0.3.
|
||||
|
||||
#### Configuration
|
||||
|
||||
You should have a Linux server online, for instance [a Digital Ocean droplet running with Ubuntu](https://www.digitalocean.com).
|
||||
|
||||
Install globally the latest `kadirahq/meteor-up`.
|
||||
|
||||
```
|
||||
npm install -g mup
|
||||
```
|
||||
|
||||
Create Meteor Up configuration files in your project directory with `mup init`. In the example below, the configuration files are created in a `.deploy` directory at the root of your app.
|
||||
|
||||
```
|
||||
cd my-app-folder
|
||||
mkdir .deploy
|
||||
cd .deploy
|
||||
mup init
|
||||
```
|
||||
|
||||
This will create two files :
|
||||
|
||||
```
|
||||
mup.js - Meteor Up configuration file
|
||||
settings.json - Settings for Meteor's settings API
|
||||
```
|
||||
|
||||
Then, replace the content of the newly created `settings.json` with your own settings (you can use the content of `sample_settings.json` as a starter).
|
||||
|
||||
Fill `mup.js` with your credentials and optional settings (check the [Mup repo](https://github.com/kadirahq/meteor-up) for additional docs).
|
||||
|
||||
**Note:** the `ROOT_URL` field should be the absolute url of your deploy ; and you need to explicitly point out to use `abernix/meteord:base` docker image with a `docker` field within the `meteor` object.
|
||||
|
||||
```
|
||||
...
|
||||
meteor: {
|
||||
...
|
||||
path: '../' // relative path of the app considering your mup config files
|
||||
env: {
|
||||
ROOT_URL: 'http://nova-app.com', // absolute url of your deploy
|
||||
...
|
||||
},
|
||||
...
|
||||
docker: {
|
||||
image:'abernix/meteord:base' // docker image working with meteor 1.4 & node 4
|
||||
},
|
||||
...
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
You can take inspiration (or copy/paste) on this [`mup.js` example](https://gist.github.com/xavcz/6ddc2bb6f67fe0936c8328ab3314641d).
|
||||
|
||||
#### Setup your server
|
||||
|
||||
From this folder, you can now setup Docker & Mongo your server with:
|
||||
```
|
||||
mup setup
|
||||
```
|
||||
|
||||
#### Deploy your app to your server
|
||||
|
||||
Still in the same folder, to deploy your app with your settings file:
|
||||
|
||||
```
|
||||
mup deploy --settings settings.json
|
||||
```
|
||||
|
||||
## Categories
|
||||
|
||||
Just like Settings, you can specify categories either via the in-app UI or via `settings.json`. Note that if you want to delete a category, you'll have to both delete it via the UI and also remove it from `settings.json`.
|
||||
|
||||
## Social Login
|
||||
|
||||
To add new social login options, you'll first need to add your API keys to your `settings.json` file. For example:
|
||||
|
||||
```json
|
||||
"oAuth": {
|
||||
"twitter": {
|
||||
"consumerKey": "foo",
|
||||
"secret": "bar"
|
||||
},
|
||||
"facebook": {
|
||||
"appId": "foo",
|
||||
"secret": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(Make sure these are not in the `public` block of `settings.json`)
|
||||
|
||||
Then, add the relevant Meteor package:
|
||||
|
||||
```sh
|
||||
meteor add accounts-twitter accounts-facebook
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
Nova's codebase is split across multiple packages, with the philosophy that you should be able to add and remove packages depending on which features you actually need.
|
||||
|
||||
#### Core Packages
|
||||
|
||||
These packages are necessary for Nova to run.
|
||||
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| `nova:lib` | Utility functions used by the app; also handles importing most external packages. |
|
||||
| `nova:events` | Event tracking.|
|
||||
| `nova:core` | Import previous core packages. |
|
||||
|
||||
#### Optional Packages
|
||||
|
||||
These packages are optional, although they might depend on each other. Note that dependencies on non-core packages should be `weak` whenever possible.
|
||||
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| `nova:api` | Generate a JSON API for posts. |
|
||||
| `nova:categories` | Posts categories. |
|
||||
| `nova:comments` | Comments. |
|
||||
| `nova:email` | Send emails. |
|
||||
| `nova:embedly` | Get metadata (thumbnails, origin, etc.) from [Embedly](http://embed.ly) when submitting new posts. |
|
||||
| `nova:forms` | Generate forms for inserting and editing documents ([README](https://github.com/TelescopeJS/Telescope/tree/nova/packages/nova-forms)). |
|
||||
| `nova:getting-started` | Generate dummy content on first run. |
|
||||
| `nova:kadira` | [Kadira](http://kadira.io) integration. |
|
||||
| `nova:newsletter` | Send an automated newsletter with [Mailchimp](http://mailchimp.com). |
|
||||
| `nova:notifications` | Notifications. |
|
||||
| `nova:posts` | Posts. |
|
||||
| `nova:RSS` | RSS feeds for posts and comments. |
|
||||
| `nova:search` | Search across posts. |
|
||||
| `nova:settings` | Legacy support for publishing settings. |
|
||||
| `nova:share` | Easy social sharing. |
|
||||
| `nova:users` | Users. |
|
||||
| `nova:voting` | Voting on posts and comments. |
|
||||
|
||||
#### Customizable Packages
|
||||
|
||||
These are the packages that you might commonly need to customize or replace to tweak your app's layout, design, and behavior. You can either clone these packages and modify them directly, or *extend* their contents (see the [Customizing Components](#customizing-components) section.), but you should **not** modify them directly.
|
||||
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| `nova:base-components` | The default components that make up the Nova front-end. |
|
||||
| `nova:base-styles` | Default styles (includes Bootstrap).|
|
||||
| `nova:base-routes` | Default routes.|
|
||||
| `nova:email-templates` | Email templates.|
|
||||
| `nova:i18n-en-us` | Contains English language strings.|
|
||||
|
||||
#### Extra Packages
|
||||
|
||||
These packages provide extra features but are not enabled out of the box.
|
||||
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| `nova:forms-tags` | A component for autofilled tags. |
|
||||
| `nova:cloudinary` | Automatically upload posts thumbnails to [Cloudinary](http://cloudinary.com).|
|
||||
|
||||
#### Debug Packages
|
||||
|
||||
These packages are provided to help you when doing local development.
|
||||
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| `nova:debug` | Provides routes and utility for debugging. |
|
||||
| `nova:demo` | A demo of how to use custom collections.|
|
||||
|
||||
## Application Structure
|
||||
|
||||
Nova's application structure is a bit different than most other Meteor apps. Generally speaking, we can distinguish between three ways of organizing code in a Meteor app: default, module-based, and package-based (which is what Nova uses).
|
||||
|
||||
The **default** app structure is what legacy Meteor apps such as [Microscope](https://github.com/DiscoverMeteor/Microscope) use. Files are stored in `/client`, `/server`, `/lib`, etc. directories and imported automatically by Meteor. This approach requires the least work, but also gives you less control over load order.
|
||||
|
||||
Starting with Meteor 1.3, the **module-based** approach is the pattern officially recommended by the [Meteor Guide](http://guide.meteor.com/structure.html). In it, all your files are stored in an `/imports` directory, with two `/client/main.js` and `/server/main.js` entry points that then import all other files. The main difference with the previous pattern is that files in `/imports` no longer run automatically.
|
||||
|
||||
Finally, with the **package-based** technique, all your code is stored in [Meteor packages](http://guide.meteor.com/using-packages.html). Packages can be loaded from Meteor's package server, or stored locally in your `/packages` directory. Note that it is recommended you use modules within your packages.
|
||||
|
||||
When customizing Nova, you can use any of these three approaches for your own custom code. But if you can, I would recommend sticking with Nova's **package-based** approach just to maintain consistency between Nova's codebase and yours.
|
||||
|
||||
Also, using packages for customization means you have an easy way to turn off any customization you've added if you need to track down the source of a problem.
|
||||
|
||||
## Files
|
||||
|
||||
Nova tries to maintain a consistent file structure for its main packages:
|
||||
|
||||
- `config.js`: the package's main namespace and set basic config options.
|
||||
- `collection.js`: the package's collection schema.
|
||||
- `callbacks.js`: callbacks used by the package.
|
||||
- `helpers.js`: collection helpers.
|
||||
- `methods.js`: collection methods.
|
||||
- `published_fields.js`: specifies which collection fields should be published in which context.
|
||||
- `custom_fields.js`: sets custom fields on *other* collections.
|
||||
- `routes.jsx`: routes.
|
||||
- `views.js`: views used for [query constructors](https://www.discovermeteor.com/blog/query-constructors/).
|
||||
- `parameters.js`: the collection's query constructor.
|
||||
- `email_routes.js`: test routes for email templates.
|
||||
- `server/publications.js`: publications.
|
||||
|
||||
## Customizing Components
|
||||
|
||||
Apart from a couple exceptions, almost all React components in Nova live inside the `nova:base-components` package. There are two main ways of customizing them.
|
||||
|
||||
### Override
|
||||
|
||||
If you only need to modify a single component, you can simply override it with a new one without having to touch the `nova:base-components` package.
|
||||
|
||||
For example, if you wanted to use your own `CustomLogo` component you would do:
|
||||
|
||||
```js
|
||||
const CustomLogo = (props) => {
|
||||
return (
|
||||
<div>/* custom component code */</div>
|
||||
)
|
||||
}
|
||||
Telescope.components.Logo = CustomLogo;
|
||||
```
|
||||
|
||||
Or, if `Logo` is defined as an ES6 class:
|
||||
|
||||
```js
|
||||
class CustomLogo extends Telescope.components.Logo{
|
||||
render() {
|
||||
return (
|
||||
<div>/* custom component code */</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
Telescope.components.Logo = CustomLogo;
|
||||
```
|
||||
|
||||
Components are generally defined as functional stateless components, unless they contain extra logic (lifecycle methods, event handlers, etc.) in which case they'll be defined as ES6 classes.
|
||||
|
||||
For components defined as ES6 classes, make sure you `extend` the original component. This will let you pick and choose which methods you actually need to replace, while inheriting the ones you didn't specify in your new component.
|
||||
|
||||
You can make the override at any point, as long as it happens before the `<Telescope.components.Logo/>` component is called from a parent component.
|
||||
|
||||
### Clone & Modify
|
||||
|
||||
For more in-depth customizations, you can also just clone the entire `nova:base-components` package and then make your modification directly there.
|
||||
|
||||
Of course, keeping your own new `components` package up to date with any future `nova:base-components` modifications will then be up to you.
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
If a component deals with a collection (`Posts`, `Comments`, etc.) its name should start with the collection's capitalized name in plural form, followed by the component's function using camelCase formatting.
|
||||
|
||||
For example: `PostsShare`.
|
||||
|
||||
The outermost HTML element within the component will have a class of the same name, but with a dash instead: `posts-share`. If possible, classes for all other elements within the component will start with the component's class: `posts-share-button`, `posts-share-divider`, etc.
|
||||
|
||||
### Get current user
|
||||
|
||||
The current user is given to the components via the React context. You can access it via `this.context.currentUser` (class) or `context.currentUser` (stateless-component).
|
||||
|
||||
The component needs to define `currentUser` in its `contextTypes`. If `contextTypes` is not defined, then `context` will be an empty object and you won't be able to access to the current user.
|
||||
|
||||
Example :
|
||||
```js
|
||||
const CustomHeader = (props, context) => {
|
||||
// if a user is connected, show its username; else say hello
|
||||
return context.currentUser ? <div>Hey ${context.currentUser.username}!</div> : <div>Hello!</div>
|
||||
};
|
||||
|
||||
// if you don't define `contextTypes` for `CustomHeader`, then the `context` argument will be an empty object
|
||||
CustomHeader.contextTypes = {
|
||||
currentUser: React.PropTypes.object
|
||||
};
|
||||
```
|
||||
|
||||
## Customizing Emails
|
||||
|
||||
Unlike components, emails don't use React but Spacebars, a variant of the Handlebars templating language.
|
||||
|
||||
All email templates live in the `nova:email-templates` package. In order to register a new template or override an existing one, first you must import it as a text asset in your `package.js` file (or store it in your `/public` directory):
|
||||
|
||||
```js
|
||||
api.addAssets(['path/to/template/newReply.handlebars',], ['server']);
|
||||
```
|
||||
|
||||
You'll then be able to load the contents of the file in your code with:
|
||||
|
||||
```js
|
||||
Assets.getText("path/to/template/newReply.handlebars")
|
||||
```
|
||||
|
||||
You can add a template with:
|
||||
|
||||
```js
|
||||
Telescope.email.addTemplates({
|
||||
newReply: Assets.getText("path/to/template/newReply.handlebars")
|
||||
});
|
||||
```
|
||||
|
||||
Or override an existing one with:
|
||||
|
||||
```js
|
||||
Telescope.email.templates.newReply = Assets.getText("path/to/template/newReply.handlebars");
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Fields
|
||||
|
||||
Out of the box, Nova has three main collections: `Posts`, `Users`, and `Comments`. Each of them has a pre-set schema, but that schema can also be extended with custom fields.
|
||||
|
||||
For example, this is how the `nova:newsletter` package extends the `Posts` schema with a `scheduledAt` property that keeps track of when a post was sent out as part of an email newsletter:
|
||||
|
||||
```js
|
||||
Posts.addField({
|
||||
fieldName: 'scheduledAt',
|
||||
fieldSchema: {
|
||||
type: Date,
|
||||
optional: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The `collection.addField()` function takes either a field object, or an array of fields. Each field has a `fieldName` property, and a `fieldSchema` property.
|
||||
|
||||
Each field schema supports all of the [SimpleSchema properties](https://github.com/aldeed/meteor-simple-schema#schema-rules), such as `type`, `optional`, etc.
|
||||
|
||||
A few special properties (`insertableIf`, `editableIf`, `control`, and `order`) are also supported by the [nova:forms](https://github.com/TelescopeJS/Telescope/tree/nova/packages/nova-forms) package.
|
||||
|
||||
Note that Telescope provides a few utility function out of the box to use with `insertableIf` and `editableIf`:
|
||||
|
||||
- `Users.is.admin`: returns `true` if a user is an admin.
|
||||
- `Users.is.memberOrAdmin`: returns `true` if a user is a member (i.e. has an account and is currently logged in) or an admin.
|
||||
- `Users.is.ownerOrAdmin`: (editing only) returns `true` if a user is a members and owns the document being edited; or is an admin.
|
||||
|
||||
Additionally, the `publish` and `join` properties come from the [Smart Publications](https://github.com/meteor-utilities/smart-publications) package. Setting `publish` to true indicates that a field should be published to the client (see also next section).
|
||||
|
||||
You can also remove a field by calling `collection.removeField(fieldName)`. For example:
|
||||
|
||||
```js
|
||||
Posts.removeField('scheduledAt');
|
||||
```
|
||||
|
||||
## Publishing Data
|
||||
|
||||
In order to make data available to the client, you need to **publish** it. Out of the box, Nova includes the following publications:
|
||||
|
||||
- `posts.list`: a list of posts
|
||||
- `posts.single`: a single post (includes more data)
|
||||
- `comments.list`: a list of comments
|
||||
- `users.single`: a single user
|
||||
- `users.current`: the current user (includes personal data)
|
||||
|
||||
While most publications look up each field's `publish` property to figure out if they should publish it or not, some (like `posts.list` and `comments.list`) only feature a smaller subset of properties for performance reasons, and thus have their own specific list of published fields.
|
||||
|
||||
For example, here's how the `nova:embedly` adds the `thumbnailUrl, `media`, `soureName`, and `sourceUrl` fields to the list of published fields for the `posts.list` publication (after having defined them as custom fields):
|
||||
|
||||
```js
|
||||
import PublicationUtils from 'meteor/utilities:smart-publications';
|
||||
|
||||
PublicationUtils.addToFields(Posts.publishedFields.list, ["thumbnailUrl", "media", "sourceName", "sourceUrl"]);
|
||||
```
|
||||
|
||||
## Subscribing
|
||||
|
||||
If you create your own new subscription, you can tell Nova to preload it (and wait for it to be loaded) with:
|
||||
|
||||
```js
|
||||
Telescope.subscriptions.preload(subscriptionName, subscriptionArguments);
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
Telescope.subscriptions.preload("posts.featured", {featuredPostId: "foo123"});
|
||||
```
|
||||
|
||||
## Loading Data
|
||||
|
||||
To load data and display it as a list of documents (or a single document), Nova uses the [React List Container](https://github.com/meteor-utilities/react-list-container) package to connect to the publications mentioned in the previous section.
|
||||
|
||||
## Callbacks
|
||||
|
||||
Nova uses a system of hooks and callbacks for many of its operations.
|
||||
|
||||
For example, here's how you would add a callback to `posts.edit.sync` to give posts an `editedAt` date every time they are modified:
|
||||
|
||||
```js
|
||||
function setEditedAt (post, user) {
|
||||
post.editedAt = new Date();
|
||||
return post;
|
||||
}
|
||||
Telescope.callbacks.add("posts.edit.sync", setEditedAt);
|
||||
```
|
||||
|
||||
If the callback function is named (i.e. declared using the `function foo () {}` syntax), you can also remove it from the callback using:
|
||||
|
||||
```js
|
||||
Telescope.callbacks.remove("posts.edit.sync", "setEditedAt");
|
||||
```
|
||||
|
||||
Methods support three distinct types of callbacks, each with their own hook:
|
||||
|
||||
- `method` callbacks are called within the body of the method, and they run both on the client and server.
|
||||
- `sync` callbacks are called in the mutator, and can run either on both client and server, *or* on the server only if the mutator is called directly.
|
||||
- `async` callbacks are called in the mutator, and only run on the server in an async non-blocking way.
|
||||
|
||||
## Posts Parameters
|
||||
|
||||
In order to filter posts by category, keyword, view, etc. Nova uses a system of successive callbacks to translate filtering options into MongoDB database queries.
|
||||
|
||||
For example, here is how the `nova:search` package adds a callback to handle the `query` parameter:
|
||||
|
||||
```js
|
||||
function addSearchQueryParameter (parameters, terms) {
|
||||
if(!!terms.query) {
|
||||
var parameters = Telescope.utils.deepExtend(true, parameters, {
|
||||
selector: {
|
||||
$or: [
|
||||
{title: {$regex: terms.query, $options: 'i'}},
|
||||
{url: {$regex: terms.query, $options: 'i'}},
|
||||
{body: {$regex: terms.query, $options: 'i'}}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
Telescope.callbacks.add("posts.parameters", addSearchQueryParameter);
|
||||
```
|
||||
|
||||
The callback takes two arguments: the current MongoDB `parameters` (an object with a `selector` and `options` properties), and the `terms` extracted from the URL.
|
||||
|
||||
It then tests for the presence of a `query` property in the `terms`, and if it finds one it then extends the `parameter` object with a MongoDB RegEx search query.
|
||||
|
||||
Finally, it then returns `parameters` to pass it on to the next callback (or to the database itself if this happens to be the last callback).
|
||||
|
||||
The `view`, `category`, `after`, `before`, etc. URL parameters are all handled using their own similar callbacks.
|
||||
|
||||
## Forms
|
||||
|
||||
See [nova:forms](https://github.com/TelescopeJS/Telescope/tree/devel/packages/nova-forms) package readme.
|
||||
|
||||
## Methods
|
||||
|
||||
You can use regular Meteor methods, or [Smart Methods](https://github.com/meteor-utilities/smart-methods).
|
||||
|
||||
## Routes
|
||||
|
||||
### Customizing Routes
|
||||
|
||||
Here's how you can add child routes to your app (using React Router):
|
||||
|
||||
```js
|
||||
Telescope.routes.add({
|
||||
name: "foo",
|
||||
path: "/foo",
|
||||
component: Foo
|
||||
});
|
||||
```
|
||||
|
||||
To change the index (`/`) route, you can do:
|
||||
|
||||
```js
|
||||
Telescope.routes.indexRoute = {
|
||||
name: "myIndexRoute",
|
||||
component: myIndexRouteComponent
|
||||
};
|
||||
```
|
||||
|
||||
For more complex router customizations, you can also disable the `nova:base-routes` package altogether and replace it with your own React Router code.
|
||||
|
||||
### Using React Router In Your Components
|
||||
|
||||
If you need to access router properties (such as the current route, path, or query parameters) inside a component, you'll need to wrap that component with the `withRouter` HoC (higher-order component):
|
||||
|
||||
```js
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { withRouter } from 'react-router'
|
||||
|
||||
class SearchForm extends Component{
|
||||
|
||||
render() {
|
||||
// this.props.router is accessible
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(SearchForm);
|
||||
```
|
||||
|
||||
## Groups & Permissions
|
||||
|
||||
User groups let you give your users permission to perform specific actions.
|
||||
|
||||
To test if a user can perform an action, we don't check if they belong to a specific group (e.g. `user.isAdmin === true`), but instead if *at least one of the groups they belong to* has the rights to perform the current action.
|
||||
|
||||
### Permissions API
|
||||
|
||||
```js
|
||||
|
||||
Users.createGroup(groupName); // create a new group
|
||||
|
||||
Users.methods.addGroup(userId, groupName); // add a user to a group (server only)
|
||||
|
||||
Users.getGroups(user); // get a list of all the groups a user belongs to
|
||||
|
||||
Users.getActions(user); // get a list of all the actions a user can perform
|
||||
|
||||
Users.canDo(user, action); // check if a user can perform a specific action
|
||||
|
||||
Users.canView(user, document); // shortcut to check if a user can view a specific document
|
||||
|
||||
Users.canEdit(user, document); // shortcut to check if a user can edit a specific document
|
||||
```
|
||||
|
||||
Documents can be Posts, Comments, or Users.
|
||||
|
||||
Note that some groups are applied automatically without having to call `addToGroup`:
|
||||
|
||||
- `anonymous`: any non-logged-in user is considered anonymous. This group is special in that anonymous users are by definition not part of any other group.
|
||||
- `default`: default group for all existing users. Is applied to every user in addition to any other groups.
|
||||
- `admins`: any user with the `isAdmin` flag set to true.
|
||||
|
||||
### Assigning Actions
|
||||
|
||||
```js
|
||||
// assuming we've created a new "mods" group
|
||||
Users.groups.mods.can("posts.edit.all"); // mods can edit anybody's posts
|
||||
Users.groups.mods.can("posts.remove.all"); // mods can delete anybody's posts
|
||||
```
|
||||
|
||||
You can also define your own custom actions:
|
||||
|
||||
```js
|
||||
Users.groups.mods.can("invite"); // new custom action
|
||||
```
|
||||
|
||||
Here's a list of all out-of-the-box permissions:
|
||||
|
||||
```js
|
||||
// anonymous actions
|
||||
posts.view.approved.own
|
||||
posts.view.approved.all
|
||||
comments.view.own
|
||||
comments.view.all
|
||||
categories.view.all
|
||||
|
||||
// default actions
|
||||
posts.view.approved.own
|
||||
posts.view.approved.all
|
||||
posts.view.pending.own
|
||||
posts.view.rejected.own
|
||||
posts.view.spam.own
|
||||
posts.view.deleted.own
|
||||
posts.new
|
||||
posts.edit.own
|
||||
posts.remove.own
|
||||
posts.upvote
|
||||
posts.cancelUpvote
|
||||
posts.downvote
|
||||
posts.cancelDownvote
|
||||
comments.view.own
|
||||
comments.view.all
|
||||
comments.new
|
||||
comments.edit.own
|
||||
comments.remove.own
|
||||
comments.upvote
|
||||
comments.cancelUpvote
|
||||
comments.downvote
|
||||
comments.cancelDownvote
|
||||
users.edit.own
|
||||
users.remove.own
|
||||
categories.view.all
|
||||
|
||||
// admin actions
|
||||
posts.view.pending.all
|
||||
posts.view.rejected.all
|
||||
posts.view.spam.all
|
||||
posts.view.deleted.all
|
||||
posts.new.approved
|
||||
posts.edit.all
|
||||
posts.remove.all
|
||||
comments.edit.all
|
||||
comments.remove.all
|
||||
users.edit.all
|
||||
users.remove.all
|
||||
categories.view.all
|
||||
categories.new
|
||||
categories.edit.all
|
||||
categories.remove.all
|
||||
```
|
||||
|
||||
The `*.*.all` actions are generally used as a proxy to check for permission when editing restricted properties. For example, to check if a user can edit a post's `status`, a check is made for the user's ability to perform the `posts.edit.all` action (as there is no dedicated `posts.edit.status` action).
|
||||
|
||||
## Internationalization
|
||||
|
||||
Nova is internationalized using [react-intl](https://github.com/yahoo/react-intl/). To add a new language, you need to:
|
||||
|
||||
1. Create a new package containing the internationalized strings (you can use `nova:i18n-en-us` as a model).
|
||||
2. Publish that package to Atmosphere and then add it to your app using `meteor add username:packagename.
|
||||
3. Set `locale` to the locale name (`fr`, `en`, `ru`, etc.) in your settings.
|
||||
|
||||
Note: make sure the locale you set matches the language package you're adding.
|
||||
|
||||
If you create a new internationalization package, let us know so we can add it here!
|
||||
|
||||
- [fr-FR](https://github.com/TelescopeJS/nova-i18n-fr-fr)
|
||||
- [es-ES](https://atmospherejs.com/fcallem/nova-i18n-es-es)
|
||||
- [pl-PL](https://atmospherejs.com/lusch/nova-i18n-pl-pl)
|
||||
- [ru-RU](https://github.com/fortunto2/nova-i18n-ru-ru)
|
||||
- [de-DE](https://atmospherejs.com/fzeidler/nova-i18n-de-de)
|
||||
- [pt-BR](https://github.com/lukasag/nova-i18n-pt-br)
|
||||
- [zh-CN](https://github.com/qge/nova-i18n-zh-cn)
|
||||
|
||||
## Cheatsheet
|
||||
|
||||
You can access a dynamically generated cheatsheet of Nova's main functions at [http://localhost:3000/cheatsheet](/cheatsheet) (replace with your own development URL).
|
||||
|
||||
## Third-Party Packages
|
||||
|
||||
- [Post By Feed](https://github.com/xavcz/nova-post-by-feed): register RSS feeds that will be fetched every 30 minutes to create new posts automatically.
|
||||
- [Post To Slack](https://github.com/xavcz/nova-slack): A package that automatically sends your posts as messages to any connected Slack Team.
|
||||
- [Upload Images](https://github.com/xavcz/nova-forms-upload): A package that extends [nova:forms](https://github.com/TelescopeJS/Telescope/tree/master/packages/nova-forms) to upload images, like an avatar, to Cloudinary from a drop zone.
|
||||
You can find the even older, non-React version of Telescope on the [legacy](https://github.com/TelescopeJS/Telescope/tree/legacy) branch.
|
||||
|
|
25
npm-debug.log.14739240
Normal file
25
npm-debug.log.14739240
Normal file
|
@ -0,0 +1,25 @@
|
|||
0 info it worked if it ends with ok
|
||||
1 verbose cli [ '/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/node',
|
||||
1 verbose cli '/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/npm',
|
||||
1 verbose cli 'config',
|
||||
1 verbose cli '--loglevel=warn',
|
||||
1 verbose cli 'get',
|
||||
1 verbose cli 'prefix' ]
|
||||
2 info using npm@3.10.10
|
||||
3 info using node@v7.2.1
|
||||
4 verbose exit [ 0, true ]
|
||||
5 verbose stack Error: write EPIPE
|
||||
5 verbose stack at exports._errnoException (util.js:1022:11)
|
||||
5 verbose stack at WriteWrap.afterWrite [as oncomplete] (net.js:804:14)
|
||||
6 verbose cwd /Users/sachagreif/Dev/Telescope
|
||||
7 error Darwin 16.3.0
|
||||
8 error argv "/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/node" "/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/npm" "config" "--loglevel=warn" "get" "prefix"
|
||||
9 error node v7.2.1
|
||||
10 error npm v3.10.10
|
||||
11 error code EPIPE
|
||||
12 error errno EPIPE
|
||||
13 error syscall write
|
||||
14 error write EPIPE
|
||||
15 error If you need help, you may report this error at:
|
||||
15 error <https://github.com/npm/npm/issues>
|
||||
16 verbose exit [ 1, true ]
|
25
npm-debug.log.748773427
Normal file
25
npm-debug.log.748773427
Normal file
|
@ -0,0 +1,25 @@
|
|||
0 info it worked if it ends with ok
|
||||
1 verbose cli [ '/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/node',
|
||||
1 verbose cli '/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/npm',
|
||||
1 verbose cli 'config',
|
||||
1 verbose cli '--loglevel=warn',
|
||||
1 verbose cli 'get',
|
||||
1 verbose cli 'prefix' ]
|
||||
2 info using npm@3.10.10
|
||||
3 info using node@v7.2.1
|
||||
4 verbose exit [ 0, true ]
|
||||
5 verbose stack Error: write EPIPE
|
||||
5 verbose stack at exports._errnoException (util.js:1022:11)
|
||||
5 verbose stack at WriteWrap.afterWrite [as oncomplete] (net.js:804:14)
|
||||
6 verbose cwd /Users/sachagreif/Dev/Telescope
|
||||
7 error Darwin 16.3.0
|
||||
8 error argv "/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/node" "/Users/sachagreif/.nvm/versions/node/v7.2.1/bin/npm" "config" "--loglevel=warn" "get" "prefix"
|
||||
9 error node v7.2.1
|
||||
10 error npm v3.10.10
|
||||
11 error code EPIPE
|
||||
12 error errno EPIPE
|
||||
13 error syscall write
|
||||
14 error write EPIPE
|
||||
15 error If you need help, you may report this error at:
|
||||
15 error <https://github.com/npm/npm/issues>
|
||||
16 verbose exit [ 1, true ]
|
39
package.json
39
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Nova",
|
||||
"version": "0.27.5",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"npm": "^3.0"
|
||||
},
|
||||
|
@ -8,40 +8,61 @@
|
|||
"lint": "eslint --cache --ext .jsx,js packages"
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-client": "^0.7.3",
|
||||
"babel-runtime": "^6.18.0",
|
||||
"bcrypt": "^0.8.7",
|
||||
"body-parser": "^1.15.2",
|
||||
"bootstrap": "^4.0.0-alpha.2",
|
||||
"classnames": "^2.2.3",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"deepmerge": "^1.2.0",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"express": "^4.14.0",
|
||||
"formsy-react": "^0.18.1",
|
||||
"formsy-react-components": "^0.8.1",
|
||||
"graphql": "^0.8.2",
|
||||
"graphql-date": "^1.0.2",
|
||||
"graphql-server-express": "^0.4.4",
|
||||
"graphql-subscriptions": "^0.2.1",
|
||||
"graphql-tag": "^1.2.1",
|
||||
"graphql-tools": "^0.9.0",
|
||||
"graphql-type-json": "^0.1.4",
|
||||
"handlebars": "^4.0.5",
|
||||
"history": "^3.0.0",
|
||||
"hoist-non-react-statics": "^1.2.0",
|
||||
"html-to-text": "^2.1.0",
|
||||
"immutability-helper": "^2.0.0",
|
||||
"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",
|
||||
"meteor-node-stubs": "^0.2.3",
|
||||
"mingo": "^0.8.1",
|
||||
"moment": "^2.13.0",
|
||||
"react": "^15.3.2",
|
||||
"react-addons-pure-render-mixin": "^15.3.2",
|
||||
"react-bootstrap": "0.30.3",
|
||||
"optics-agent": "^1.0.5",
|
||||
"react": "^15.4.1",
|
||||
"react-addons-pure-render-mixin": "^15.4.1",
|
||||
"react-apollo": "^0.8.1",
|
||||
"react-bootstrap": "^0.30.7",
|
||||
"react-bootstrap-datetimepicker": "0.0.22",
|
||||
"react-cookie": "^0.4.6",
|
||||
"react-datetime": "^2.3.2",
|
||||
"react-dom": "^15.3.2",
|
||||
"react-dom": "^15.4.1",
|
||||
"react-helmet": "^3.1.0",
|
||||
"react-intl": "^2.1.3",
|
||||
"react-komposer": "^1.8.0",
|
||||
"react-mounter": "^1.2.0",
|
||||
"react-no-ssr": "^1.1.0",
|
||||
"react-router": "^3.0.0-beta.1",
|
||||
"react-redux": "^5.0.1",
|
||||
"react-router": "^3.0.0",
|
||||
"react-router-bootstrap": "^0.23.1",
|
||||
"react-router-scroll": "^0.4.1",
|
||||
"recompose": "^0.21.2",
|
||||
"redux": "^3.6.0",
|
||||
"rss": "^1.2.1",
|
||||
"sanitize-html": "^1.11.4",
|
||||
"speakingurl": "^9.0.0",
|
||||
"tracker-component": "^1.3.14",
|
||||
"underscore": "^1.8.3",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"private": true,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Change Log
|
||||
|
||||
### v2.0.1-nova-patch
|
||||
### v2.0.1-patch
|
||||
We use this patch to avoid data injection failure during server-side rendering on Meteor 1.4.
|
||||
All the credits for this package goes to Arunoda, Kadira's team & @rigconfig.
|
||||
See https://github.com/meteor/meteor/issues/7992
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
/* eslint-disable */
|
||||
|
||||
InjectData = {};
|
||||
InjectData = {};
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
Package.describe({
|
||||
"summary": "A way to inject data to the client with initial HTML",
|
||||
"version": "2.0.1-nova-patch",
|
||||
"version": "2.0.1-patch",
|
||||
"git": "https://github.com/meteorhacks/inject-data",
|
||||
"name": "meteorhacks:inject-data"
|
||||
});
|
||||
|
|
1
packages/_react-router-ssr/.npm/package/.gitignore
vendored
Normal file
1
packages/_react-router-ssr/.npm/package/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules
|
579
packages/_react-router-ssr/.npm/package/npm-shrinkwrap.json
generated
Normal file
579
packages/_react-router-ssr/.npm/package/npm-shrinkwrap.json
generated
Normal file
|
@ -0,0 +1,579 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"abab": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.3.tgz",
|
||||
"from": "abab@>=1.0.0 <2.0.0"
|
||||
},
|
||||
"acorn": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz",
|
||||
"from": "acorn@>=2.4.0 <3.0.0"
|
||||
},
|
||||
"acorn-globals": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz",
|
||||
"from": "acorn-globals@>=1.0.4 <2.0.0"
|
||||
},
|
||||
"amdefine": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
|
||||
"from": "amdefine@>=0.0.4"
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz",
|
||||
"from": "ansi-regex@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
|
||||
"from": "ansi-styles@>=2.2.1 <3.0.0"
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
|
||||
"from": "asn1@>=0.2.3 <0.3.0"
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
|
||||
"from": "assert-plus@>=0.2.0 <0.3.0"
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"from": "asynckit@>=0.4.0 <0.5.0"
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
|
||||
"from": "aws-sign2@>=0.6.0 <0.7.0"
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.5.0.tgz",
|
||||
"from": "aws4@>=1.2.1 <2.0.0"
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz",
|
||||
"from": "bcrypt-pbkdf@>=1.0.0 <2.0.0"
|
||||
},
|
||||
"boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"from": "boolbase@>=1.0.0 <1.1.0"
|
||||
},
|
||||
"boom": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
|
||||
"from": "boom@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"caseless": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
|
||||
"from": "caseless@>=0.11.0 <0.12.0"
|
||||
},
|
||||
"chalk": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
"from": "chalk@>=1.1.1 <2.0.0"
|
||||
},
|
||||
"cheerio": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.20.0.tgz",
|
||||
"from": "cheerio@0.20.0"
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
|
||||
"from": "combined-stream@>=1.0.5 <1.1.0"
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
|
||||
"from": "commander@>=2.9.0 <3.0.0"
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.2.3.tgz",
|
||||
"from": "cookie@0.2.3"
|
||||
},
|
||||
"cookie-parser": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.1.tgz",
|
||||
"from": "cookie-parser@1.4.1"
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"from": "cookie-signature@1.0.6"
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"from": "core-util-is@>=1.0.0 <1.1.0"
|
||||
},
|
||||
"cryptiles": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
|
||||
"from": "cryptiles@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"css-select": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
||||
"from": "css-select@>=1.2.0 <1.3.0"
|
||||
},
|
||||
"css-what": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz",
|
||||
"from": "css-what@>=2.1.0 <2.2.0"
|
||||
},
|
||||
"cssom": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.1.tgz",
|
||||
"from": "cssom@>=0.3.0 <0.4.0"
|
||||
},
|
||||
"cssstyle": {
|
||||
"version": "0.2.37",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz",
|
||||
"from": "cssstyle@>=0.2.29 <0.3.0"
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"from": "dashdash@>=1.12.0 <2.0.0",
|
||||
"dependencies": {
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"from": "assert-plus@>=1.0.0 <2.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deep-is": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
|
||||
"from": "deep-is@>=0.1.3 <0.2.0"
|
||||
},
|
||||
"deepmerge": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-0.2.10.tgz",
|
||||
"from": "deepmerge@0.2.10"
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"from": "delayed-stream@>=1.0.0 <1.1.0"
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
|
||||
"from": "dom-serializer@>=0.1.0 <0.2.0",
|
||||
"dependencies": {
|
||||
"domelementtype": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
|
||||
"from": "domelementtype@>=1.1.1 <1.2.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domelementtype": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
|
||||
"from": "domelementtype@>=1.0.0 <2.0.0"
|
||||
},
|
||||
"domhandler": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz",
|
||||
"from": "domhandler@>=2.3.0 <2.4.0"
|
||||
},
|
||||
"domutils": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
|
||||
"from": "domutils@1.5.1"
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
|
||||
"from": "ecc-jsbn@>=0.1.1 <0.2.0"
|
||||
},
|
||||
"entities": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
|
||||
"from": "entities@>=1.1.1 <1.2.0"
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"from": "escape-string-regexp@>=1.0.2 <2.0.0"
|
||||
},
|
||||
"escodegen": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz",
|
||||
"from": "escodegen@>=1.6.1 <2.0.0"
|
||||
},
|
||||
"esprima": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
|
||||
"from": "esprima@>=2.7.1 <3.0.0"
|
||||
},
|
||||
"estraverse": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz",
|
||||
"from": "estraverse@>=1.9.1 <2.0.0"
|
||||
},
|
||||
"esutils": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
|
||||
"from": "esutils@>=2.0.2 <3.0.0"
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz",
|
||||
"from": "extend@>=3.0.0 <3.1.0"
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz",
|
||||
"from": "extsprintf@1.0.2"
|
||||
},
|
||||
"fast-levenshtein": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.5.tgz",
|
||||
"from": "fast-levenshtein@>=2.0.4 <2.1.0"
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"from": "forever-agent@>=0.6.1 <0.7.0"
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.2.tgz",
|
||||
"from": "form-data@>=2.1.1 <2.2.0"
|
||||
},
|
||||
"generate-function": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz",
|
||||
"from": "generate-function@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"generate-object-property": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
|
||||
"from": "generate-object-property@>=1.1.0 <2.0.0"
|
||||
},
|
||||
"getpass": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz",
|
||||
"from": "getpass@>=0.1.1 <0.2.0",
|
||||
"dependencies": {
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"from": "assert-plus@>=1.0.0 <2.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graceful-readlink": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
|
||||
"from": "graceful-readlink@>=1.0.0"
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
|
||||
"from": "har-validator@>=2.0.6 <2.1.0"
|
||||
},
|
||||
"has-ansi": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
|
||||
"from": "has-ansi@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"hawk": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
|
||||
"from": "hawk@>=3.1.3 <3.2.0"
|
||||
},
|
||||
"hoek": {
|
||||
"version": "2.16.3",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
|
||||
"from": "hoek@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"htmlparser2": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz",
|
||||
"from": "htmlparser2@>=3.8.1 <3.9.0",
|
||||
"dependencies": {
|
||||
"entities": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz",
|
||||
"from": "entities@>=1.0.0 <1.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
|
||||
"from": "http-signature@>=1.1.0 <1.2.0"
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"from": "inherits@>=2.0.1 <2.1.0"
|
||||
},
|
||||
"is-my-json-valid": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz",
|
||||
"from": "is-my-json-valid@>=2.12.4 <3.0.0"
|
||||
},
|
||||
"is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"from": "is-property@>=1.0.0 <2.0.0"
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"from": "is-typedarray@>=1.0.0 <1.1.0"
|
||||
},
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"from": "isarray@0.0.1"
|
||||
},
|
||||
"isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
"from": "isstream@>=0.1.2 <0.2.0"
|
||||
},
|
||||
"jodid25519": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz",
|
||||
"from": "jodid25519@>=1.0.0 <2.0.0"
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz",
|
||||
"from": "jsbn@>=0.1.0 <0.2.0"
|
||||
},
|
||||
"jsdom": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz",
|
||||
"from": "jsdom@>=7.0.2 <8.0.0"
|
||||
},
|
||||
"json-schema": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||
"from": "json-schema@0.2.3"
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"from": "json-stringify-safe@>=5.0.1 <5.1.0"
|
||||
},
|
||||
"jsonpointer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.0.tgz",
|
||||
"from": "jsonpointer@>=4.0.0 <5.0.0"
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.1.tgz",
|
||||
"from": "jsprim@>=1.2.2 <2.0.0"
|
||||
},
|
||||
"levn": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
|
||||
"from": "levn@>=0.3.0 <0.4.0"
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.2.tgz",
|
||||
"from": "lodash@>=4.1.0 <5.0.0"
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz",
|
||||
"from": "mime-db@>=1.25.0 <1.26.0"
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.13",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz",
|
||||
"from": "mime-types@>=2.1.7 <2.2.0"
|
||||
},
|
||||
"nth-check": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
|
||||
"from": "nth-check@>=1.0.1 <1.1.0"
|
||||
},
|
||||
"nwmatcher": {
|
||||
"version": "1.3.9",
|
||||
"resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.3.9.tgz",
|
||||
"from": "nwmatcher@>=1.3.7 <2.0.0"
|
||||
},
|
||||
"oauth-sign": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
|
||||
"from": "oauth-sign@>=0.8.1 <0.9.0"
|
||||
},
|
||||
"optionator": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
|
||||
"from": "optionator@>=0.8.1 <0.9.0"
|
||||
},
|
||||
"parse5": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz",
|
||||
"from": "parse5@>=1.5.1 <2.0.0"
|
||||
},
|
||||
"pinkie": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
||||
"from": "pinkie@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"pinkie-promise": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
|
||||
"from": "pinkie-promise@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"prelude-ls": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
|
||||
"from": "prelude-ls@>=1.1.2 <1.2.0"
|
||||
},
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"from": "punycode@>=1.4.1 <2.0.0"
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.3.0.tgz",
|
||||
"from": "qs@>=6.3.0 <6.4.0"
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"from": "readable-stream@>=1.1.0 <1.2.0"
|
||||
},
|
||||
"request": {
|
||||
"version": "2.79.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz",
|
||||
"from": "request@>=2.55.0 <3.0.0"
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
|
||||
"from": "sax@>=1.1.4 <2.0.0"
|
||||
},
|
||||
"sntp": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
|
||||
"from": "sntp@>=1.0.0 <2.0.0"
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
|
||||
"from": "source-map@>=0.2.0 <0.3.0"
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.10.1.tgz",
|
||||
"from": "sshpk@>=1.7.0 <2.0.0",
|
||||
"dependencies": {
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"from": "assert-plus@>=1.0.0 <2.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"from": "string_decoder@>=0.10.0 <0.11.0"
|
||||
},
|
||||
"stringstream": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
|
||||
"from": "stringstream@>=0.0.4 <0.1.0"
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"from": "strip-ansi@>=3.0.0 <4.0.0"
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
|
||||
"from": "supports-color@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"symbol-tree": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.1.4.tgz",
|
||||
"from": "symbol-tree@>=3.1.0 <4.0.0"
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz",
|
||||
"from": "tough-cookie@>=2.2.0 <3.0.0"
|
||||
},
|
||||
"tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"from": "tr46@>=0.0.1 <0.1.0"
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
|
||||
"from": "tunnel-agent@>=0.4.1 <0.5.0"
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "0.14.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.3.tgz",
|
||||
"from": "tweetnacl@>=0.14.0 <0.15.0"
|
||||
},
|
||||
"type-check": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||
"from": "type-check@>=0.3.2 <0.4.0"
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz",
|
||||
"from": "uuid@>=3.0.0 <4.0.0"
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz",
|
||||
"from": "verror@1.3.6"
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-2.0.1.tgz",
|
||||
"from": "webidl-conversions@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"whatwg-url-compat": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url-compat/-/whatwg-url-compat-0.6.5.tgz",
|
||||
"from": "whatwg-url-compat@>=0.6.5 <0.7.0"
|
||||
},
|
||||
"wordwrap": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
"from": "wordwrap@>=1.0.0 <1.1.0"
|
||||
},
|
||||
"xml-name-validator": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz",
|
||||
"from": "xml-name-validator@>=2.0.1 <3.0.0"
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
||||
"from": "xtend@>=4.0.0 <5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, browserHistory } from 'react-router';
|
||||
import Cookie from 'react-cookie';
|
||||
|
||||
const ReactRouterSSR = {
|
||||
Run(routes, clientOptions) {
|
||||
|
@ -54,7 +55,8 @@ const ReactRouterSSR = {
|
|||
);
|
||||
|
||||
if (typeof clientOptions.wrapperHook === 'function') {
|
||||
app = clientOptions.wrapperHook(app);
|
||||
const loginToken = Cookie.load('meteor_login_token') || localStorage['Meteor.loginToken'];
|
||||
app = clientOptions.wrapperHook(app, loginToken);
|
||||
}
|
||||
|
||||
if (typeof clientOptions.renderHook === 'function') {
|
||||
|
|
|
@ -184,9 +184,6 @@ function generateSSRData(clientOptions, serverOptions, req, res, renderProps) {
|
|||
if (frData) {
|
||||
ssrContext.addData(frData.collectionData);
|
||||
}
|
||||
if (serverOptions.preRender) {
|
||||
serverOptions.preRender(req, res);
|
||||
}
|
||||
|
||||
// Uncomment these two lines if you want to easily trigger
|
||||
// multiple client requests from different browsers at the same time
|
||||
|
@ -203,11 +200,16 @@ function generateSSRData(clientOptions, serverOptions, req, res, renderProps) {
|
|||
...serverOptions.props
|
||||
};
|
||||
|
||||
fetchComponentData(serverOptions, renderProps);
|
||||
// fetchComponentData(serverOptions, renderProps);
|
||||
let app = <RouterContext {...renderProps} />;
|
||||
|
||||
if (typeof clientOptions.wrapperHook === 'function') {
|
||||
app = clientOptions.wrapperHook(app);
|
||||
const loginToken = req.cookies['meteor_login_token'];
|
||||
app = clientOptions.wrapperHook(app, loginToken);
|
||||
}
|
||||
|
||||
if (serverOptions.preRender) {
|
||||
serverOptions.preRender(req, res, app);
|
||||
}
|
||||
|
||||
if (!serverOptions.disableSSR){
|
||||
|
@ -219,7 +221,8 @@ function generateSSRData(clientOptions, serverOptions, req, res, renderProps) {
|
|||
css = global.__STYLE_COLLECTOR__;
|
||||
|
||||
if (typeof serverOptions.dehydrateHook === 'function') {
|
||||
InjectData.pushData(res, 'dehydrated-initial-data', JSON.stringify(serverOptions.dehydrateHook()));
|
||||
const data = serverOptions.dehydrateHook();
|
||||
InjectData.pushData(res, 'dehydrated-initial-data', JSON.stringify(data));
|
||||
}
|
||||
|
||||
if (serverOptions.postRender) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Package.describe({
|
||||
name: 'reactrouter:react-router-ssr',
|
||||
version: '3.1.6-nova-patch',
|
||||
version: '3.1.6-patch',
|
||||
summary: 'Server-side rendering for react-router and react-meteor-data rehydratating Meteor subscriptions',
|
||||
git: 'https://github.com/thereactivestack/meteor-react-router-ssr.git',
|
||||
documentation: 'README.md'
|
||||
|
@ -19,7 +19,7 @@ Package.onUse(function(api) {
|
|||
'tracker',
|
||||
'minimongo@1.0.0',
|
||||
'meteorhacks:fast-render@2.16.0',
|
||||
'meteorhacks:inject-data@2.0.1-nova-patch',
|
||||
'meteorhacks:inject-data@2.0.1-patch',
|
||||
'tmeasday:check-npm-versions@0.2.0'
|
||||
]);
|
||||
|
||||
|
@ -34,7 +34,7 @@ Package.onUse(function(api) {
|
|||
api.use([
|
||||
'autopublish@1.0.0',
|
||||
'tmeasday:publish-counts@0.7.0',
|
||||
'promise@0.5.1'
|
||||
'promise@0.8.8'
|
||||
], 'server', {weak: true})
|
||||
|
||||
api.export('ReactRouterSSR');
|
||||
|
|
1
packages/customization-demo/client.js
Normal file
1
packages/customization-demo/client.js
Normal file
|
@ -0,0 +1 @@
|
|||
import './lib/modules.js';
|
|
@ -3,7 +3,7 @@ Let's add a callback to the new post method that
|
|||
appends a random emoji to the newly submitted post's title.
|
||||
*/
|
||||
|
||||
import Telescope from 'meteor/nova:lib';
|
||||
import { addCallback } from 'meteor/nova:core';
|
||||
|
||||
function PostsNewAddRandomEmoji (post, user) {
|
||||
|
||||
|
@ -11,4 +11,4 @@ function PostsNewAddRandomEmoji (post, user) {
|
|||
|
||||
return post;
|
||||
}
|
||||
Telescope.callbacks.add("posts.new.sync", PostsNewAddRandomEmoji);
|
||||
addCallback("posts.new.sync", PostsNewAddRandomEmoji);
|
21
packages/customization-demo/lib/components/CustomLogo.jsx
Normal file
21
packages/customization-demo/lib/components/CustomLogo.jsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
The original Logo components is defined using React's
|
||||
functional stateless component syntax, so we redefine
|
||||
it the same way.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { IndexLink } from 'react-router';
|
||||
import Users from 'meteor/nova:users';
|
||||
import { replaceComponent } from 'meteor/nova:core';
|
||||
|
||||
const CustomLogo = ({logoUrl, siteTitle, currentUser}) => {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="logo-text"><IndexLink to="/">⭐{siteTitle}⭐</IndexLink></h1>
|
||||
{ currentUser ? <span className="logo-hello">Welcome {Users.getDisplayName(currentUser)} 👋</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
replaceComponent('Logo', CustomLogo);
|
|
@ -7,24 +7,25 @@ all of the class's other methods (other render
|
|||
functions, event handlers, etc.).
|
||||
*/
|
||||
|
||||
import Telescope from 'meteor/nova:lib';
|
||||
import { Components, getRawComponent, replaceComponent }from 'meteor/nova:core';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { FormattedMessage /*, intlShape */ } from 'react-intl';
|
||||
|
||||
class CustomNewsletter extends Telescope.components.Newsletter {
|
||||
class CustomNewsletter extends getRawComponent('Newsletter') {
|
||||
|
||||
render() {
|
||||
// console.log(this.renderButton); <-- exists
|
||||
|
||||
return this.state.showBanner
|
||||
? (
|
||||
<div className="newsletter">
|
||||
<h4 className="newsletter-tagline">✉️<FormattedMessage id="newsletter.subscribe_prompt"/>✉️</h4>
|
||||
{this.context.currentUser ? this.renderButton() : this.renderForm()}
|
||||
<a onClick={this.dismissBanner} className="newsletter-close"><Telescope.components.Icon name="close"/></a>
|
||||
{this.props.currentUser ? this.renderButton() : this.renderForm()}
|
||||
<a onClick={this.dismissBanner} className="newsletter-close"><Components.Icon name="close"/></a>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CustomNewsletter;
|
||||
replaceComponent('Newsletter', CustomNewsletter);
|
114
packages/customization-demo/lib/components/CustomPostsItem.jsx
Normal file
114
packages/customization-demo/lib/components/CustomPostsItem.jsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
import { Components, getRawComponent, replaceComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { FormattedMessage, FormattedRelative } from 'react-intl';
|
||||
import { Link } from 'react-router';
|
||||
import Posts from "meteor/nova:posts";
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
class CustomPostsItem extends getRawComponent('PostsItem') {
|
||||
|
||||
render() {
|
||||
|
||||
const post = this.props.post;
|
||||
|
||||
let postClass = "posts-item";
|
||||
if (post.sticky) postClass += " posts-sticky";
|
||||
|
||||
// ⭐ custom code starts here ⭐
|
||||
if (post.color) {
|
||||
postClass += " post-"+post.color;
|
||||
}
|
||||
// ⭐ custom code ends here ⭐
|
||||
|
||||
return (
|
||||
<div className={postClass}>
|
||||
|
||||
<div className="posts-item-vote">
|
||||
<Components.Vote collection={Posts} document={post} currentUser={this.props.currentUser}/>
|
||||
</div>
|
||||
|
||||
{post.thumbnailUrl ? <Components.PostsThumbnail post={post}/> : null}
|
||||
|
||||
<div className="posts-item-content">
|
||||
|
||||
<h3 className="posts-item-title">
|
||||
<Link to={Posts.getLink(post)} className="posts-item-title-link" target={Posts.getLinkTarget(post)}>
|
||||
{post.title}
|
||||
</Link>
|
||||
{this.renderCategories()}
|
||||
</h3>
|
||||
|
||||
<div className="posts-item-meta">
|
||||
{post.user? <div className="posts-item-user"><Components.UsersAvatar user={post.user} size="small"/><Components.UsersName user={post.user}/></div> : null}
|
||||
<div className="posts-item-date"><FormattedRelative value={post.postedAt}/></div>
|
||||
<div className="posts-item-comments">
|
||||
<Link to={Posts.getPageUrl(post)}>
|
||||
<FormattedMessage id="comments.count" values={{count: post.commentCount}}/>
|
||||
</Link>
|
||||
</div>
|
||||
{this.props.currentUser && this.props.currentUser.isAdmin ? <Components.PostsStats post={post} /> : null}
|
||||
{Posts.options.mutations.edit.check(this.props.currentUser, post) ? this.renderActions() : null}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{this.renderCommenters()}
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CustomPostsItem.propTypes = {
|
||||
currentUser: React.PropTypes.object,
|
||||
post: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
CustomPostsItem.fragment = gql`
|
||||
fragment CustomPostsItemFragment on Post {
|
||||
_id
|
||||
title
|
||||
url
|
||||
slug
|
||||
thumbnailUrl
|
||||
baseScore
|
||||
postedAt
|
||||
sticky
|
||||
status
|
||||
categories {
|
||||
# ...minimumCategoryInfo
|
||||
_id
|
||||
name
|
||||
slug
|
||||
}
|
||||
commentCount
|
||||
commenters {
|
||||
# ...avatarUserInfo
|
||||
_id
|
||||
displayName
|
||||
emailHash
|
||||
slug
|
||||
}
|
||||
upvoters {
|
||||
_id
|
||||
}
|
||||
downvoters {
|
||||
_id
|
||||
}
|
||||
upvotes # should be asked only for admins?
|
||||
score # should be asked only for admins?
|
||||
viewCount # should be asked only for admins?
|
||||
clickCount # should be asked only for admins?
|
||||
user {
|
||||
# ...avatarUserInfo
|
||||
_id
|
||||
displayName
|
||||
emailHash
|
||||
slug
|
||||
}
|
||||
color
|
||||
}
|
||||
`;
|
||||
|
||||
replaceComponent('PostsItem', CustomPostsItem);
|
|
@ -4,6 +4,7 @@ Browse to http://localhost:3000/my-custom-route to see it.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { registerComponent } from 'meteor/nova:core';
|
||||
|
||||
const MyCustomPage = () => {
|
||||
return (
|
||||
|
@ -14,4 +15,4 @@ const MyCustomPage = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export default MyCustomPage;
|
||||
registerComponent('MyCustomPage', MyCustomPage);
|
|
@ -1,5 +1,4 @@
|
|||
import Posts from "meteor/nova:posts";
|
||||
import Users from 'meteor/nova:users';
|
||||
|
||||
/*
|
||||
Let's assign a color to each post (why? cause we want to, that's why).
|
||||
|
@ -7,11 +6,6 @@ We'll do that by adding a custom field to the Posts collection.
|
|||
Note that this requires our custom package to depend on nova:posts and nova:users.
|
||||
*/
|
||||
|
||||
// check if user can create a new post
|
||||
const canInsert = user => Users.canDo(user, "posts.new");
|
||||
// check if user can edit a post
|
||||
const canEdit = Users.canEdit;
|
||||
|
||||
Posts.addField(
|
||||
{
|
||||
fieldName: 'color',
|
||||
|
@ -19,8 +13,9 @@ Posts.addField(
|
|||
type: String,
|
||||
control: "select", // use a select form control
|
||||
optional: true, // this field is not required
|
||||
insertableIf: canInsert,
|
||||
editableIf: canEdit,
|
||||
insertableBy: ['members'],
|
||||
editableBy: ['members'],
|
||||
viewableBy: ['members'],
|
||||
form: {
|
||||
options: function () { // options for the select form control
|
||||
return [
|
||||
|
@ -32,16 +27,6 @@ Posts.addField(
|
|||
];
|
||||
}
|
||||
},
|
||||
publish: true // make that field public and send it to the client
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
The main post list view uses a special object to determine which fields to publish,
|
||||
so we also add our new field to that object:
|
||||
*/
|
||||
|
||||
import PublicationUtils from 'meteor/utilities:smart-publications';
|
||||
|
||||
PublicationUtils.addToFields(Posts.publishedFields.list, ["color"]);
|
|
@ -2,9 +2,8 @@
|
|||
Let's add an international label to the field added in custom_fields.js
|
||||
*/
|
||||
|
||||
import Telescope from 'meteor/nova:lib';
|
||||
import { addStrings } from 'meteor/nova:core';
|
||||
|
||||
Telescope.strings.en = {
|
||||
...Telescope.strings.en, // get all the string translated
|
||||
addStrings('en', {
|
||||
"posts.color": "Color" // add a new one (collection.field: "Label")
|
||||
};
|
||||
});
|
19
packages/customization-demo/lib/modules.js
Normal file
19
packages/customization-demo/lib/modules.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Let's import all our files here.
|
||||
*/
|
||||
|
||||
// general business logic of this customization
|
||||
import "./callbacks.js"
|
||||
import "./emails.js"
|
||||
import "./custom_fields.js"
|
||||
import "./i18n.js"
|
||||
import "./groups.js"
|
||||
|
||||
// custom components
|
||||
import "./components/CustomLogo.jsx";
|
||||
import "./components/CustomNewsletter.jsx";
|
||||
import "./components/CustomPostsItem.jsx";
|
||||
import "./components/MyCustomPage.jsx";
|
||||
|
||||
// custom routes
|
||||
import "./routes.jsx";
|
8
packages/customization-demo/lib/routes.jsx
Normal file
8
packages/customization-demo/lib/routes.jsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
A new custom route for our custom page.
|
||||
Browse to http://localhost:3000/my-custom-route to see it.
|
||||
*/
|
||||
|
||||
import { addRoute, getComponent } from 'meteor/nova:core';
|
||||
|
||||
addRoute({name: "myCustomRoute", path: "/my-custom-route", component: getComponent("MyCustomPage")});
|
|
@ -1,3 +1,9 @@
|
|||
.logo-hello {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.post-red{
|
||||
background-color: #FFD3D2;
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
Package.describe({
|
||||
name: "my-custom-package"
|
||||
name: "customization-demo"
|
||||
});
|
||||
|
||||
Package.onUse( function(api) {
|
||||
|
||||
api.versionsFrom("METEOR@1.0");
|
||||
|
||||
api.use([
|
||||
'fourseven:scss',
|
||||
|
||||
|
@ -15,6 +13,9 @@ Package.onUse( function(api) {
|
|||
'nova:users'
|
||||
]);
|
||||
|
||||
api.mainModule('server.js', 'server');
|
||||
api.mainModule('client.js', 'client');
|
||||
|
||||
api.addFiles([
|
||||
'lib/modules.js'
|
||||
], ['client', 'server']);
|
2
packages/customization-demo/server.js
Normal file
2
packages/customization-demo/server.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import './lib/modules.js';
|
||||
import './lib/server/templates.js';
|
1
packages/framework-demo/client.js
Normal file
1
packages/framework-demo/client.js
Normal file
|
@ -0,0 +1 @@
|
|||
import './lib/modules.js';
|
26
packages/framework-demo/lib/collection.js
Normal file
26
packages/framework-demo/lib/collection.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
|
||||
The main Movies collection definition file.
|
||||
|
||||
*/
|
||||
|
||||
import schema from './schema.js';
|
||||
import mutations from './mutations.js';
|
||||
import resolvers from './resolvers.js';
|
||||
import { createCollection } from 'meteor/nova:core';
|
||||
|
||||
const Movies = createCollection({
|
||||
|
||||
collectionName: 'movies',
|
||||
|
||||
typeName: 'Movie',
|
||||
|
||||
schema,
|
||||
|
||||
resolvers,
|
||||
|
||||
mutations,
|
||||
|
||||
});
|
||||
|
||||
export default Movies;
|
7
packages/framework-demo/lib/components.js
Normal file
7
packages/framework-demo/lib/components.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import './components/Accounts.jsx';
|
||||
import './components/MoviesDetails.jsx';
|
||||
import './components/MoviesEditForm.jsx';
|
||||
import './components/MoviesItem.jsx';
|
||||
import './components/MoviesList.jsx';
|
||||
import './components/MoviesNewForm.jsx';
|
||||
import './components/MoviesWrapper.jsx';
|
59
packages/framework-demo/lib/components/Accounts.jsx
Normal file
59
packages/framework-demo/lib/components/Accounts.jsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import React, { PropTypes, Component } from 'react';
|
||||
import { Button, FormControl } from 'react-bootstrap';
|
||||
import { Accounts } from 'meteor/std:accounts-ui';
|
||||
import { withApollo } from 'react-apollo';
|
||||
import { registerComponent } from 'meteor/nova:core';
|
||||
|
||||
Accounts.ui.config({
|
||||
passwordSignupFields: 'USERNAME_AND_EMAIL',
|
||||
});
|
||||
|
||||
const AccountsForm = ({client}) => {
|
||||
return (
|
||||
<div>
|
||||
<Accounts.ui.LoginForm
|
||||
onPostSignUpHook={() => client.resetStore()}
|
||||
onSignedInHook={() => client.resetStore()}
|
||||
onSignedOutHook={() => client.resetStore()}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
class AccountsButton extends Accounts.ui.Button {
|
||||
render () {
|
||||
const {label, href, type, disabled, className, onClick} = this.props;
|
||||
if (type === 'link') {
|
||||
return <a href={ href } className={ className } onClick={ onClick }>{ label }</a>;
|
||||
}
|
||||
return <Button
|
||||
bsStyle="primary"
|
||||
className={ className }
|
||||
type={ type }
|
||||
disabled={ disabled }
|
||||
onClick={ onClick }>{ label }
|
||||
</Button>;
|
||||
}
|
||||
}
|
||||
|
||||
class AccountsField extends Accounts.ui.Field {
|
||||
|
||||
render() {
|
||||
const { id, hint, /* label, */ type = 'text', onChange, className = "field", defaultValue = "", message } = this.props;
|
||||
const { mount = true } = this.state;
|
||||
return mount ? (
|
||||
<div className={ className }>
|
||||
<FormControl id={ id } type={ type } inputRef={ref => { this.input = ref; }} onChange={ onChange } placeholder={ hint } defaultValue={ defaultValue } />
|
||||
{message && (
|
||||
<span className={['message', message.type].join(' ').trim()}>
|
||||
{message.message}</span>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
|
||||
Accounts.ui.Button = AccountsButton;
|
||||
Accounts.ui.Field = AccountsField;
|
||||
|
||||
registerComponent('AccountsForm', AccountsForm, withApollo);
|
49
packages/framework-demo/lib/components/MoviesDetails.jsx
Normal file
49
packages/framework-demo/lib/components/MoviesDetails.jsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
|
||||
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 '../collection.js';
|
||||
import { withDocument, registerComponent } from 'meteor/nova:core';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MoviesDetails.fragment = gql`
|
||||
fragment moviesDetailsFragment on Movie {
|
||||
_id
|
||||
name
|
||||
createdAt
|
||||
year
|
||||
review
|
||||
privateComments
|
||||
user {
|
||||
displayName
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const options = {
|
||||
collection: Movies,
|
||||
queryName: 'moviesSingleQuery',
|
||||
fragment: MoviesDetails.fragment,
|
||||
};
|
||||
|
||||
registerComponent('MoviesDetails', MoviesDetails, withDocument(options));
|
26
packages/framework-demo/lib/components/MoviesEditForm.jsx
Normal file
26
packages/framework-demo/lib/components/MoviesEditForm.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
|
||||
A component to configure the "edit movie" form.
|
||||
Wrapped with the "withDocument" container.
|
||||
|
||||
*/
|
||||
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { Components, registerComponent } from "meteor/nova:core";
|
||||
import Movies from '../collection.js';
|
||||
|
||||
const MoviesEditForm = (props, context) => {
|
||||
return (
|
||||
<Components.SmartForm
|
||||
collection={Movies}
|
||||
documentId={props.documentId}
|
||||
showRemove={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
MoviesEditForm.contextTypes = {
|
||||
closeCallback: React.PropTypes.func,
|
||||
}
|
||||
|
||||
registerComponent('MoviesEditForm', MoviesEditForm);
|
69
packages/framework-demo/lib/components/MoviesItem.jsx
Normal file
69
packages/framework-demo/lib/components/MoviesItem.jsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
|
||||
An item in the movies list.
|
||||
Wrapped with the "withCurrentUser" container.
|
||||
|
||||
*/
|
||||
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { ModalTrigger } from 'meteor/nova:core';
|
||||
import { Components, registerComponent, withCurrentUser } from 'meteor/nova:core';
|
||||
import Movies from '../collection.js';
|
||||
|
||||
class MoviesItem extends Component {
|
||||
|
||||
renderDetails() {
|
||||
|
||||
const movie = this.props;
|
||||
|
||||
return (
|
||||
<div style={{display: 'inline-block', marginRight: '5px'}}>
|
||||
<ModalTrigger
|
||||
label="View Details"
|
||||
component={<Button bsStyle="primary">Read Review</Button>}
|
||||
>
|
||||
<Components.MoviesDetails documentId={movie._id}/>
|
||||
</ModalTrigger>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
renderEdit() {
|
||||
|
||||
const movie = this.props;
|
||||
|
||||
return (
|
||||
<div style={{display: 'inline-block', marginRight: '5px'}}>
|
||||
<ModalTrigger
|
||||
label="Edit Movie"
|
||||
component={<Button bsStyle="primary">Edit Movie</Button>}
|
||||
>
|
||||
<Components.MoviesEditForm currentUser={this.props.currentUser} documentId={movie._id} refetch={this.props.refetch}/>
|
||||
</ModalTrigger>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const movie = this.props;
|
||||
|
||||
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()}
|
||||
{Movies.options.mutations.edit.check(this.props.currentUser, movie) ? this.renderEdit() : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MoviesItem.displayName = 'MoviesItem';
|
||||
|
||||
registerComponent('MoviesItem', MoviesItem, withCurrentUser);
|
76
packages/framework-demo/lib/components/MoviesList.jsx
Normal file
76
packages/framework-demo/lib/components/MoviesList.jsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
|
||||
List of movies.
|
||||
Wrapped with the "withList" and "withCurrentUser" containers.
|
||||
|
||||
*/
|
||||
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import gql from 'graphql-tag';
|
||||
import Movies from '../collection.js';
|
||||
import { Components, registerComponent, ModalTrigger, withList, withCurrentUser } from 'meteor/nova: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} currentUser={this.props.currentUser} refetch={this.props.refetch} />)}
|
||||
{hasMore ? <LoadMore {...this.props}/> : <p>No more movies</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MoviesList.displayName = 'MoviesList';
|
||||
|
||||
export const MoviesListFragment = gql`
|
||||
fragment moviesItemFragment on Movie {
|
||||
_id
|
||||
name
|
||||
year
|
||||
createdAt
|
||||
user {
|
||||
displayName
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const listOptions = {
|
||||
collection: Movies,
|
||||
queryName: 'moviesListQuery',
|
||||
fragment: MoviesListFragment,
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
registerComponent('MoviesList', MoviesList, withList(listOptions), withCurrentUser);
|
25
packages/framework-demo/lib/components/MoviesNewForm.jsx
Normal file
25
packages/framework-demo/lib/components/MoviesNewForm.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
|
||||
A component to configure the "new movie" form.
|
||||
|
||||
*/
|
||||
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import Movies from '../collection.js';
|
||||
import { MoviesListFragment } from './MoviesList.jsx';
|
||||
import { Components, registerComponent, withMessages } from 'meteor/nova:core';
|
||||
|
||||
const MoviesNewForm = (props, context) => {
|
||||
return (
|
||||
<Components.SmartForm
|
||||
collection={Movies}
|
||||
mutationFragment={MoviesListFragment}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
MoviesNewForm.contextTypes = {
|
||||
closeCallback: React.PropTypes.func,
|
||||
}
|
||||
|
||||
registerComponent('MoviesNewForm', MoviesNewForm, withMessages);
|
28
packages/framework-demo/lib/components/MoviesWrapper.jsx
Normal file
28
packages/framework-demo/lib/components/MoviesWrapper.jsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
|
||||
Wrapper for the Movies components
|
||||
|
||||
*/
|
||||
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { Components, registerComponent } from 'meteor/nova:core';
|
||||
|
||||
class MoviesWrapper extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="wrapper framework-demo">
|
||||
|
||||
<div className="header">
|
||||
<Components.AccountsForm />
|
||||
</div>
|
||||
|
||||
<div className="main">
|
||||
<Components.MoviesList />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
registerComponent('MoviesWrapper', MoviesWrapper);
|
12
packages/framework-demo/lib/i18n.js
Normal file
12
packages/framework-demo/lib/i18n.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
|
||||
Add strings for internationalization.
|
||||
|
||||
*/
|
||||
|
||||
import { addStrings } from 'meteor/nova:core';
|
||||
|
||||
addStrings('en', {
|
||||
'movies.delete': "Delete Movie",
|
||||
'movies.delete_confirm': "Delete Movie?"
|
||||
});
|
7
packages/framework-demo/lib/modules.js
Normal file
7
packages/framework-demo/lib/modules.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import './collection.js';
|
||||
import './i18n.js';
|
||||
import './permissions.js';
|
||||
import './components.js';
|
||||
import './routes.js';
|
||||
import './schema.js';
|
||||
import './parameters.js';
|
104
packages/framework-demo/lib/mutations.js
Normal file
104
packages/framework-demo/lib/mutations.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
|
||||
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 } from 'meteor/nova:core';
|
||||
import Users from 'meteor/nova:users';
|
||||
|
||||
const performCheck = (mutation, user, document) => {
|
||||
if (!mutation.check(user, document)) throw new Error(`Sorry, you don't have the rights to perform the mutation ${mutation.name} on document _id = ${document._id}`);
|
||||
}
|
||||
|
||||
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;
|
16
packages/framework-demo/lib/parameters.js
Normal file
16
packages/framework-demo/lib/parameters.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
|
||||
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/nova:core';
|
||||
|
||||
function sortByCreatedAt (parameters, terms) {
|
||||
return {options: {sort: {createdAt: -1}}};
|
||||
}
|
||||
|
||||
addCallback("movies.parameters", sortByCreatedAt);
|
20
packages/framework-demo/lib/permissions.js
Normal file
20
packages/framework-demo/lib/permissions.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import Users from 'meteor/nova:users';
|
||||
|
||||
const guestsActions = [
|
||||
'movies.view.own',
|
||||
'movies.view.all',
|
||||
];
|
||||
Users.groups.guests.can(membersActions);
|
||||
|
||||
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);
|
62
packages/framework-demo/lib/resolvers.js
Normal file
62
packages/framework-demo/lib/resolvers.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
|
||||
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/nova: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;
|
4
packages/framework-demo/lib/routes.js
Normal file
4
packages/framework-demo/lib/routes.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { addRoute, getComponent } from 'meteor/nova:core';
|
||||
|
||||
// add new "/movies" route that loads the MoviesWrapper component
|
||||
addRoute({ name: 'movies', path: 'movies', component: getComponent('MoviesWrapper') });
|
64
packages/framework-demo/lib/schema.js
Normal file
64
packages/framework-demo/lib/schema.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
|
||||
A SimpleSchema-compatible JSON schema
|
||||
|
||||
*/
|
||||
|
||||
import Users from 'meteor/nova:users';
|
||||
import { GraphQLSchema } from 'meteor/nova:core';
|
||||
|
||||
// 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;
|
80
packages/framework-demo/lib/seed.js
Normal file
80
packages/framework-demo/lib/seed.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
|
||||
Seed the database with some dummy content.
|
||||
|
||||
*/
|
||||
|
||||
import Movies from './collection.js';
|
||||
import Users from 'meteor/nova:users';
|
||||
import { newMutation } from 'meteor/nova: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
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
4
packages/framework-demo/lib/style.css
Normal file
4
packages/framework-demo/lib/style.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.framework-demo .accounts-ui{
|
||||
border: 1px solid #ccc;
|
||||
margin-bottom: 20px;
|
||||
}
|
19
packages/framework-demo/package.js
Normal file
19
packages/framework-demo/package.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
Package.describe({
|
||||
name: 'framework-demo',
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
api.use([
|
||||
'nova:core@1.0.0',
|
||||
'nova:forms@1.0.0',
|
||||
|
||||
'std:accounts-ui@1.2.17',
|
||||
]);
|
||||
|
||||
api.addFiles('lib/style.css', 'client');
|
||||
|
||||
api.mainModule('server.js', 'server');
|
||||
api.mainModule('client.js', 'client');
|
||||
|
||||
});
|
3
packages/framework-demo/server.js
Normal file
3
packages/framework-demo/server.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import './lib/modules.js';
|
||||
|
||||
import './lib/seed.js';
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
This file centralizes all our custom component overrides.
|
||||
*/
|
||||
|
||||
import Telescope from 'meteor/nova:lib';
|
||||
|
||||
import CustomLogo from "./components/CustomLogo.jsx";
|
||||
import CustomNewsletter from "./components/CustomNewsletter.jsx";
|
||||
import CustomPostsItem from "./components/CustomPostsItem.jsx";
|
||||
|
||||
Telescope.components.Logo = CustomLogo;
|
||||
Telescope.components.Newsletter = CustomNewsletter;
|
||||
Telescope.components.PostsItem = CustomPostsItem;
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
The original Logo components is defined using React's
|
||||
functional stateless component syntax, so we redefine
|
||||
it the same way.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { IndexLink } from 'react-router';
|
||||
|
||||
const CustomLogo = ({logoUrl, siteTitle}) => {
|
||||
return (
|
||||
<h1 className="logo-text"><IndexLink to="/">⭐{siteTitle}⭐</IndexLink></h1>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomLogo;
|
|
@ -1,74 +0,0 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import Posts from "meteor/nova:posts";
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { FormattedMessage, FormattedRelative } from 'react-intl';
|
||||
import { Link } from 'react-router';
|
||||
// import { Button } from 'react-bootstrap';
|
||||
// import moment from 'moment';
|
||||
// import { ModalTrigger } from "meteor/nova:core";
|
||||
// import Categories from "meteor/nova:categories";
|
||||
|
||||
class CustomPostsItem extends Telescope.components.PostsItem {
|
||||
|
||||
render() {
|
||||
|
||||
const post = this.props.post;
|
||||
|
||||
let postClass = "posts-item";
|
||||
if (post.sticky) postClass += " posts-sticky";
|
||||
|
||||
// ⭐ custom code starts here ⭐
|
||||
if (post.color) {
|
||||
postClass += " post-"+post.color;
|
||||
}
|
||||
// ⭐ custom code ends here ⭐
|
||||
|
||||
return (
|
||||
<div className={postClass}>
|
||||
|
||||
<div className="posts-item-vote">
|
||||
<Telescope.components.Vote post={post} />
|
||||
</div>
|
||||
|
||||
{post.thumbnailUrl ? <Telescope.components.PostsThumbnail post={post}/> : null}
|
||||
|
||||
<div className="posts-item-content">
|
||||
|
||||
<h3 className="posts-item-title">
|
||||
<Link to={Posts.getLink(post)} className="posts-item-title-link" target={Posts.getLinkTarget(post)}>
|
||||
{post.title}
|
||||
</Link>
|
||||
{this.renderCategories()}
|
||||
</h3>
|
||||
|
||||
<div className="posts-item-meta">
|
||||
{post.user? <div className="posts-item-user"><Telescope.components.UsersAvatar user={post.user} size="small"/><Telescope.components.UsersName user={post.user}/></div> : null}
|
||||
<div className="posts-item-date"><FormattedRelative value={post.postedAt}/></div>
|
||||
<div className="posts-item-comments">
|
||||
<Link to={Posts.getPageUrl(post)}>
|
||||
<FormattedMessage id="comments.count" values={{count: post.commentCount}}/>
|
||||
</Link>
|
||||
</div>
|
||||
{this.context.currentUser && this.context.currentUser.isAdmin ? <Telescope.components.PostsStats post={post} /> : null}
|
||||
{this.renderActions()}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{this.renderCommenters()}
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CustomPostsItem.propTypes = {
|
||||
post: React.PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
CustomPostsItem.contextTypes = {
|
||||
currentUser: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default CustomPostsItem;
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
Let's import all our files here.
|
||||
*/
|
||||
|
||||
import "./callbacks.js"
|
||||
import "./emails.js"
|
||||
import "./components.js"
|
||||
import "./custom_fields.js"
|
||||
import "./intl.js"
|
||||
import "./groups.js"
|
||||
import "./routes.jsx"
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
A new custom route for our custom page.
|
||||
Browse to http://localhost:3000/my-custom-route to see it.
|
||||
*/
|
||||
|
||||
import Telescope from 'meteor/nova:lib';
|
||||
import MyCustomPage from './components/MyCustomPage.jsx';
|
||||
|
||||
Telescope.routes.add({name:"myCustomRoute", path:"/my-custom-route", component:MyCustomPage});
|
|
@ -1,12 +1,12 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import Posts from "meteor/nova:posts";
|
||||
import Comments from "meteor/nova:comments";
|
||||
import Users from 'meteor/nova:users';
|
||||
import { Utils } from 'meteor/nova:core';
|
||||
|
||||
export const servePostsApi = (terms) => {
|
||||
var posts = [];
|
||||
|
||||
var parameters = Posts.parameters.get(terms);
|
||||
var parameters = Posts.getParameters(terms);
|
||||
|
||||
const postsCursor = Posts.find(parameters.selector, parameters.options);
|
||||
|
||||
|
@ -26,10 +26,10 @@ export const servePostsApi = (terms) => {
|
|||
postOutput.body = post.body;
|
||||
|
||||
if(post.url)
|
||||
postOutput.domain = Telescope.utils.getDomain(url);
|
||||
postOutput.domain = Utils.getDomain(url);
|
||||
|
||||
if (post.thumbnailUrl) {
|
||||
postOutput.thumbnailUrl = Telescope.utils.addHttp(post.thumbnailUrl);
|
||||
postOutput.thumbnailUrl = Utils.addHttp(post.thumbnailUrl);
|
||||
}
|
||||
|
||||
var twitterName = Users.getTwitterNameById(post.userId);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Package.describe({
|
||||
name: "nova:api",
|
||||
summary: "Telescope API package",
|
||||
version: "0.27.5-nova",
|
||||
version: "1.0.0",
|
||||
git: "https://github.com/TelescopeJS/Telescope.git"
|
||||
});
|
||||
|
||||
|
@ -9,7 +9,7 @@ Package.onUse(function (api) {
|
|||
|
||||
api.versionsFrom(['METEOR@1.0']);
|
||||
|
||||
api.use(['nova:core@0.27.5-nova']);
|
||||
api.use(['nova:core@1.0.0']);
|
||||
|
||||
api.mainModule("lib/server.js", "server");
|
||||
// api.mainModule("lib/client.js", "client");
|
||||
|
|
1
packages/nova-apollo/README.md
Normal file
1
packages/nova-apollo/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
Nova Apollo Client & Server package. It handles the data layer of Nova.
|
89
packages/nova-apollo/lib/client.js
Normal file
89
packages/nova-apollo/lib/client.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
// note(oct. 28, 2016)
|
||||
// by-pass the meteor integration to use the features of apollo-client 0.5.x / graphql-server 0.4.x
|
||||
|
||||
// -------
|
||||
// start of main-client from apollostack/meteor-integration
|
||||
|
||||
import { createNetworkInterface } from 'apollo-client';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import 'isomorphic-fetch';
|
||||
|
||||
const defaultNetworkInterfaceConfig = {
|
||||
path: '/graphql',
|
||||
options: {},
|
||||
};
|
||||
|
||||
export const createMeteorNetworkInterface = (givenConfig) => {
|
||||
const config = _.extend(defaultNetworkInterfaceConfig, givenConfig);
|
||||
|
||||
// absoluteUrl adds a '/', so let's remove it first
|
||||
let path = config.path;
|
||||
if (path[0] === '/') {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
// For SSR
|
||||
const url = Meteor.absoluteUrl(path);
|
||||
const networkInterface = createNetworkInterface({
|
||||
uri: url,
|
||||
opts: {
|
||||
credentials: 'same-origin',
|
||||
}
|
||||
});
|
||||
|
||||
networkInterface.use([{
|
||||
applyMiddleware(request, next) {
|
||||
|
||||
// login token created by meteorhacks:fast-render and caught during server-side rendering by rr:react-router-ssr
|
||||
const { cookieLoginToken } = config;
|
||||
// Meteor accounts-base login token stored in local storage, only exists client-side
|
||||
const localStorageLoginToken = Meteor.isClient && Accounts._storedLoginToken();
|
||||
|
||||
// on initial load, prefer to use the server cookie if existing
|
||||
let currentUserToken = cookieLoginToken || localStorageLoginToken;
|
||||
|
||||
// ...a login token has been passed to the config, however the "true" one is different ⚠️
|
||||
if (Meteor.isClient && cookieLoginToken && cookieLoginToken !== localStorageLoginToken) {
|
||||
// be sure to pass the right token to the request 🎉
|
||||
currentUserToken = localStorageLoginToken;
|
||||
}
|
||||
|
||||
if (!currentUserToken) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!request.options.headers) {
|
||||
request.options.headers = new Headers();
|
||||
}
|
||||
|
||||
request.options.headers.Authorization = currentUserToken;
|
||||
|
||||
next();
|
||||
},
|
||||
}]);
|
||||
|
||||
return networkInterface;
|
||||
};
|
||||
|
||||
export const meteorClientConfig = (networkInterfaceConfig) => {
|
||||
return {
|
||||
ssrMode: Meteor.isServer,
|
||||
networkInterface: createMeteorNetworkInterface(networkInterfaceConfig),
|
||||
queryDeduplication: true,
|
||||
|
||||
// Default to using Mongo _id, must use _id for queries.
|
||||
dataIdFromObject: (result) => {
|
||||
if (result._id && result.__typename) {
|
||||
const dataId = result.__typename + result._id;
|
||||
return dataId;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// end of main-client from apollostack/meteor-integration
|
||||
// --------
|
||||
|
||||
// export const client = new ApolloClient(meteorClientConfig());
|
34
packages/nova-apollo/lib/export.js
Normal file
34
packages/nova-apollo/lib/export.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { GraphQLSchema } from 'meteor/nova:lib';
|
||||
|
||||
import { makeExecutableSchema } from 'graphql-tools';
|
||||
|
||||
import { meteorClientConfig } from './client.js';
|
||||
|
||||
import { createApolloServer } from './server.js';
|
||||
import generateTypeDefs from './schema';
|
||||
|
||||
import OpticsAgent from 'optics-agent'
|
||||
|
||||
Meteor.startup(function () {
|
||||
const typeDefs = generateTypeDefs();
|
||||
|
||||
GraphQLSchema.finalSchema = typeDefs;
|
||||
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers: GraphQLSchema.resolvers,
|
||||
});
|
||||
|
||||
if (process.env.OPTICS_API_KEY) {
|
||||
OpticsAgent.instrumentSchema(schema)
|
||||
}
|
||||
|
||||
// uncomment for debug
|
||||
// console.log('// --> starting graphql server');
|
||||
|
||||
createApolloServer({
|
||||
schema,
|
||||
});
|
||||
});
|
||||
|
||||
export { meteorClientConfig };
|
20
packages/nova-apollo/lib/schema.js
Normal file
20
packages/nova-apollo/lib/schema.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { GraphQLSchema } from 'meteor/nova:lib';
|
||||
|
||||
const generateTypeDefs = () => [`
|
||||
|
||||
scalar JSON
|
||||
scalar Date
|
||||
|
||||
${GraphQLSchema.getCollectionsSchemas()}
|
||||
${GraphQLSchema.getAdditionalSchemas()}
|
||||
|
||||
type Query {
|
||||
${GraphQLSchema.queries.join('\n')}
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
${GraphQLSchema.mutations.join('\n')}
|
||||
}
|
||||
`];
|
||||
|
||||
export default generateTypeDefs;
|
138
packages/nova-apollo/lib/server.js
Normal file
138
packages/nova-apollo/lib/server.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
// note(oct. 28, 2016)
|
||||
// by-pass the meteor integration to use the features of apollo-client 0.5.x / graphql-server 0.4.x
|
||||
|
||||
// -------
|
||||
// start of main-client from apollostack/meteor-integration
|
||||
|
||||
import { graphqlExpress, graphiqlExpress } from 'graphql-server-express';
|
||||
import bodyParser from 'body-parser';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import express from 'express';
|
||||
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { WebApp } from 'meteor/webapp';
|
||||
import { check } from 'meteor/check';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { _ } from 'meteor/underscore';
|
||||
|
||||
import Users from 'meteor/nova:users';
|
||||
|
||||
import { GraphQLSchema } from 'meteor/nova:lib';
|
||||
|
||||
import OpticsAgent from 'optics-agent'
|
||||
|
||||
|
||||
const defaultConfig = {
|
||||
path: '/graphql',
|
||||
maxAccountsCacheSizeInMB: 1,
|
||||
graphiql : Meteor.isDevelopment,
|
||||
graphiqlPath : '/graphiql',
|
||||
graphiqlOptions : {
|
||||
passHeader : "'Authorization': localStorage['Meteor.loginToken']"
|
||||
},
|
||||
configServer: (graphQLServer) => {},
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
formatError: e => ({
|
||||
message: e.message,
|
||||
locations: e.locations,
|
||||
path: e.path
|
||||
}),
|
||||
debug: Meteor.isDevelopment,
|
||||
};
|
||||
|
||||
export const createApolloServer = (givenOptions = {}, givenConfig = {}) => {
|
||||
|
||||
let graphiqlOptions = Object.assign({}, defaultConfig.graphiqlOptions, givenConfig.graphiqlOptions);
|
||||
let config = Object.assign({}, defaultConfig, givenConfig);
|
||||
config.graphiqlOptions = graphiqlOptions;
|
||||
|
||||
const graphQLServer = express();
|
||||
|
||||
config.configServer(graphQLServer)
|
||||
|
||||
// Load the cookie parsing middleware, used to grab login token
|
||||
graphQLServer.use(cookieParser());
|
||||
|
||||
// Use Optics middleware
|
||||
if (process.env.OPTICS_API_KEY) {
|
||||
graphQLServer.use(OpticsAgent.middleware());
|
||||
}
|
||||
|
||||
// GraphQL endpoint
|
||||
graphQLServer.use(config.path, bodyParser.json(), graphqlExpress(async (req) => {
|
||||
let options,
|
||||
user = null;
|
||||
|
||||
// console.log('Login token: ', req.cookies.meteor_login_token);
|
||||
|
||||
if (_.isFunction(givenOptions))
|
||||
options = givenOptions(req);
|
||||
else
|
||||
options = givenOptions;
|
||||
|
||||
// Merge in the defaults
|
||||
options = Object.assign({}, defaultOptions, options);
|
||||
|
||||
if (options.context) {
|
||||
// don't mutate the context provided in options
|
||||
options.context = Object.assign({}, options.context);
|
||||
} else {
|
||||
options.context = {};
|
||||
}
|
||||
|
||||
// Add Optics to GraphQL context object
|
||||
if (process.env.OPTICS_API_KEY) {
|
||||
options.context.opticsContext = OpticsAgent.context(req);
|
||||
}
|
||||
|
||||
options.context.getViewableFields = Users.getViewableFields;
|
||||
|
||||
// Get the token from the header
|
||||
if (req.headers.authorization) {
|
||||
const token = req.headers.authorization;
|
||||
check(token, String);
|
||||
const hashedToken = Accounts._hashLoginToken(token);
|
||||
|
||||
// Get the user from the database
|
||||
user = await Users.findOne(
|
||||
{"services.resume.loginTokens.hashedToken": hashedToken},
|
||||
// {fields: {
|
||||
// _id: 1,
|
||||
// 'services.resume.loginTokens.$': 1
|
||||
// }}
|
||||
);
|
||||
|
||||
if (user) {
|
||||
|
||||
const expiresAt = Accounts._tokenExpiration(user.services.resume.loginTokens[0].when);
|
||||
const isExpired = expiresAt < new Date();
|
||||
|
||||
if (!isExpired) {
|
||||
|
||||
options.context.userId = user._id;
|
||||
options.context.currentUser = user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options.context = deepmerge(options.context, GraphQLSchema.context);
|
||||
|
||||
return options;
|
||||
|
||||
}));
|
||||
|
||||
// Start GraphiQL if enabled
|
||||
if (config.graphiql) {
|
||||
graphQLServer.use(config.graphiqlPath, graphiqlExpress(_.extend(config.graphiqlOptions, {endpointURL : config.path})));
|
||||
}
|
||||
|
||||
// This binds the specified paths to the Express server running Apollo + GraphiQL
|
||||
WebApp.connectHandlers.use(Meteor.bindEnvironment(graphQLServer));
|
||||
};
|
||||
|
||||
// end of main-client from apollostack/meteor-integration
|
||||
// -------
|
24
packages/nova-apollo/package.js
Normal file
24
packages/nova-apollo/package.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
Package.describe({
|
||||
name: "nova:apollo",
|
||||
summary: "Nova Apollo Server package",
|
||||
version: "1.0.0",
|
||||
git: "https://github.com/TelescopeJS/Telescope.git"
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
api.versionsFrom(['METEOR@1.0']);
|
||||
|
||||
api.use([
|
||||
|
||||
// Nova packages
|
||||
|
||||
'nova:core@1.0.0',
|
||||
'nova:users@1.0.0',
|
||||
|
||||
]);
|
||||
|
||||
api.mainModule("lib/client.js", "client");
|
||||
api.mainModule("lib/export.js", "server"); // client.js inside of export.js for ssr purpose
|
||||
|
||||
});
|
|
@ -1,60 +1,42 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import NovaForm from "meteor/nova:forms";
|
||||
import Categories from "meteor/nova:categories";
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
// import { DocumentContainer } from "meteor/utilities:react-list-container";
|
||||
import { intlShape } from 'react-intl';
|
||||
import { Components, registerComponent, getRawComponent } from 'meteor/nova:lib';
|
||||
import Categories from "meteor/nova:categories";
|
||||
import { withMessages } from 'meteor/nova:core';
|
||||
|
||||
class CategoriesEditForm extends Component{
|
||||
const CategoriesEditForm = (props, context) => {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.deleteCategory = this.deleteCategory.bind(this);
|
||||
}
|
||||
return (
|
||||
<div className="categories-edit-form">
|
||||
<Components.SmartForm
|
||||
collection={Categories}
|
||||
documentId={props.category._id}
|
||||
mutationFragment={getRawComponent('CategoriesList').fragment}
|
||||
successCallback={category => {
|
||||
props.closeCallback();
|
||||
props.flash(context.intl.formatMessage({id: 'categories.edit_success'}, {name: category.name}), "success");
|
||||
}}
|
||||
removeSuccessCallback={({documentId, documentTitle}) => {
|
||||
props.closeCallback();
|
||||
props.flash(context.intl.formatMessage({id: 'categories.delete_success'}, {name: documentTitle}), "success");
|
||||
// context.events.track("category deleted", {_id: documentId});
|
||||
}}
|
||||
showRemove={true}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
deleteCategory() {
|
||||
const category = this.props.category;
|
||||
if (window.confirm(`Delete category “${category.name}”?`)) {
|
||||
this.context.actions.call("categories.deleteById", category._id, (error, result) => {
|
||||
if (error) {
|
||||
this.context.messages.flash(error.message, "error");
|
||||
} else {
|
||||
this.context.messages.flash(`Category “${category.name}” deleted and removed from ${result} posts.`, "success");
|
||||
}
|
||||
this.context.closeCallback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<div className="categories-edit-form">
|
||||
<NovaForm
|
||||
document={this.props.category}
|
||||
collection={Categories}
|
||||
methodName="categories.edit"
|
||||
successCallback={(category)=>{
|
||||
this.context.messages.flash("Category edited.", "success");
|
||||
}}
|
||||
/>
|
||||
<hr/>
|
||||
<a onClick={this.deleteCategory} className="categories-delete-link"><Telescope.components.Icon name="close"/> <FormattedMessage id="categories.delete"/></a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CategoriesEditForm.propTypes = {
|
||||
category: React.PropTypes.object.isRequired
|
||||
category: React.PropTypes.object.isRequired,
|
||||
closeCallback: React.PropTypes.func,
|
||||
flash: React.PropTypes.func,
|
||||
}
|
||||
|
||||
CategoriesEditForm.contextTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
closeCallback: React.PropTypes.func,
|
||||
currentUser: React.PropTypes.object,
|
||||
messages: React.PropTypes.object,
|
||||
intl: intlShape,
|
||||
// events: React.PropTypes.object,
|
||||
};
|
||||
|
||||
module.exports = CategoriesEditForm;
|
||||
export default CategoriesEditForm;
|
||||
registerComponent('CategoriesEditForm', CategoriesEditForm, withMessages);
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import { /* ModalTrigger, */ ContextPasser } from "meteor/nova:core";
|
||||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Button, DropdownButton, MenuItem, Modal } from 'react-bootstrap';
|
||||
import { ShowIf, withList } from "meteor/nova:core";
|
||||
import { withRouter } from 'react-router'
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
// import Users from 'meteor/nova:users';
|
||||
import Categories from 'meteor/nova:categories';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
// note: cannot use ModalTrigger component because of https://github.com/react-bootstrap/react-bootstrap/issues/1808
|
||||
|
||||
|
@ -43,9 +44,7 @@ class CategoriesList extends Component {
|
|||
<Modal.Title><FormattedMessage id="categories.edit"/></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<ContextPasser currentUser={this.context.currentUser} messages={this.context.messages} actions={this.context.actions} closeCallback={this.closeModal}>
|
||||
<Telescope.components.CategoriesEditForm category={category}/>
|
||||
</ContextPasser>
|
||||
<Components.CategoriesEditForm category={category} closeCallback={this.closeModal} />
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
)
|
||||
|
@ -59,9 +58,7 @@ class CategoriesList extends Component {
|
|||
<Modal.Title><FormattedMessage id="categories.new"/></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<ContextPasser currentUser={this.context.currentUser} messages={this.context.messages} closeCallback={this.closeModal}>
|
||||
<Telescope.components.CategoriesNewForm/>
|
||||
</ContextPasser>
|
||||
<Components.CategoriesNewForm closeCallback={this.closeModal}/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
)
|
||||
|
@ -69,22 +66,20 @@ class CategoriesList extends Component {
|
|||
|
||||
renderCategoryNewButton() {
|
||||
return (
|
||||
<Telescope.components.CanDo action="categories.new">
|
||||
<div className="category-menu-item dropdown-item"><MenuItem><Button bsStyle="primary" onClick={this.openCategoryNewModal}><FormattedMessage id="categories.new"/></Button></MenuItem></div>
|
||||
</Telescope.components.CanDo>
|
||||
<div className="category-menu-item dropdown-item">
|
||||
<MenuItem>
|
||||
<Button bsStyle="primary" onClick={this.openCategoryNewModal}>
|
||||
<FormattedMessage id="categories.new"/>
|
||||
</Button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
// const CategoriesNewForm = Telescope.components.CategoriesNewForm;
|
||||
// return (
|
||||
// <ModalTrigger title="New Category" component={<MenuItem className="dropdown-item post-category"><Button bsStyle="primary">New Category</Button></MenuItem>}>
|
||||
// <CategoriesNewForm/>
|
||||
// </ModalTrigger>
|
||||
// )
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const categories = this.props.categories;
|
||||
// const context = this.context;
|
||||
const categories = this.props.results;
|
||||
|
||||
const currentQuery = _.clone(this.props.router.location.query);
|
||||
delete currentQuery.cat;
|
||||
|
||||
|
@ -103,13 +98,43 @@ class CategoriesList extends Component {
|
|||
</MenuItem>
|
||||
</LinkContainer>
|
||||
</div>
|
||||
{categories && categories.length > 0 ? categories.map((category, index) => <Telescope.components.Category key={index} category={category} index={index} openModal={_.partial(this.openCategoryEditModal, index)}/>) : null}
|
||||
{this.renderCategoryNewButton()}
|
||||
{
|
||||
// categories data are loaded
|
||||
!this.props.loading ?
|
||||
// there are currently categories
|
||||
categories && categories.length > 0 ?
|
||||
categories.map((category, index) => <Components.Category key={index} category={category} index={index} openModal={_.partial(this.openCategoryEditModal, index)}/>)
|
||||
// not any category found
|
||||
: null
|
||||
// categories are loading
|
||||
: <div className="dropdown-item"><MenuItem><Components.Loading /></MenuItem></div>
|
||||
}
|
||||
<Components.ShowIf check={Categories.options.mutations.new.check}>{this.renderCategoryNewButton()}</Components.ShowIf>
|
||||
</DropdownButton>
|
||||
|
||||
<div>
|
||||
{/* modals cannot be inside DropdownButton component (see GH issue) */}
|
||||
{categories && categories.length > 0 ? categories.map((category, index) => this.renderCategoryEditModal(category, index)) : null}
|
||||
{this.renderCategoryNewModal()}
|
||||
{
|
||||
/*
|
||||
Modals cannot be inside DropdownButton component (see GH issue link on top of the file)
|
||||
-> we place them in a <div> outside the <DropdownButton> component
|
||||
*/
|
||||
|
||||
/* Modals for each category to edit */
|
||||
// categories data are loaded
|
||||
!this.props.loading ?
|
||||
// there are currently categories
|
||||
categories && categories.length > 0 ?
|
||||
categories.map((category, index) => this.renderCategoryEditModal(category, index))
|
||||
// not any category found
|
||||
: null
|
||||
// categories are loading
|
||||
: null
|
||||
}
|
||||
|
||||
{
|
||||
/* modal for creating a new category */
|
||||
this.renderCategoryNewModal()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -118,14 +143,26 @@ class CategoriesList extends Component {
|
|||
}
|
||||
|
||||
CategoriesList.propTypes = {
|
||||
categories: React.PropTypes.array
|
||||
}
|
||||
|
||||
CategoriesList.contextTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
currentUser: React.PropTypes.object,
|
||||
messages: React.PropTypes.object,
|
||||
results: React.PropTypes.array,
|
||||
};
|
||||
|
||||
module.exports = withRouter(CategoriesList);
|
||||
export default withRouter(CategoriesList);
|
||||
CategoriesList.fragment = gql`
|
||||
fragment categoriesListFragment on Category {
|
||||
_id
|
||||
name
|
||||
description
|
||||
order
|
||||
slug
|
||||
image
|
||||
}
|
||||
`;
|
||||
|
||||
const categoriesListOptions = {
|
||||
collection: Categories,
|
||||
queryName: 'categoriesListQuery',
|
||||
fragment: CategoriesList.fragment,
|
||||
limit: 0,
|
||||
pollInterval: 0,
|
||||
};
|
||||
|
||||
registerComponent('CategoriesList', CategoriesList, withRouter, withList(categoriesListOptions));
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import React, { PropTypes, Component } from 'react';
|
||||
//import { Messages } from "meteor/nova:core";
|
||||
import { intlShape } from 'react-intl';
|
||||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import Categories from "meteor/nova:categories";
|
||||
import NovaForm from "meteor/nova:forms";
|
||||
import { withMessages } from 'meteor/nova:core';
|
||||
|
||||
const CategoriesNewForm = (props, context) => {
|
||||
|
||||
return (
|
||||
<div className="categories-new-form">
|
||||
<NovaForm
|
||||
<Components.SmartForm
|
||||
collection={Categories}
|
||||
methodName="categories.new"
|
||||
successCallback={(category)=>{
|
||||
context.messages.flash("Category created.", "success");
|
||||
successCallback={category => {
|
||||
props.closeCallback();
|
||||
props.flash(context.intl.formatMessage({id: 'categories.new_success'}, {name: category.name}), "success");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -20,10 +21,13 @@ const CategoriesNewForm = (props, context) => {
|
|||
|
||||
CategoriesNewForm.displayName = "CategoriesNewForm";
|
||||
|
||||
CategoriesNewForm.contextTypes = {
|
||||
currentUser: React.PropTypes.object,
|
||||
messages: React.PropTypes.object
|
||||
CategoriesNewForm.propTypes = {
|
||||
closeCallback: React.PropTypes.func,
|
||||
flash: React.PropTypes.func,
|
||||
};
|
||||
|
||||
module.exports = CategoriesNewForm;
|
||||
export default CategoriesNewForm;
|
||||
CategoriesNewForm.contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
registerComponent('CategoriesNewForm', CategoriesNewForm, withMessages);
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import Users from 'meteor/nova:users';
|
||||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import { MenuItem } from 'react-bootstrap';
|
||||
import { withRouter } from 'react-router'
|
||||
import { /* Button, DropdownButton, */ MenuItem } from 'react-bootstrap';
|
||||
// import classNames from "classnames";
|
||||
// import { Messages, ModalTrigger } from 'meteor/nova:core';
|
||||
import Categories from 'meteor/nova:categories';
|
||||
import { ShowIf } from 'meteor/nova:core';
|
||||
|
||||
class Category extends Component {
|
||||
|
||||
renderEdit() {
|
||||
return (
|
||||
<Telescope.components.CanDo action="categories.edit.all">
|
||||
<a onClick={this.props.openModal} className="edit-category-link"><Telescope.components.Icon name="edit"/></a>
|
||||
</Telescope.components.CanDo>
|
||||
<a onClick={this.props.openModal} className="edit-category-link">
|
||||
<Components.Icon name="edit"/>
|
||||
</a>
|
||||
);
|
||||
// return (
|
||||
// <ModalTrigger title="Edit Category" component={<a className="edit-category-link"><Telescope.components.Icon name="edit"/></a>}>
|
||||
// <Telescope.componentsCategoriesEditForm category={this.props.category}/>
|
||||
// <ModalTrigger title="Edit Category" component={<a className="edit-category-link"><Components.Icon name="edit"/></a>}>
|
||||
// <ComponentsCategoriesEditForm category={this.props.category}/>
|
||||
// </ModalTrigger>
|
||||
// )
|
||||
}
|
||||
|
@ -38,11 +37,11 @@ class Category extends Component {
|
|||
eventKey={index+1}
|
||||
key={category._id}
|
||||
>
|
||||
{currentCategorySlug === category.slug ? <Telescope.components.Icon name="voted"/> : null}
|
||||
{currentCategorySlug === category.slug ? <Components.Icon name="voted"/> : null}
|
||||
{category.name}
|
||||
</MenuItem>
|
||||
</LinkContainer>
|
||||
{Users.canDo(this.context.currentUser, "categories.edit.all") ? this.renderEdit() : null}
|
||||
<Components.ShowIf check={Categories.options.mutations.edit.check} document={category}>{this.renderEdit()}</Components.ShowIf>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -53,11 +52,6 @@ Category.propTypes = {
|
|||
index: React.PropTypes.number,
|
||||
currentCategorySlug: React.PropTypes.string,
|
||||
openModal: React.PropTypes.func
|
||||
}
|
||||
|
||||
Category.contextTypes = {
|
||||
currentUser: React.PropTypes.object
|
||||
};
|
||||
|
||||
module.exports = withRouter(Category);
|
||||
export default withRouter(Category);
|
||||
registerComponent('Category', Category, withRouter);
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
import './components.js';
|
||||
import './config.js';
|
||||
import './routes.js';
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import React, { PropTypes, Component } from 'react';
|
||||
import NovaForm from "meteor/nova:forms";
|
||||
import Comments from "meteor/nova:comments";
|
||||
|
||||
class CommentsEdit extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="comments-edit-form">
|
||||
<NovaForm
|
||||
collection={Comments}
|
||||
document={this.props.comment}
|
||||
methodName="comments.edit"
|
||||
successCallback={this.props.successCallback}
|
||||
layout="elementOnly"
|
||||
cancelCallback={this.props.cancelCallback}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CommentsEdit.propTypes = {
|
||||
comment: React.PropTypes.object.isRequired,
|
||||
successCallback: React.PropTypes.func,
|
||||
cancelCallback: React.PropTypes.func
|
||||
}
|
||||
|
||||
CommentsEdit.contextTypes = {
|
||||
currentUser: React.PropTypes.object
|
||||
}
|
||||
|
||||
module.exports = CommentsEdit;
|
|
@ -0,0 +1,29 @@
|
|||
import { Components, registerComponent, getRawComponent } from 'meteor/nova:core';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import Comments from "meteor/nova:comments";
|
||||
import { withMessages } from 'meteor/nova:core';
|
||||
|
||||
const CommentsEditForm = (props, context) => {
|
||||
return (
|
||||
<div className="comments-edit-form">
|
||||
<Components.SmartForm
|
||||
layout="elementOnly"
|
||||
collection={Comments}
|
||||
documentId={props.comment._id}
|
||||
successCallback={props.successCallback}
|
||||
cancelCallback={props.cancelCallback}
|
||||
removeSuccessCallback={props.removeSuccessCallback}
|
||||
showRemove={true}
|
||||
mutationFragment={getRawComponent('PostsCommentsThread').fragment}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
CommentsEditForm.propTypes = {
|
||||
comment: React.PropTypes.object.isRequired,
|
||||
successCallback: React.PropTypes.func,
|
||||
cancelCallback: React.PropTypes.func
|
||||
};
|
||||
|
||||
registerComponent('CommentsEditForm', CommentsEditForm, withMessages);
|
|
@ -1,14 +1,14 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { intlShape, FormattedMessage, FormattedRelative } from 'react-intl';
|
||||
// import moment from 'moment';
|
||||
// import Users from 'meteor/nova:users';
|
||||
import { ShowIf, withCurrentUser, withMessages } from 'meteor/nova:core';
|
||||
import Comments from 'meteor/nova:comments';
|
||||
|
||||
class CommentsItem extends Component{
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
['showReply', 'replyCancelCallback', 'replySuccessCallback', 'showEdit', 'editCancelCallback', 'editSuccessCallback', 'deleteComment'].forEach(methodName => {this[methodName] = this[methodName].bind(this)});
|
||||
['showReply', 'replyCancelCallback', 'replySuccessCallback', 'showEdit', 'editCancelCallback', 'editSuccessCallback', 'removeSuccessCallback'].forEach(methodName => {this[methodName] = this[methodName].bind(this)});
|
||||
this.state = {
|
||||
showReply: false,
|
||||
showEdit: false
|
||||
|
@ -43,32 +43,24 @@ class CommentsItem extends Component{
|
|||
this.setState({showEdit: false});
|
||||
}
|
||||
|
||||
deleteComment() {
|
||||
|
||||
const comment = this.props.comment;
|
||||
const deleteConfirmMessage = this.context.intl.formatMessage({id: "comments.delete_confirm"}, {body: Telescope.utils.trimWords(comment.body, 20)});
|
||||
const deleteSuccessMessage = this.context.intl.formatMessage({id: "comments.delete_success"}, {body: Telescope.utils.trimWords(comment.body, 20)});
|
||||
|
||||
if (window.confirm(deleteConfirmMessage)) {
|
||||
this.context.actions.call('comments.deleteById', comment._id, (error, result) => {
|
||||
this.context.messages.flash(deleteSuccessMessage, "success");
|
||||
this.context.events.track("comment deleted", {'_id': comment._id});
|
||||
});
|
||||
}
|
||||
|
||||
removeSuccessCallback({documentId}) {
|
||||
const deleteDocumentSuccess = this.context.intl.formatMessage({id: 'comments.delete_success'});
|
||||
this.props.flash(deleteDocumentSuccess, "success");
|
||||
// todo: handle events in async callback
|
||||
// this.context.events.track("comment deleted", {_id: documentId});
|
||||
}
|
||||
|
||||
renderComment() {
|
||||
const htmlBody = {__html: this.props.comment.htmlBody};
|
||||
|
||||
const showReplyButton = !this.props.comment.isDeleted && !!this.context.currentUser;
|
||||
const showReplyButton = !this.props.comment.isDeleted && !!this.props.currentUser;
|
||||
|
||||
return (
|
||||
<div className="comments-item-text">
|
||||
<div dangerouslySetInnerHTML={htmlBody}></div>
|
||||
{ showReplyButton ?
|
||||
<a className="comments-item-reply-link" onClick={this.showReply}>
|
||||
<Telescope.components.Icon name="reply"/> <FormattedMessage id="comments.reply"/>
|
||||
<Components.Icon name="reply"/> <FormattedMessage id="comments.reply"/>
|
||||
</a> : null}
|
||||
</div>
|
||||
)
|
||||
|
@ -78,7 +70,7 @@ class CommentsItem extends Component{
|
|||
|
||||
return (
|
||||
<div className="comments-item-reply">
|
||||
<Telescope.components.CommentsNew
|
||||
<Components.CommentsNewForm
|
||||
postId={this.props.comment.postId}
|
||||
parentComment={this.props.comment}
|
||||
successCallback={this.replySuccessCallback}
|
||||
|
@ -92,10 +84,11 @@ class CommentsItem extends Component{
|
|||
renderEdit() {
|
||||
|
||||
return (
|
||||
<Telescope.components.CommentsEdit
|
||||
<Components.CommentsEditForm
|
||||
comment={this.props.comment}
|
||||
successCallback={this.editSuccessCallback}
|
||||
cancelCallback={this.editCancelCallback}
|
||||
removeSuccessCallback={this.removeSuccessCallback}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -107,15 +100,14 @@ class CommentsItem extends Component{
|
|||
<div className="comments-item" id={comment._id}>
|
||||
<div className="comments-item-body">
|
||||
<div className="comments-item-meta">
|
||||
<Telescope.components.UsersAvatar size="small" user={comment.user}/>
|
||||
<Telescope.components.UsersName user={comment.user}/>
|
||||
<Components.UsersAvatar size="small" user={comment.user}/>
|
||||
<Components.UsersName user={comment.user}/>
|
||||
<div className="comments-item-date"><FormattedRelative value={comment.postedAt}/></div>
|
||||
<Telescope.components.CanDo action="comments.edit" document={this.props.comment}>
|
||||
<Components.ShowIf check={Comments.options.mutations.edit.check} document={this.props.comment}>
|
||||
<div>
|
||||
<a className="comment-edit" onClick={this.showEdit}><FormattedMessage id="comments.edit"/></a>
|
||||
<a className="comment-delete" onClick={this.deleteComment}><FormattedMessage id="comments.delete"/></a>
|
||||
</div>
|
||||
</Telescope.components.CanDo>
|
||||
</Components.ShowIf>
|
||||
</div>
|
||||
{this.state.showEdit ? this.renderEdit() : this.renderComment()}
|
||||
</div>
|
||||
|
@ -128,14 +120,12 @@ class CommentsItem extends Component{
|
|||
|
||||
CommentsItem.propTypes = {
|
||||
comment: React.PropTypes.object.isRequired, // the current comment
|
||||
currentUser: React.PropTypes.object,
|
||||
};
|
||||
|
||||
CommentsItem.contextTypes = {
|
||||
currentUser: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
messages: React.PropTypes.object,
|
||||
events: React.PropTypes.object,
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
module.exports = CommentsItem;
|
||||
registerComponent('CommentsItem', CommentsItem, withCurrentUser, withMessages);
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import React from 'react';
|
||||
import {/* injectIntl, */ FormattedMessage} from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const CommentsList = ({results, hasMore, ready, count, totalCount, loadMore}) => {
|
||||
const CommentsList = ({comments, commentCount}) => {
|
||||
|
||||
if (!!results.length) {
|
||||
if (commentCount > 0) {
|
||||
return (
|
||||
<div className="comments-list">
|
||||
{results.map(comment => <Telescope.components.CommentsNode comment={comment} key={comment._id} />)}
|
||||
{hasMore ? (ready ? <Telescope.components.CommentsLoadMore loadMore={loadMore} count={count} totalCount={totalCount} /> : <Telescope.components.Loading/>) : null}
|
||||
</div>
|
||||
)
|
||||
} else if (!ready) {
|
||||
return (
|
||||
<div className="comments-list">
|
||||
<Telescope.components.Loading/>
|
||||
{comments.map(comment => <Components.CommentsNode comment={comment} key={comment._id} />)}
|
||||
{/*hasMore ? (ready ? <Components.CommentsLoadMore loadMore={loadMore} count={count} totalCount={totalCount} /> : <Components.Loading/>) : null*/}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
|
@ -31,4 +25,4 @@ const CommentsList = ({results, hasMore, ready, count, totalCount, loadMore}) =>
|
|||
|
||||
CommentsList.displayName = "CommentsList";
|
||||
|
||||
module.exports = CommentsList;
|
||||
registerComponent('CommentsList', CommentsList);
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { registerComponent } from 'meteor/nova:lib';
|
||||
import React from 'react';
|
||||
|
||||
const CommentsLoadMore = ({loadMore, count, totalCount}) => {
|
||||
const label = totalCount ? `Load More (${count}/${totalCount})` : "Load More";
|
||||
return <a className="comments-load-more" onClick={loadMore}>{label}</a>
|
||||
return <a className="comments-load-more" onClick={e => { e.preventDefault(); loadMore();}}>{label}</a>
|
||||
}
|
||||
|
||||
CommentsLoadMore.displayName = "CommentsLoadMore";
|
||||
|
||||
module.exports = CommentsLoadMore;
|
||||
registerComponent('CommentsLoadMore', CommentsLoadMore);
|
|
@ -1,5 +1,4 @@
|
|||
import React, { PropTypes, Component } from 'react';
|
||||
import NovaForm from "meteor/nova:forms";
|
||||
import Comments from "meteor/nova:comments";
|
||||
|
||||
class CommentsNew extends Component {
|
||||
|
@ -18,7 +17,7 @@ class CommentsNew extends Component {
|
|||
|
||||
return (
|
||||
<div className="comments-new-form">
|
||||
<NovaForm
|
||||
<Components.SmartForm
|
||||
collection={Comments}
|
||||
methodName="comments.new"
|
||||
prefilledProps={prefilledProps}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { Components, registerComponent, getRawComponent } from 'meteor/nova:core';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import Comments from "meteor/nova:comments";
|
||||
import { ShowIf, withMessages } from 'meteor/nova:core';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const CommentsNewForm = (props, context) => {
|
||||
|
||||
let prefilledProps = {postId: props.postId};
|
||||
|
||||
if (props.parentComment) {
|
||||
prefilledProps = Object.assign(prefilledProps, {
|
||||
parentCommentId: props.parentComment._id,
|
||||
// if parent comment has a topLevelCommentId use it; if it doesn't then it *is* the top level comment
|
||||
topLevelCommentId: props.parentComment.topLevelCommentId || props.parentComment._id
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Components.ShowIf
|
||||
check={Comments.options.mutations.new.check}
|
||||
failureComponent={<FormattedMessage id="users.cannot_comment"/>}
|
||||
>
|
||||
<div className="comments-new-form">
|
||||
<Components.SmartForm
|
||||
collection={Comments}
|
||||
mutationFragment={getRawComponent('PostsCommentsThread').fragment}
|
||||
successCallback={props.successCallback}
|
||||
cancelCallback={props.type === "reply" ? props.cancelCallback : null}
|
||||
prefilledProps={prefilledProps}
|
||||
layout="elementOnly"
|
||||
/>
|
||||
</div>
|
||||
</Components.ShowIf>
|
||||
)
|
||||
|
||||
};
|
||||
|
||||
CommentsNewForm.propTypes = {
|
||||
postId: React.PropTypes.string.isRequired,
|
||||
type: React.PropTypes.string, // "comment" or "reply"
|
||||
parentComment: React.PropTypes.object, // if reply, the comment being replied to
|
||||
parentCommentId: React.PropTypes.string, // if reply
|
||||
topLevelCommentId: React.PropTypes.string, // if reply
|
||||
successCallback: React.PropTypes.func, // a callback to execute when the submission has been successful
|
||||
cancelCallback: React.PropTypes.func,
|
||||
novaFormMutation: React.PropTypes.func,
|
||||
router: React.PropTypes.object,
|
||||
flash: React.PropTypes.func,
|
||||
};
|
||||
|
||||
registerComponent('CommentsNewForm', CommentsNewForm, withMessages);
|
|
@ -1,4 +1,4 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
|
||||
class CommentsNode extends Component {
|
||||
|
@ -6,7 +6,7 @@ class CommentsNode extends Component {
|
|||
renderComment(comment) {
|
||||
|
||||
return (
|
||||
<Telescope.components.CommentsItem comment={comment} key={comment._id} />
|
||||
<Components.CommentsItem comment={comment} key={comment._id} />
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ class CommentsNode extends Component {
|
|||
return (
|
||||
<div className="comments-node">
|
||||
{this.renderComment(comment)}
|
||||
{children ? this.renderChildren(children) : ""}
|
||||
{children ? this.renderChildren(children) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -37,8 +37,4 @@ CommentsNode.propTypes = {
|
|||
comment: React.PropTypes.object.isRequired, // the current comment
|
||||
};
|
||||
|
||||
CommentsNode.contextTypes = {
|
||||
currentUser: React.PropTypes.object, // the current user
|
||||
};
|
||||
|
||||
module.exports = CommentsNode;
|
||||
registerComponent('CommentsNode', CommentsNode);
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { IntlProvider, intlShape} from 'react-intl';
|
||||
import { AppComposer } from "meteor/nova:core";
|
||||
|
||||
class App extends Component {
|
||||
|
||||
getLocale() {
|
||||
return Telescope.settings.get("locale", "en");
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
|
||||
const messages = Telescope.strings[this.getLocale()] || {};
|
||||
const intlProvider = new IntlProvider({locale: this.getLocale()}, messages);
|
||||
|
||||
const {intl} = intlProvider.getChildContext();
|
||||
|
||||
return {
|
||||
currentUser: this.props.currentUser,
|
||||
actions: this.props.actions,
|
||||
events: this.props.events,
|
||||
messages: this.props.messages,
|
||||
intl: intl
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<IntlProvider locale={this.getLocale()} messages={Telescope.strings[this.getLocale()]}>
|
||||
{
|
||||
this.props.ready ?
|
||||
<Telescope.components.Layout>{this.props.children}</Telescope.components.Layout>
|
||||
: <Telescope.components.AppLoading />
|
||||
}
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
ready: React.PropTypes.bool,
|
||||
currentUser: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
events: React.PropTypes.object,
|
||||
messages: React.PropTypes.object,
|
||||
}
|
||||
|
||||
App.childContextTypes = {
|
||||
currentUser: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
events: React.PropTypes.object,
|
||||
messages: React.PropTypes.object,
|
||||
intl: intlShape
|
||||
}
|
||||
|
||||
module.exports = AppComposer(App);
|
||||
export default AppComposer(App);
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const AppLoading = () => <p><FormattedMessage id="app.loading"/></p>
|
||||
|
||||
AppLoading.displayName = "AppLoading";
|
||||
|
||||
module.exports = AppLoading;
|
||||
export default AppLoading;
|
|
@ -1,3 +1,4 @@
|
|||
import { registerComponent } from 'meteor/nova:lib';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
@ -11,5 +12,4 @@ const Error404 = () => {
|
|||
|
||||
Error404.displayName = "Error404";
|
||||
|
||||
module.exports = Error404;
|
||||
export default Error404;
|
||||
registerComponent('Error404', Error404);
|
|
@ -1,6 +1,6 @@
|
|||
import { registerComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
//import { Messages } from "meteor/nova:core";
|
||||
|
||||
class Flash extends Component{
|
||||
|
||||
|
@ -10,21 +10,21 @@ class Flash extends Component{
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.context.messages.markAsSeen(this.props.message._id);
|
||||
this.props.markAsSeen(this.props.message._id);
|
||||
}
|
||||
|
||||
dismissFlash(e) {
|
||||
e.preventDefault();
|
||||
this.context.messages.clear(this.props.message._id);
|
||||
this.props.clear(this.props.message._id);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let type = this.props.message.type;
|
||||
type = type === "error" ? "danger" : type; // if type is "error", use "danger" instead
|
||||
let flashType = this.props.message.flashType;
|
||||
flashType = flashType === "error" ? "danger" : flashType; // if flashType is "error", use "danger" instead
|
||||
|
||||
return (
|
||||
<Alert className="flash-message" bsStyle={type} onDismiss={this.dismissFlash}>
|
||||
<Alert className="flash-message" bsStyle={flashType} onDismiss={this.dismissFlash}>
|
||||
{this.props.message.content}
|
||||
</Alert>
|
||||
)
|
||||
|
@ -35,8 +35,4 @@ Flash.propTypes = {
|
|||
message: React.PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
Flash.contextTypes = {
|
||||
messages: React.PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
module.exports = Flash;
|
||||
registerComponent('Flash', Flash);
|
|
@ -1,14 +1,17 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import { withMessages } from 'meteor/nova:core';
|
||||
import React from 'react';
|
||||
|
||||
const FlashMessages = ({messages}) => {
|
||||
const FlashMessages = ({messages, clear, markAsSeen}) => {
|
||||
return (
|
||||
<div className="flash-messages">
|
||||
{messages.map((message, index) => <Telescope.components.Flash key={index} message={message} />)}
|
||||
{messages
|
||||
.filter(message => message.show)
|
||||
.map(message => <Components.Flash key={message._id} message={message} clear={clear} markAsSeen={markAsSeen} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FlashMessages.displayName = "FlashMessages";
|
||||
|
||||
module.exports = FlashMessages;
|
||||
registerComponent('FlashMessages', FlashMessages, withMessages);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { registerComponent } from 'meteor/nova:lib';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
@ -9,4 +10,4 @@ const Footer = props => {
|
|||
|
||||
Footer.displayName = "Footer";
|
||||
|
||||
module.exports = Footer;
|
||||
registerComponent('Footer', Footer);
|
|
@ -1,16 +1,16 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { registerComponent, Utils, getSetting, Headtags } from 'meteor/nova:core';
|
||||
|
||||
class HeadTags extends Component {
|
||||
render() {
|
||||
|
||||
const url = !!this.props.url ? this.props.url : Telescope.utils.getSiteUrl();
|
||||
const title = !!this.props.title ? this.props.title : Telescope.settings.get("title", "Nova");
|
||||
const description = !!this.props.description ? this.props.description : Telescope.settings.get("tagline");
|
||||
const url = !!this.props.url ? this.props.url : Utils.getSiteUrl();
|
||||
const title = !!this.props.title ? this.props.title : getSetting("title", "Nova");
|
||||
const description = !!this.props.description ? this.props.description : getSetting("tagline");
|
||||
|
||||
// default image meta: logo url, else site image defined in settings
|
||||
let image = !!Telescope.settings.get("siteImage") ? Telescope.settings.get("siteImage"): Telescope.settings.get("logoUrl");
|
||||
let image = !!getSetting("siteImage") ? getSetting("siteImage"): getSetting("logoUrl");
|
||||
|
||||
// overwrite default image if one is passed as props
|
||||
if (!!this.props.image) {
|
||||
|
@ -19,10 +19,10 @@ class HeadTags extends Component {
|
|||
|
||||
// add site url base if the image is stored locally
|
||||
if (!!image && image.indexOf('//') === -1) {
|
||||
image = Telescope.utils.getSiteUrl() + image;
|
||||
image = Utils.getSiteUrl() + image;
|
||||
}
|
||||
|
||||
const meta = Telescope.headtags.meta.concat([
|
||||
const meta = Headtags.meta.concat([
|
||||
{ charset: "utf-8" },
|
||||
{ name: "description", content: description },
|
||||
// responsive
|
||||
|
@ -40,9 +40,9 @@ class HeadTags extends Component {
|
|||
{ name: "twitter:description", content: description }
|
||||
]);
|
||||
|
||||
const link = Telescope.headtags.link.concat([
|
||||
{ rel: "canonical", href: Telescope.utils.getSiteUrl() },
|
||||
{ rel: "shortcut icon", href: Telescope.settings.get("faviconUrl", "/img/favicon.ico") }
|
||||
const link = Headtags.link.concat([
|
||||
{ rel: "canonical", href: Utils.getSiteUrl() },
|
||||
{ rel: "shortcut icon", href: getSetting("faviconUrl", "/img/favicon.ico") }
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@ -60,5 +60,4 @@ HeadTags.propTypes = {
|
|||
image: React.PropTypes.string,
|
||||
};
|
||||
|
||||
module.exports = HeadTags;
|
||||
export default HeadTags;
|
||||
registerComponent('HeadTags', HeadTags);
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import React from 'react';
|
||||
//import { Messages } from "meteor/nova:core";
|
||||
import { withCurrentUser, getSetting, Components, registerComponent } from 'meteor/nova:core';
|
||||
|
||||
const Header = (props, {currentUser}) => {
|
||||
const Header = (props, context) => {
|
||||
|
||||
const logoUrl = Telescope.settings.get("logoUrl");
|
||||
const siteTitle = Telescope.settings.get("title", "Nova");
|
||||
const tagline = Telescope.settings.get("tagline");
|
||||
const logoUrl = getSetting("logoUrl");
|
||||
const siteTitle = getSetting("title", "Nova");
|
||||
const tagline = getSetting("tagline");
|
||||
|
||||
return (
|
||||
<div className="header-wrapper">
|
||||
|
@ -14,18 +13,18 @@ const Header = (props, {currentUser}) => {
|
|||
<header className="header">
|
||||
|
||||
<div className="logo">
|
||||
<Telescope.components.Logo logoUrl={logoUrl} siteTitle={siteTitle} />
|
||||
<Components.Logo logoUrl={logoUrl} siteTitle={siteTitle} />
|
||||
{tagline ? <h2 className="tagline">{tagline}</h2> : "" }
|
||||
</div>
|
||||
|
||||
<div className="nav">
|
||||
|
||||
<div className="nav-user">
|
||||
{currentUser ? <Telescope.components.UsersMenu/> : <Telescope.components.UsersAccountMenu/>}
|
||||
{!!props.currentUser ? <Components.UsersMenu/> : <Components.UsersAccountMenu/>}
|
||||
</div>
|
||||
|
||||
<div className="nav-new-post">
|
||||
<Telescope.components.PostsNewButton/>
|
||||
<Components.PostsNewButton/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -37,8 +36,8 @@ const Header = (props, {currentUser}) => {
|
|||
|
||||
Header.displayName = "Header";
|
||||
|
||||
Header.contextTypes = {
|
||||
Header.propTypes = {
|
||||
currentUser: React.PropTypes.object,
|
||||
};
|
||||
|
||||
module.exports = Header;
|
||||
registerComponent('Header', Header, withCurrentUser);
|
||||
|
|
|
@ -1,38 +1,35 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { FlashContainer } from "meteor/nova:core";
|
||||
|
||||
class Layout extends Component {
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<div className="wrapper" id="wrapper">
|
||||
|
||||
<Telescope.components.HeadTags />
|
||||
<Components.HeadTags />
|
||||
|
||||
<Telescope.components.UsersProfileCheck {...this.props} />
|
||||
<Components.UsersProfileCheck {...this.props} />
|
||||
|
||||
<Telescope.components.Header {...this.props}/>
|
||||
<Components.Header {...this.props}/>
|
||||
|
||||
<div className="main">
|
||||
|
||||
<FlashContainer component={Telescope.components.FlashMessages}/>
|
||||
<Components.FlashMessages />
|
||||
|
||||
<Telescope.components.Newsletter />
|
||||
<Components.Newsletter />
|
||||
|
||||
{this.props.children}
|
||||
|
||||
</div>
|
||||
|
||||
<Telescope.components.Footer {...this.props}/>
|
||||
<Components.Footer {...this.props}/>
|
||||
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Layout.displayName = "Layout";
|
||||
|
||||
module.exports = Layout;
|
||||
registerComponent('Layout', Layout);
|
|
@ -1,3 +1,4 @@
|
|||
import { registerComponent } from 'meteor/nova:lib';
|
||||
import React from 'react';
|
||||
import { IndexLink } from 'react-router';
|
||||
|
||||
|
@ -21,4 +22,4 @@ const Logo = ({logoUrl, siteTitle}) => {
|
|||
|
||||
Logo.displayName = "Logo";
|
||||
|
||||
module.exports = Logo;
|
||||
registerComponent('Logo', Logo);
|
|
@ -1,4 +1,4 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { FormattedMessage, intlShape } from 'react-intl';
|
||||
import Formsy from 'formsy-react';
|
||||
|
@ -6,6 +6,7 @@ import { Input } from 'formsy-react-components';
|
|||
import { Button } from 'react-bootstrap';
|
||||
import Cookie from 'react-cookie';
|
||||
import Users from 'meteor/nova:users';
|
||||
import { withCurrentUser, withMutation, withMessages } from 'meteor/nova:core';
|
||||
|
||||
class Newsletter extends Component {
|
||||
|
||||
|
@ -16,29 +17,27 @@ class Newsletter extends Component {
|
|||
this.dismissBanner = this.dismissBanner.bind(this);
|
||||
|
||||
this.state = {
|
||||
showBanner: showBanner(context.currentUser)
|
||||
showBanner: showBanner(props.currentUser)
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if (nextContext.currentUser) {
|
||||
this.setState({showBanner: showBanner(nextContext.currentUser)});
|
||||
if (nextProps.currentUser) {
|
||||
this.setState({showBanner: showBanner(nextProps.currentUser)});
|
||||
}
|
||||
}
|
||||
|
||||
subscribeEmail(data) {
|
||||
this.context.actions.call("newsletter.addEmail", data.email, (error, result) => {
|
||||
if (error) {
|
||||
console.log(error); // eslint-disable-line
|
||||
this.context.messages.flash(error.message, "error");
|
||||
} else {
|
||||
this.successCallbackSubscription(result);
|
||||
}
|
||||
this.props.addEmailNewsletter({email: data.email}).then(result => {
|
||||
this.successCallbackSubscription(result);
|
||||
}).catch(error => {
|
||||
console.log(error);
|
||||
this.props.flash(error.message, "error");
|
||||
});
|
||||
}
|
||||
|
||||
successCallbackSubscription(result) {
|
||||
this.context.messages.flash(this.context.intl.formatMessage({id: "newsletter.success_message"}), "success");
|
||||
this.props.flash(this.context.intl.formatMessage({id: "newsletter.success_message"}), "success");
|
||||
this.dismissBanner();
|
||||
}
|
||||
|
||||
|
@ -47,20 +46,16 @@ class Newsletter extends Component {
|
|||
|
||||
this.setState({showBanner: false});
|
||||
|
||||
// set cookie
|
||||
// set cookie to keep the banner dismissed persistently
|
||||
Cookie.save('showBanner', "no");
|
||||
|
||||
// set user setting too (if logged in)
|
||||
if (this.context.currentUser) {
|
||||
this.context.actions.call('users.setSetting', this.context.currentUser._id, 'newsletter.showBanner', false);
|
||||
}
|
||||
}
|
||||
|
||||
renderButton() {
|
||||
return <Telescope.components.NewsletterButton
|
||||
return <Components.NewsletterButton
|
||||
label="newsletter.subscribe"
|
||||
mutationName="addUserNewsletter"
|
||||
successCallback={() => this.successCallbackSubscription()}
|
||||
subscribeText={this.context.intl.formatMessage({id: "newsletter.subscribe"})}
|
||||
user={this.context.currentUser}
|
||||
user={this.props.currentUser}
|
||||
/>
|
||||
}
|
||||
|
||||
|
@ -84,30 +79,32 @@ class Newsletter extends Component {
|
|||
? (
|
||||
<div className="newsletter">
|
||||
<h4 className="newsletter-tagline"><FormattedMessage id="newsletter.subscribe_prompt"/></h4>
|
||||
{this.context.currentUser ? this.renderButton() : this.renderForm()}
|
||||
<a onClick={this.dismissBanner} className="newsletter-close"><Telescope.components.Icon name="close"/></a>
|
||||
{this.props.currentUser ? this.renderButton() : this.renderForm()}
|
||||
<a onClick={this.dismissBanner} className="newsletter-close"><Components.Icon name="close"/></a>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
|
||||
Newsletter.contextTypes = {
|
||||
currentUser: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
messages: React.PropTypes.object,
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
const mutationOptions = {
|
||||
name: 'addEmailNewsletter',
|
||||
args: {email: 'String'}
|
||||
}
|
||||
|
||||
function showBanner (user) {
|
||||
return (
|
||||
// showBanner cookie either doesn't exist or is not set to "no"
|
||||
Cookie.load('showBanner') !== "no"
|
||||
// and showBanner user setting either doesn't exist or is set to true
|
||||
&& Users.getSetting(user, 'newsletter.showBanner', true)
|
||||
// && Users.getSetting(user, 'newsletter.showBanner', true)
|
||||
// and user is not subscribed to the newsletter already (setting either DNE or is not set to false)
|
||||
&& !Users.getSetting(user, 'newsletter.subscribed', false)
|
||||
&& !Users.getSetting(user, 'newsletter_subscribeToNewsletter', false)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = Newsletter;
|
||||
export default Newsletter;
|
||||
registerComponent('Newsletter', Newsletter, withMutation(mutationOptions), withCurrentUser, withMessages);
|
||||
|
|
|
@ -1,53 +1,61 @@
|
|||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Button } from 'react-bootstrap';
|
||||
// import { Messages } from 'meteor/nova:core';
|
||||
import Users from 'meteor/nova:users';
|
||||
import { withMutation, withCurrentUser, withMessages } from 'meteor/nova:core';
|
||||
|
||||
class NewsletterButton extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.subscriptionAction = this.subscriptionAction.bind(this);
|
||||
}
|
||||
|
||||
subscriptionAction() {
|
||||
const action = Users.getSetting(this.props.user, 'newsletter.subscribed', false) ?
|
||||
'newsletter.removeUser' : 'newsletter.addUser';
|
||||
this.context.actions.call(action, this.props.user, (error, result) => {
|
||||
if (error) {
|
||||
console.log(error); // eslint-disable-line
|
||||
this.context.messages.flash(error.message, "error");
|
||||
} else {
|
||||
this.props.successCallback(result);
|
||||
}
|
||||
});
|
||||
|
||||
// use async/await + try/catch <=> promise.then(res => ..).catch(e => ...)
|
||||
async subscriptionAction() {
|
||||
|
||||
const {
|
||||
flash,
|
||||
mutationName,
|
||||
successCallback,
|
||||
user,
|
||||
[mutationName]: mutationToTrigger, // dynamic 'mutationToTrigger' variable based on the mutationName (addUserNewsletter or removeUserNewsletter)
|
||||
} = this.props;
|
||||
|
||||
try {
|
||||
const mutationResult = await mutationToTrigger({userId: user._id});
|
||||
|
||||
successCallback(mutationResult);
|
||||
} catch(error) {
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
|
||||
flash(error.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const isSubscribed = Users.getSetting(this.props.user, 'newsletter.subscribed', false);
|
||||
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="newsletter-button"
|
||||
onClick={this.subscriptionAction}
|
||||
bsStyle="primary"
|
||||
>
|
||||
{isSubscribed ? <FormattedMessage id="newsletter.unsubscribe"/> : <FormattedMessage id="newsletter.subscribe"/>}
|
||||
<FormattedMessage id={this.props.label}/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NewsletterButton.propTypes = {
|
||||
user: React.PropTypes.object.isRequired,
|
||||
successCallback: React.PropTypes.func.isRequired,
|
||||
mutationName: PropTypes.string.isRequired, // mutation to fire
|
||||
label: PropTypes.string.isRequired, // label of the button
|
||||
user: PropTypes.object.isRequired, // user to operate on
|
||||
successCallback: PropTypes.func.isRequired, // what do to after the mutationName
|
||||
addUserNewsletter: PropTypes.func.isRequired, // prop given by withMutation HOC
|
||||
removeUserNewsletter: PropTypes.func.isRequired, // prop given by withMutation HOC
|
||||
};
|
||||
|
||||
NewsletterButton.contextTypes = {
|
||||
currentUser: React.PropTypes.object,
|
||||
messages: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
}
|
||||
const addOptions = {name: 'addUserNewsletter', args: {userId: 'String'}};
|
||||
const removeOptions = {name: 'removeUserNewsletter', args: {userId: 'String'}};
|
||||
|
||||
module.exports = NewsletterButton;
|
||||
export default NewsletterButton;
|
||||
registerComponent('NewsletterButton', NewsletterButton, withCurrentUser, withMutation(addOptions), withMutation(removeOptions), withMessages);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { registerComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { intlShape } from 'react-intl';
|
||||
import Formsy from 'formsy-react';
|
||||
|
@ -60,10 +61,7 @@ class SearchForm extends Component{
|
|||
}
|
||||
|
||||
SearchForm.contextTypes = {
|
||||
currentRoute: React.PropTypes.object,
|
||||
currentUser: React.PropTypes.object,
|
||||
intl: intlShape
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = withRouter(SearchForm);
|
||||
export default withRouter(SearchForm);
|
||||
registerComponent('SearchForm', SearchForm, withRouter);
|
|
@ -1,43 +0,0 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { FormattedMessage, intlShape } from 'react-intl';
|
||||
import NovaForm from "meteor/nova:forms";
|
||||
import { DocumentContainer } from "meteor/utilities:react-list-container";
|
||||
//import { Messages } from "meteor/nova:core";
|
||||
|
||||
class SettingsEditForm extends Component{
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<div className="settings-edit-form">
|
||||
<p><FormattedMessage id="settings.json_message"/></p>
|
||||
<DocumentContainer
|
||||
collection={Telescope.settings.collection}
|
||||
publication="settings.admin"
|
||||
selector={{}}
|
||||
terms={{}}
|
||||
component={NovaForm}
|
||||
componentProps={{
|
||||
// note: the document prop will be passed from DocumentContainer
|
||||
collection: Telescope.settings.collection,
|
||||
currentUser: this.context.currentUser,
|
||||
methodName: "settings.edit",
|
||||
successCallback: (category) => {
|
||||
this.context.messages.flash(this.context.intl.formatMessage({id: "settings.edited"}), "success");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsEditForm.contextTypes = {
|
||||
currentUser: React.PropTypes.object,
|
||||
messages: React.PropTypes.object,
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
module.exports = SettingsEditForm;
|
||||
export default SettingsEditForm;
|
|
@ -1,55 +1,81 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import { Components, registerComponent } from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Users from 'meteor/nova:users';
|
||||
import { withCurrentUser, withMessages } from 'meteor/nova:core';
|
||||
import { withVote, hasUpvoted, hasDownvoted } from 'meteor/nova:voting';
|
||||
|
||||
class Vote extends Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.upvote = this.upvote.bind(this);
|
||||
this.getActionClass = this.getActionClass.bind(this);
|
||||
// this.startLoading = this.startLoading.bind(this);
|
||||
// this.stopLoading = this.stopLoading.bind(this);
|
||||
this.state = {
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
note: with optimisitc UI, loading functions are not needed
|
||||
also, setState triggers issues when the component is unmounted
|
||||
before the vote mutation returns.
|
||||
|
||||
*/
|
||||
|
||||
// startLoading() {
|
||||
// this.setState({ loading: true });
|
||||
// }
|
||||
|
||||
// stopLoading() {
|
||||
// this.setState({ loading: false });
|
||||
// }
|
||||
|
||||
upvote(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const post = this.props.post;
|
||||
const user = this.context.currentUser;
|
||||
// this.startLoading();
|
||||
|
||||
const document = this.props.document;
|
||||
const collection = this.props.collection;
|
||||
const user = this.props.currentUser;
|
||||
|
||||
if(!user){
|
||||
this.context.messages.flash("Please log in first");
|
||||
} else if (user.hasUpvoted(post)) {
|
||||
this.context.actions.call('posts.cancelUpvote', post._id, () => {
|
||||
this.context.events.track("post upvote cancelled", {'_id': post._id});
|
||||
});
|
||||
this.props.flash("Please log in first");
|
||||
// this.stopLoading();
|
||||
} else {
|
||||
this.context.actions.call('posts.upvote', post._id, () => {
|
||||
this.context.events.track("post upvoted", {'_id': post._id});
|
||||
const voteType = hasUpvoted(user, document) ? "cancelUpvote" : "upvote";
|
||||
this.props.vote({document, voteType, collection, currentUser: this.props.currentUser}).then(result => {
|
||||
// this.stopLoading();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getActionClass() {
|
||||
const document = this.props.document;
|
||||
const user = this.props.currentUser;
|
||||
|
||||
const isUpvoted = hasUpvoted(user, document);
|
||||
const isDownvoted = hasDownvoted(user, document);
|
||||
const actionsClass = classNames(
|
||||
'vote',
|
||||
{voted: isUpvoted || isDownvoted},
|
||||
{upvoted: isUpvoted},
|
||||
{downvoted: isDownvoted}
|
||||
);
|
||||
|
||||
return actionsClass;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const post = this.props.post;
|
||||
const user = this.context.currentUser;
|
||||
|
||||
const hasUpvoted = Users.hasUpvoted(user, post);
|
||||
const hasDownvoted = Users.hasDownvoted(user, post);
|
||||
const actionsClass = classNames(
|
||||
"vote",
|
||||
{voted: hasUpvoted || hasDownvoted},
|
||||
{upvoted: hasUpvoted},
|
||||
{downvoted: hasDownvoted}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={actionsClass}>
|
||||
<div className={this.getActionClass()}>
|
||||
<a className="upvote-button" onClick={this.upvote}>
|
||||
<Telescope.components.Icon name="upvote" />
|
||||
{this.state.loading ? <Components.Icon name="spinner" /> : <Components.Icon name="upvote" /> }
|
||||
<div className="sr-only">Upvote</div>
|
||||
<div className="vote-count">{post.baseScore || 0}</div>
|
||||
<div className="vote-count">{this.props.document.baseScore || 0}</div>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
|
@ -58,15 +84,15 @@ class Vote extends Component {
|
|||
}
|
||||
|
||||
Vote.propTypes = {
|
||||
post: React.PropTypes.object.isRequired, // the current post
|
||||
document: React.PropTypes.object.isRequired, // the document to upvote
|
||||
collection: React.PropTypes.object.isRequired, // the collection containing the document
|
||||
vote: React.PropTypes.func.isRequired, // mutate function with callback inside
|
||||
currentUser: React.PropTypes.object, // user might not be logged in, so don't make it required
|
||||
};
|
||||
|
||||
Vote.contextTypes = {
|
||||
currentUser: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
events: React.PropTypes.object,
|
||||
messages: React.PropTypes.object
|
||||
};
|
||||
|
||||
module.exports = Vote;
|
||||
export default Vote;
|
||||
registerComponent('Vote', Vote, withMessages, withVote);
|
||||
|
|
|
@ -1,79 +1,74 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
|
||||
// common
|
||||
|
||||
Telescope.registerComponent("App", require('./common/App.jsx'));
|
||||
Telescope.registerComponent("Footer", require('./common/Footer.jsx'));
|
||||
Telescope.registerComponent("Header", require('./common/Header.jsx'));
|
||||
Telescope.registerComponent("Layout", require('./common/Layout.jsx'));
|
||||
Telescope.registerComponent("Logo", require('./common/Logo.jsx'));
|
||||
Telescope.registerComponent("Flash", require('./common/Flash.jsx'));
|
||||
Telescope.registerComponent('HeadTags', require('./common/HeadTags.jsx'));
|
||||
Telescope.registerComponent("FlashMessages", require('./common/FlashMessages.jsx'));
|
||||
Telescope.registerComponent("Newsletter", require('./common/Newsletter.jsx'));
|
||||
Telescope.registerComponent("NewsletterButton", require('./common/NewsletterButton.jsx'));
|
||||
Telescope.registerComponent("Icon", require('./common/Icon.jsx'));
|
||||
Telescope.registerComponent("SearchForm", require('./common/SearchForm.jsx'));
|
||||
Telescope.registerComponent("AppLoading", require('./common/AppLoading.jsx'));
|
||||
Telescope.registerComponent("Error404", require('./common/Error404.jsx'));
|
||||
Telescope.registerComponent("Loading", require('./common/Loading.jsx'));
|
||||
Telescope.registerComponent("Vote", require('./common/Vote.jsx'));
|
||||
Telescope.registerComponent("SettingsEditForm", require('./common/SettingsEditForm.jsx'));
|
||||
import './common/Footer.jsx';
|
||||
import './common/Header.jsx';
|
||||
import './common/Layout.jsx';
|
||||
import './common/Logo.jsx';
|
||||
import './common/Flash.jsx';
|
||||
import './common/HeadTags.jsx';
|
||||
import './common/FlashMessages.jsx';
|
||||
import './common/Newsletter.jsx';
|
||||
import './common/NewsletterButton.jsx';
|
||||
import './common/SearchForm.jsx';
|
||||
import './common/Error404.jsx';
|
||||
import './common/Vote.jsx';
|
||||
|
||||
// posts
|
||||
|
||||
Telescope.registerComponent("PostsHome", require('./posts/PostsHome.jsx'));
|
||||
Telescope.registerComponent("PostsSingle", require('./posts/PostsSingle.jsx'));
|
||||
Telescope.registerComponent("PostsNewButton", require('./posts/PostsNewButton.jsx'));
|
||||
Telescope.registerComponent("PostsLoadMore", require('./posts/PostsLoadMore.jsx'));
|
||||
Telescope.registerComponent("PostsNoMore", require('./posts/PostsNoMore.jsx'));
|
||||
Telescope.registerComponent("PostsNoResults", require('./posts/PostsNoResults.jsx'));
|
||||
Telescope.registerComponent("PostsItem", require('./posts/PostsItem.jsx'));
|
||||
Telescope.registerComponent("PostsLoading", require('./posts/PostsLoading.jsx'));
|
||||
Telescope.registerComponent("PostsViews", require('./posts/PostsViews.jsx'));
|
||||
Telescope.registerComponent("PostsList", require('./posts/PostsList.jsx'));
|
||||
Telescope.registerComponent("PostsListHeader", require('./posts/PostsListHeader.jsx'));
|
||||
Telescope.registerComponent("PostsCategories", require('./posts/PostsCategories.jsx'));
|
||||
Telescope.registerComponent("PostsCommenters", require('./posts/PostsCommenters.jsx'));
|
||||
Telescope.registerComponent("PostsPage", require('./posts/PostsPage.jsx'));
|
||||
Telescope.registerComponent("PostsStats", require('./posts/PostsStats.jsx'));
|
||||
Telescope.registerComponent("PostsDaily", require('./posts/PostsDaily.jsx'));
|
||||
Telescope.registerComponent("PostsDay", require('./posts/PostsDay.jsx'));
|
||||
Telescope.registerComponent("PostsThumbnail", require('./posts/PostsThumbnail.jsx'));
|
||||
Telescope.registerComponent("PostsEditForm", require('./posts/PostsEditForm.jsx'));
|
||||
Telescope.registerComponent("PostsNewForm", require('./posts/PostsNewForm.jsx'));
|
||||
Telescope.registerComponent("PostsCommentsThread", require('./posts/PostsCommentsThread.jsx'));
|
||||
import './posts/PostsHome.jsx';
|
||||
import './posts/PostsSingle.jsx';
|
||||
import './posts/PostsNewButton.jsx';
|
||||
import './posts/PostsLoadMore.jsx';
|
||||
import './posts/PostsNoMore.jsx';
|
||||
import './posts/PostsNoResults.jsx';
|
||||
import './posts/PostsItem.jsx';
|
||||
import './posts/PostsLoading.jsx';
|
||||
import './posts/PostsViews.jsx';
|
||||
import './posts/PostsList.jsx';
|
||||
import './posts/PostsListHeader.jsx';
|
||||
import './posts/PostsCategories.jsx';
|
||||
import './posts/PostsCommenters.jsx';
|
||||
import './posts/PostsPage.jsx';
|
||||
import './posts/PostsStats.jsx';
|
||||
import './posts/PostsDaily.jsx';
|
||||
import './posts/PostsDailyList.jsx';
|
||||
import './posts/PostsDay.jsx';
|
||||
import './posts/PostsThumbnail.jsx';
|
||||
import './posts/PostsEditForm.jsx';
|
||||
import './posts/PostsNewForm.jsx';
|
||||
import './posts/PostsCommentsThread.jsx';
|
||||
|
||||
// comments
|
||||
|
||||
Telescope.registerComponent("CommentsItem", require('./comments/CommentsItem.jsx'));
|
||||
Telescope.registerComponent("CommentsList", require('./comments/CommentsList.jsx'));
|
||||
Telescope.registerComponent("CommentsNode", require('./comments/CommentsNode.jsx'));
|
||||
Telescope.registerComponent("CommentsNew", require('./comments/CommentsNew.jsx'));
|
||||
Telescope.registerComponent("CommentsEdit", require('./comments/CommentsEdit.jsx'));
|
||||
Telescope.registerComponent("CommentsLoadMore", require('./comments/CommentsLoadMore.jsx'));
|
||||
import './comments/CommentsItem.jsx';
|
||||
import './comments/CommentsList.jsx';
|
||||
import './comments/CommentsNode.jsx';
|
||||
import './comments/CommentsNewForm.jsx';
|
||||
import './comments/CommentsEditForm.jsx';
|
||||
import './comments/CommentsLoadMore.jsx';
|
||||
|
||||
// categories
|
||||
|
||||
Telescope.registerComponent("CategoriesList", require('./categories/CategoriesList.jsx'));
|
||||
Telescope.registerComponent("Category", require('./categories/Category.jsx'));
|
||||
Telescope.registerComponent("CategoriesEditForm", require('./categories/CategoriesEditForm.jsx'));
|
||||
Telescope.registerComponent("CategoriesNewForm", require('./categories/CategoriesNewForm.jsx'));
|
||||
import './categories/CategoriesList.jsx';
|
||||
import './categories/Category.jsx';
|
||||
import './categories/CategoriesEditForm.jsx';
|
||||
import './categories/CategoriesNewForm.jsx';
|
||||
|
||||
// permissions
|
||||
|
||||
Telescope.registerComponent("CanDo", require('./permissions/CanDo.jsx'));
|
||||
import './permissions/CanDo.jsx';
|
||||
|
||||
// users
|
||||
|
||||
Telescope.registerComponent("UsersSingle", require('./users/UsersSingle.jsx'));
|
||||
Telescope.registerComponent("UsersAccount", require('./users/UsersAccount.jsx'));
|
||||
Telescope.registerComponent("UsersEdit", require('./users/UsersEdit.jsx'));
|
||||
Telescope.registerComponent("UsersProfile", require('./users/UsersProfile.jsx'));
|
||||
Telescope.registerComponent("UsersProfileCheck", require('./users/UsersProfileCheck.jsx'));
|
||||
Telescope.registerComponent("UsersAvatar", require('./users/UsersAvatar.jsx'));
|
||||
Telescope.registerComponent("UsersName", require('./users/UsersName.jsx'));
|
||||
Telescope.registerComponent("UsersMenu", require('./users/UsersMenu.jsx'));
|
||||
Telescope.registerComponent("UsersAccountMenu", require('./users/UsersAccountMenu.jsx'));
|
||||
Telescope.registerComponent("UsersAccountForm", require('./users/UsersAccountForm.jsx'));
|
||||
Telescope.registerComponent("UsersResetPassword", require('./users/UsersResetPassword.jsx'));
|
||||
import './users/UsersSingle.jsx';
|
||||
import './users/UsersAccount.jsx';
|
||||
import './users/UsersEditForm.jsx';
|
||||
import './users/UsersProfile.jsx';
|
||||
import './users/UsersProfileCheck.jsx';
|
||||
import './users/UsersAvatar.jsx';
|
||||
import './users/UsersName.jsx';
|
||||
import './users/UsersMenu.jsx';
|
||||
import './users/UsersAccountMenu.jsx';
|
||||
import './users/UsersAccountForm.jsx';
|
||||
import './users/UsersResetPassword.jsx';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue