mirror of
https://github.com/wassname/talk.git
synced 2026-08-17 11:27:52 +08:00
Merge branch 'master' of github.com:coralproject/talk into load-more-comments
This commit is contained in:
+7
-9
@@ -1,9 +1,10 @@
|
||||
# Contributor's Guide
|
||||
|
||||
Welcome! We are very excited that you are interested in contributing to Talk.
|
||||
Welcome! We are very excited that you are interested in contributing to Talk.
|
||||
|
||||
This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
|
||||
|
||||
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html).
|
||||
|
||||
## Product Roadmap
|
||||
|
||||
@@ -13,7 +14,7 @@ You can view product ideas and our longer term roadmap here https://trello.com/b
|
||||
|
||||
|
||||
## Contribute to the documentation
|
||||
Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally.
|
||||
Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally.
|
||||
|
||||
We are looking for _documentarians_ to:
|
||||
|
||||
@@ -40,7 +41,7 @@ Talk is designed to integrate into existing environments in a variety of ways:
|
||||
|
||||
If you're considering deploying Talk, [please let us know](https://github.com/coralproject/talk/wiki/Contact-Us)! We are quite literally doing this for you and want to help you succeed any way we can.
|
||||
|
||||
If you are writing custom integration code in your fork of Talk, please consider keeping it generic and filing a Pull Request to contribute it back to the project! See our [forking and merging guidelines](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging) for more info.
|
||||
If you are writing custom integration code in your fork of Talk, please consider keeping it generic and filing a Pull Request to contribute it back to the project! See our [forking and merging guidelines](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging) for more info.
|
||||
|
||||
## Write some code
|
||||
|
||||
@@ -48,9 +49,9 @@ First, [set up a dev environment](https://github.com/coralproject/talk/blob/mast
|
||||
|
||||
### Build a New Feature / Plugin
|
||||
|
||||
Talk is beginning life as a Commenting Platform, but is architected to support many varieties of community engagement.
|
||||
Talk is beginning life as a Commenting Platform, but is architected to support many varieties of community engagement.
|
||||
|
||||
Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early and often if you'd like to help. We would love to hear your ideas for features and plugins and help you find a way to productively engage the project.
|
||||
Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early and often if you'd like to help. We would love to hear your ideas for features and plugins and help you find a way to productively engage the project.
|
||||
|
||||
To get an idea of where the Coral Team is going, see:
|
||||
|
||||
@@ -66,13 +67,10 @@ Examples:
|
||||
|
||||
### Work on the Core
|
||||
|
||||
There is always more work to be done to make an application more stable, scaleable and secure.
|
||||
There is always more work to be done to make an application more stable, scaleable and secure.
|
||||
|
||||
If you see issues in the code or have ideas on how we may improve Talk, please consider:
|
||||
|
||||
* [contributing a fix](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging),
|
||||
* [filing an issue](https://github.com/coralproject/talk/issues), or
|
||||
* or otherwise [letting us know](https://github.com/coralproject/talk/wiki/Contact-Us).
|
||||
|
||||
|
||||
|
||||
|
||||
+60
-61
@@ -1,95 +1,94 @@
|
||||
# Installing a dev environment
|
||||
|
||||
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html).
|
||||
# Installation
|
||||
|
||||
## Requirements
|
||||
|
||||
### System
|
||||
|
||||
- Any flavor of Linux, OSX or Windows
|
||||
- Any flavour of Linux, OSX or Windows
|
||||
- 1GB memory (minimum)
|
||||
- 5GB storage (minimum)
|
||||
|
||||
### Software
|
||||
## Installation From Source
|
||||
|
||||
* [Node](https://nodejs.org/es/download/package-manager) v7 or later
|
||||
* Mongo v3.2 or later
|
||||
* Redis v3.2 or later
|
||||
### Requirements
|
||||
|
||||
There are some runtime requirements for running Talk from source:
|
||||
|
||||
- [Node](https://nodejs.org/) v7 or later
|
||||
- [MongoDB](https://www.mongodb.com/) v3.2 or later
|
||||
- [Redis](https://redis.io/) v3.2 or later
|
||||
- [Yarn](https://yarnpkg.com/) v0.19.1 or later
|
||||
|
||||
_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_
|
||||
|
||||
## First time setup
|
||||
### Installing
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# Download the tarball containing the repository
|
||||
curl -L https://github.com/coralproject/talk/tarball/master -o coralproject-talk.tar.gz
|
||||
|
||||
Navigate to a directory.
|
||||
# Untar that file and change to that directory
|
||||
tar xpf coralproject-talk.tar.gz
|
||||
mv coralproject-talk-* coralproject-talk
|
||||
cd coralproject-talk
|
||||
|
||||
```
|
||||
git clone https://github.com/coralproject/talk
|
||||
cd talk
|
||||
yarn install
|
||||
```
|
||||
# Install package dependancies
|
||||
yarn
|
||||
|
||||
### Environmental Variables
|
||||
|
||||
Talk uses environmental variables for configuration. You can learn about them in the [README file](README.md).
|
||||
|
||||
|
||||
## Workflows
|
||||
|
||||
### The server
|
||||
|
||||
Starting the server:
|
||||
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
Browse to `http://localhost:3000` (or your custom port.)
|
||||
|
||||
### Building the front end
|
||||
|
||||
Our build process will build all front end components registered [here](https://github.com/coralproject/talk/blob/6052cac1d3494f8060325a88bb2ce03c88c2f94c/webpack.config.dev.js#L9-L15).
|
||||
|
||||
One time build:
|
||||
|
||||
```
|
||||
# Build static files
|
||||
yarn build
|
||||
```
|
||||
|
||||
Build, then rebuild when a file is updated (development build):
|
||||
### Running
|
||||
|
||||
```
|
||||
yarn build-watch
|
||||
Refer to the `README.md` file for required configuration variables to add to the
|
||||
environment.
|
||||
|
||||
You can start the server after configuring the server using the command:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
You can see other scripts we've made available by consulting the `package.json`
|
||||
file under the `scripts` key including:
|
||||
|
||||
### Testing
|
||||
- `yarn test` run unit tests
|
||||
- `yarn e2e` run end to end tests
|
||||
- `yarn build-watch` watch for changes to client files and build static assets
|
||||
- `yarn dev-start` watch for changes to server files and reload the server
|
||||
|
||||
Run all tests once:
|
||||
## Installation From Docker Hub
|
||||
|
||||
`
|
||||
yarn test
|
||||
`
|
||||
### Requirements
|
||||
|
||||
Run our end to end tests (will install Selenium and nightwatch):
|
||||
There are some runtime requirements for running Talk for Docker:
|
||||
|
||||
`
|
||||
yarn e2e
|
||||
`
|
||||
- [MongoDB](https://www.mongodb.com/) v3.2 or later
|
||||
- [Redis](https://redis.io/) v3.2 or later
|
||||
- [Docker](https://www.docker.com/) v1.13.0 or later
|
||||
- [Docker Compose](https://docs.docker.com/compose/) v1.10.0 or later
|
||||
|
||||
_Please ensure all tests are passing before submitting a PR!_
|
||||
_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_
|
||||
|
||||
## Troubleshooting
|
||||
### Installing
|
||||
|
||||
```bash
|
||||
# Create a directory for talk
|
||||
mkdir coralproject-talk
|
||||
cd coralproject-talk
|
||||
|
||||
##### Can't ping the redis server!
|
||||
# Download the docker-compose.yml file from the repository
|
||||
curl -LO https://raw.githubusercontent.com/coralproject/talk/master/docker-compose.yml
|
||||
```
|
||||
|
||||
- Check that Redis Server is running.
|
||||
- Check that TALK_REDIS_URL is set.
|
||||
|
||||
##### Authenticaiton doesn't work!
|
||||
|
||||
- Make sure Redis is the correct version.
|
||||
At this stage, you should refer to the `README.md` file for required
|
||||
configuration variables to add to the environment key for the `talk` service
|
||||
listed in the `docker-compose.yml` file.
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
# Start the services using compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
@@ -12,7 +12,7 @@ See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CO
|
||||
|
||||
To set up a development environment or build from source, see [INSTALL.md](https://github.com/coralproject/talk/blob/master/INSTALL.md).
|
||||
|
||||
To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!!
|
||||
To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!
|
||||
|
||||
|
||||
### Configuration
|
||||
@@ -37,6 +37,7 @@ Facebook Login enabled app.
|
||||
- `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using.
|
||||
- `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`.
|
||||
- `TALK_SMTP_PORT` (*required for email*) - SMTP port.
|
||||
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint.
|
||||
|
||||
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
|
||||
alternative methods of loading configuration during development.
|
||||
|
||||
@@ -94,6 +94,11 @@ if (app.get('env') !== 'production') {
|
||||
endpointURL: '/api/v1/graph/ql'
|
||||
}));
|
||||
|
||||
// GraphQL documention.
|
||||
app.get('/admin/docs', (req, res) => {
|
||||
res.render('admin/docs');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const util = require('../util');
|
||||
// const util = require('./util');
|
||||
const program = require('./commander');
|
||||
|
||||
program
|
||||
@@ -15,18 +15,14 @@ program
|
||||
.command('users', 'work with the application auth')
|
||||
.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// The ensures that the child process that is created here is always cleaned up
|
||||
// properly when the parent process dies.
|
||||
util.onshutdown([
|
||||
(signal) => {
|
||||
if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) {
|
||||
program.runningCommand.kill(signal);
|
||||
}
|
||||
/**
|
||||
* When this provess exists, check to see if we have a running command, if we do
|
||||
* check to see if it is still running. If it is, then kill it with a SIGINT
|
||||
* signal. This is for the use case where we want to kill the process that is
|
||||
* labled with the PID written out by the parent process.
|
||||
*/
|
||||
process.once('exit', () => {
|
||||
if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) {
|
||||
program.runningCommand.kill('SIGINT');
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ const Table = require('cli-table');
|
||||
const AssetModel = require('../models/asset');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const util = require('../util');
|
||||
const util = require('./util');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
const program = require('./commander');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const util = require('../util');
|
||||
const util = require('./util');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const kue = require('../services/kue');
|
||||
|
||||
|
||||
+2
-3
@@ -2,13 +2,12 @@
|
||||
|
||||
const app = require('../app');
|
||||
const program = require('./commander');
|
||||
const debug = require('debug')('talk:server');
|
||||
const http = require('http');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const kue = require('../services/kue');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('../util');
|
||||
const util = require('./util');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
@@ -79,7 +78,7 @@ function onListening() {
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${ addr}`
|
||||
: `port ${ addr.port}`;
|
||||
debug(`Listening on ${ bind}`);
|
||||
console.log(`Listening on ${ bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+162
-54
@@ -9,7 +9,10 @@ const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingModel = require('../models/setting');
|
||||
const SettingsService = require('../services/settings');
|
||||
const util = require('../util');
|
||||
const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const util = require('./util');
|
||||
const errors = require('../errors');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
@@ -29,60 +32,165 @@ program
|
||||
// Setup the application
|
||||
//==============================================================================
|
||||
|
||||
SettingsService
|
||||
.init()
|
||||
.then((settings) => {
|
||||
if (program.defaults) {
|
||||
return settings.save();
|
||||
}
|
||||
const performSetup = () => {
|
||||
|
||||
console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n');
|
||||
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
default: settings.organizationName,
|
||||
validate: (input) => {
|
||||
if (input && input.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Organization Name is required.';
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
choices: SettingModel.MODERATION_OPTIONS,
|
||||
name: 'moderation',
|
||||
default: settings.moderation,
|
||||
message: 'Select a moderation mode'
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'requireEmailConfirmation',
|
||||
default: settings.requireEmailConfirmation,
|
||||
message: 'Should emails always be confirmed'
|
||||
}
|
||||
])
|
||||
.then((answers) => {
|
||||
|
||||
// Update the settings that were changed.
|
||||
Object.keys(answers).forEach((key) => {
|
||||
if (answers[key] !== undefined) {
|
||||
settings[key] = answers[key];
|
||||
}
|
||||
if (program.defaults) {
|
||||
return SettingsService
|
||||
.init()
|
||||
.then(() => {
|
||||
console.log('Settings created.');
|
||||
console.log('\nTalk is now installed!');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
return settings.save();
|
||||
// Get the current settings, we are expecing an error here.
|
||||
return SettingsService
|
||||
.retrieve()
|
||||
.then(() => {
|
||||
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
throw errors.ErrSettingsInit;
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (err !== errors.ErrSettingsNotInit) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
// Create the base settings model.
|
||||
let settings = new SettingModel();
|
||||
|
||||
console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n');
|
||||
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
default: settings.organizationName,
|
||||
validate: (input) => {
|
||||
if (input && input.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Organization Name is required.';
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
choices: SettingModel.MODERATION_OPTIONS,
|
||||
name: 'moderation',
|
||||
default: settings.moderation,
|
||||
message: 'Select a moderation mode'
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'requireEmailConfirmation',
|
||||
default: settings.requireEmailConfirmation,
|
||||
message: 'Should emails always be confirmed'
|
||||
}
|
||||
])
|
||||
.then((answers) => {
|
||||
|
||||
// Update the settings that were changed.
|
||||
Object.keys(answers).forEach((key) => {
|
||||
if (answers[key] !== undefined) {
|
||||
settings[key] = answers[key];
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
|
||||
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'displayName',
|
||||
message: 'Display Name',
|
||||
filter: (displayName) => {
|
||||
return UsersService
|
||||
.isValidDisplayName(displayName, false)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
format: 'email',
|
||||
validate: (value) => {
|
||||
if (value && value.length >= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Email is required';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: (password) => {
|
||||
return UsersService
|
||||
.isValidPassword(password)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword) => {
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
]);
|
||||
})
|
||||
.then((user) => {
|
||||
|
||||
if (user.password !== user.confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
return SetupService.setup({
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
password: user.password
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(({user}) => {
|
||||
console.log('Settings created.');
|
||||
console.log(`User ${user.id} created.`);
|
||||
console.log('\nTalk is now installed!');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Talk is now installed!');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
};
|
||||
|
||||
// Start tthe setup process.
|
||||
performSetup();
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ const inquirer = require('inquirer');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('../util');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
|
||||
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
|
||||
|
||||
+12
-2
@@ -2,7 +2,6 @@ const pkg = require('../package.json');
|
||||
const dotenv = require('dotenv');
|
||||
const fs = require('fs');
|
||||
const program = require('commander');
|
||||
const util = require('../util');
|
||||
|
||||
// Perform rewrites to the runtime environment variables based on the contents
|
||||
// of the process.env.REWRITE_ENV if it exists. This is done here as it is the
|
||||
@@ -27,6 +26,10 @@ const parseArgs = require('minimist')(process.argv.slice(2), {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* If the config flag is present, then we have to load the configuration from
|
||||
* the file specified. We will then load those values into the environment.
|
||||
*/
|
||||
if (parseArgs.config) {
|
||||
let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'}));
|
||||
|
||||
@@ -35,8 +38,15 @@ if (parseArgs.config) {
|
||||
});
|
||||
}
|
||||
|
||||
// If the `--pid` flag is used, put the current pid there.
|
||||
/**
|
||||
* If the pid flag is present, then we have to create a pid file at the location
|
||||
* specified.
|
||||
*/
|
||||
if (parseArgs.pid) {
|
||||
const util = require('./util');
|
||||
|
||||
console.log('Wrote PID');
|
||||
|
||||
util.pid(parseArgs.pid);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
|
||||
|
||||
import ModerationContainer from 'containers/ModerationQueue/ModerationContainer';
|
||||
import CommentStream from 'containers/CommentStream/CommentStream';
|
||||
import Configure from 'containers/Configure/Configure';
|
||||
import Streams from 'containers/Streams/Streams';
|
||||
import CommunityContainer from 'containers/Community/CommunityContainer';
|
||||
import Configure from 'containers/Configure/Configure';
|
||||
import LayoutContainer from 'containers/LayoutContainer';
|
||||
import CommentStream from 'containers/CommentStream/CommentStream';
|
||||
import InstallContainer from 'containers/Install/InstallContainer';
|
||||
import CommunityContainer from 'containers/Community/CommunityContainer';
|
||||
import ModerationContainer from 'containers/ModerationQueue/ModerationContainer';
|
||||
|
||||
const routes = (
|
||||
<Route path='/admin' component={LayoutContainer}>
|
||||
<IndexRoute component={ModerationContainer} />
|
||||
<Route path='embed' component={CommentStream} />
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='streams' component={Streams} />
|
||||
</Route>
|
||||
<div>
|
||||
<Route exact path="/admin/install" component={InstallContainer}/>
|
||||
<Route path='/admin' component={LayoutContainer}>
|
||||
<IndexRoute component={ModerationContainer} />
|
||||
<Route path='embed' component={CommentStream} />
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='streams' component={Streams} />
|
||||
</Route>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import coralApi from 'coral-framework/helpers/response';
|
||||
import * as actions from '../constants/install';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
|
||||
export const nextStep = () => ({type: actions.NEXT_STEP});
|
||||
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
|
||||
export const goToStep = step => ({type: actions.GO_TO_STEP, step});
|
||||
|
||||
const installRequest = () => ({type: actions.INSTALL_REQUEST});
|
||||
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
|
||||
const installFailure = error => ({type: actions.INSTALL_FAILURE, error});
|
||||
|
||||
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
|
||||
const hasError = error => ({type: actions.HAS_ERROR, error});
|
||||
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
|
||||
|
||||
const validation = (formData, dispatch, next) => {
|
||||
if (!(formData != null)) {
|
||||
return dispatch(hasError());
|
||||
}
|
||||
|
||||
// Required Validation
|
||||
const empty = Object.keys(formData).filter(name => {
|
||||
const cond = !formData[name].length;
|
||||
|
||||
if (cond) {
|
||||
|
||||
// Adding Error
|
||||
dispatch(addError(name, 'This field is required.'));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (empty.length) {
|
||||
return dispatch(hasError());
|
||||
}
|
||||
|
||||
// RegExp Validation
|
||||
const validation = Object.keys(formData).filter(name => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
|
||||
// Adding Error
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (validation.length) {
|
||||
return dispatch(hasError());
|
||||
}
|
||||
|
||||
dispatch(clearErrors());
|
||||
next();
|
||||
};
|
||||
|
||||
export const submitSettings = () => (dispatch, getState) => {
|
||||
const settingsFormData = getState().install.toJS().data.settings;
|
||||
validation(settingsFormData, dispatch, function() {
|
||||
dispatch(nextStep());
|
||||
});
|
||||
};
|
||||
|
||||
export const submitUser = () => (dispatch, getState) => {
|
||||
const userFormData = getState().install.toJS().data.user;
|
||||
validation(userFormData, dispatch, function() {
|
||||
const data = getState().install.toJS().data;
|
||||
dispatch(installRequest());
|
||||
coralApi('/setup', {method: 'POST', body: data})
|
||||
.then(result => {
|
||||
console.log(result);
|
||||
dispatch(installSuccess());
|
||||
dispatch(nextStep());
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(installFailure(`${error.message}`));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
|
||||
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
|
||||
|
||||
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
|
||||
const checkInstallSuccess = installed => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
const checkInstallFailure = error => ({type: actions.CHECK_INSTALL_FAILURE, error});
|
||||
|
||||
export const checkInstall = next => dispatch => {
|
||||
dispatch(checkInstallRequest());
|
||||
coralApi('/setup')
|
||||
.then(({installed}) => {
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(checkInstallFailure(`${error.message}`));
|
||||
});
|
||||
};
|
||||
@@ -11,6 +11,7 @@ export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
|
||||
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
|
||||
|
||||
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
|
||||
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
|
||||
|
||||
export const fetchSettings = () => dispatch => {
|
||||
dispatch({type: SETTINGS_LOADING});
|
||||
@@ -33,6 +34,10 @@ export const updateWordlist = (listName, list) => {
|
||||
return {type: WORDLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const updateDomainlist = (listName, list) => {
|
||||
return {type: DOMAINLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const saveSettingsToServer = () => (dispatch, getState) => {
|
||||
let settings = getState().settings.toJS().settings;
|
||||
if (settings.charCount) {
|
||||
|
||||
@@ -6,34 +6,49 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {Logo} from './Logo';
|
||||
|
||||
export default ({handleLogout}) => (
|
||||
export default ({handleLogout, restricted = false}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo />
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink className={styles.navLink} to="/admin"
|
||||
activeClassName={styles.active}>{lang.t('configure.moderate')}</IndexLink>
|
||||
<Link className={styles.navLink} to="/admin/community"
|
||||
activeClassName={styles.active}>{lang.t('configure.community')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure"
|
||||
activeClassName={styles.active}>{lang.t('configure.configure')}</Link>
|
||||
{
|
||||
!restricted ?
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink className={styles.navLink} to="/admin"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.moderate')}
|
||||
</IndexLink>
|
||||
<Link className={styles.navLink} to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.community')}
|
||||
</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.configure')}
|
||||
</Link>
|
||||
<Link className={styles.navLink} to="/admin/streams"
|
||||
activeClassName={styles.active}>{lang.t('configure.streams')}</Link>
|
||||
</Navigation>
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={handleLogout}>Sign Out</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.streams')}
|
||||
</Link>
|
||||
</Navigation>
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={handleLogout}>Sign Out</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</Header>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export const NEXT_STEP = 'NEXT_STEP';
|
||||
export const GO_TO_STEP = 'GO_TO_STEP';
|
||||
export const PREVIOUS_STEP = 'PREVIOUS_STEP';
|
||||
export const ADD_ERROR = 'ADD_ERROR';
|
||||
export const HAS_ERROR = 'HAS_ERROR';
|
||||
export const SHOW_ERRORS = 'SHOW_ERRORS';
|
||||
export const CLEAR_ERRORS = 'CLEAR_ERRORS';
|
||||
export const INSTALL_REQUEST = 'INSTALL_REQUEST';
|
||||
export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
|
||||
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
|
||||
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
|
||||
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
|
||||
|
||||
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
|
||||
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
|
||||
export const CHECK_INSTALL_FAILURE = 'CHECK_INSTALL_FAILURE';
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
updateSettings,
|
||||
saveSettingsToServer,
|
||||
updateWordlist,
|
||||
updateDomainlist
|
||||
} from '../../actions/settings';
|
||||
|
||||
import {Button, List, Item} from 'coral-ui';
|
||||
@@ -14,6 +15,7 @@ import translations from '../../translations.json';
|
||||
import EmbedLink from './EmbedLink';
|
||||
import CommentSettings from './CommentSettings';
|
||||
import Wordlist from './Wordlist';
|
||||
import Domainlist from './Domainlist';
|
||||
import has from 'lodash/has';
|
||||
|
||||
class Configure extends Component {
|
||||
@@ -47,6 +49,11 @@ class Configure extends Component {
|
||||
this.props.dispatch(updateWordlist(listName, list));
|
||||
}
|
||||
|
||||
onChangeDomainlist = (listName, list) => {
|
||||
this.setState({changed: true});
|
||||
this.props.dispatch(updateDomainlist(listName, list));
|
||||
}
|
||||
|
||||
onSettingUpdate = (setting) => {
|
||||
this.setState({changed: true});
|
||||
this.props.dispatch(updateSettings(setting));
|
||||
@@ -73,7 +80,14 @@ class Configure extends Component {
|
||||
errors={this.state.errors}
|
||||
settingsError={this.onSettingError}/>;
|
||||
case 'embed':
|
||||
return <EmbedLink title={pageTitle} />;
|
||||
return has(this, 'props.settings.domains.whitelist')
|
||||
? <div>
|
||||
<Domainlist
|
||||
domains={this.props.settings.domains.whitelist}
|
||||
onChangeDomainlist={this.onChangeDomainlist}/>
|
||||
<EmbedLink title={pageTitle} />
|
||||
</div>
|
||||
: <EmbedLink title={pageTitle} />;
|
||||
case 'wordlist':
|
||||
return has(this, 'props.settings.wordlist')
|
||||
? <Wordlist
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
import styles from './Configure.css';
|
||||
import {Card} from 'coral-ui';
|
||||
|
||||
const Domainlist = ({domains, onChangeDomainlist}) => (
|
||||
<div>
|
||||
<h3>{lang.t('configure.domain-list-title')}</h3>
|
||||
<Card id={styles.domainlist}>
|
||||
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => onChangeDomainlist('whitelist', tags)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Domainlist;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import styles from './style.css';
|
||||
import {Wizard, WizardNav} from 'coral-ui';
|
||||
import {Layout} from '../../components/ui/Layout';
|
||||
|
||||
import {
|
||||
nextStep,
|
||||
previousStep,
|
||||
goToStep,
|
||||
updateUserFormData,
|
||||
updateSettingsFormData,
|
||||
submitSettings,
|
||||
submitUser,
|
||||
checkInstall
|
||||
} from '../../actions/install';
|
||||
|
||||
import InitialStep from './components/Steps/InitialStep';
|
||||
import AddOrganizationName from './components/Steps/AddOrganizationName';
|
||||
import CreateYourAccount from './components/Steps/CreateYourAccount';
|
||||
import FinalStep from './components/Steps/FinalStep';
|
||||
|
||||
class InstallContainer extends Component {
|
||||
componentDidMount() {
|
||||
const {checkInstall} = this.props;
|
||||
checkInstall(() => {
|
||||
this.context.router.push('/admin');
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {install} = this.props;
|
||||
|
||||
return (
|
||||
<Layout restricted={true}>
|
||||
<div className={styles.Install}>
|
||||
{
|
||||
!install.alreadyInstalled ? (
|
||||
<div>
|
||||
<h2>Welcome to the Coral Project</h2>
|
||||
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
|
||||
<Wizard currentStep={install.step} {...this.props}>
|
||||
<InitialStep/>
|
||||
<AddOrganizationName/>
|
||||
<CreateYourAccount/>
|
||||
<FinalStep/>
|
||||
</Wizard>
|
||||
</div>
|
||||
) : (
|
||||
<div>Talk is already installed</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InstallContainer.contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
install: state.install.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
checkInstall: next => dispatch(checkInstall(next)),
|
||||
nextStep: () => dispatch(nextStep()),
|
||||
previousStep: () => dispatch(previousStep()),
|
||||
goToStep: step => dispatch(goToStep(step)),
|
||||
handleSettingsChange: e => {
|
||||
const {name, value} = e.currentTarget;
|
||||
dispatch(updateSettingsFormData(name, value));
|
||||
},
|
||||
handleUserChange: e => {
|
||||
const {name, value} = e.currentTarget;
|
||||
dispatch(updateUserFormData(name, value));
|
||||
},
|
||||
handleSettingsSubmit: e => {
|
||||
e.preventDefault();
|
||||
dispatch(submitSettings());
|
||||
},
|
||||
handleUserSubmit: e => {
|
||||
e.preventDefault();
|
||||
dispatch(submitUser());
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InstallContainer);
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {FormField, Button} from 'coral-ui';
|
||||
|
||||
const AddOrganizationName = props => {
|
||||
const {handleSettingsChange, handleSettingsSubmit, install} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<p>
|
||||
Please tell us the name of your organization. This will appear in emails when
|
||||
inviting new team members
|
||||
</p>
|
||||
<div className={styles.form}>
|
||||
<form onSubmit={handleSettingsSubmit}>
|
||||
<FormField
|
||||
className={styles.FormField}
|
||||
id="organizationName"
|
||||
type="text"
|
||||
label='Organization name'
|
||||
onChange={handleSettingsChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.organizationName} />
|
||||
<Button type="submit" cStyle='black' full>Save</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddOrganizationName;
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {FormField, Button, Spinner} from 'coral-ui';
|
||||
|
||||
const InitialStep = props => {
|
||||
const {handleUserChange, handleUserSubmit, install} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<div className={styles.form}>
|
||||
<form onSubmit={handleUserSubmit}>
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
id="email"
|
||||
type="email"
|
||||
label='Email address'
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.email}
|
||||
noValidate
|
||||
/>
|
||||
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
id="displayName"
|
||||
type="text"
|
||||
label='Username'
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.displayName}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
id="password"
|
||||
type="password"
|
||||
label='Password'
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.password}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
label='Confirm Password'
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.confirmPassword}
|
||||
/>
|
||||
|
||||
{
|
||||
!props.install.isLoading ?
|
||||
<Button cStyle='black' type="submit" full>Save</Button>
|
||||
:
|
||||
<Spinner />
|
||||
}
|
||||
{props.install.installRequest === 'FAILURE' && <div className={styles.error}>Error: {props.install.installRequestError}</div>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitialStep;
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
const InitialStep = () => {
|
||||
return (
|
||||
<div className={`${styles.step} ${styles.finalStep}`}>
|
||||
<p>
|
||||
Thanks for installing Talk! We sent an email to verify your email
|
||||
address. While you finish setting the account, you can start engaging
|
||||
with your readers now.
|
||||
</p>
|
||||
<Button raised><Link to='/admin'>Launch Talk</Link></Button>
|
||||
<Button cStyle='black' raised><a href="http://coralproject.net">Close this Installer</a></Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitialStep;
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {Button} from 'coral-ui';
|
||||
|
||||
const InitialStep = props => {
|
||||
const {nextStep} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<p>
|
||||
The remainder of the Talk installation will take about ten minutes.
|
||||
Once you complete the following two steps, you will have a free
|
||||
installation and provision of Mongo and Redis.
|
||||
</p>
|
||||
<Button cStyle='green' onClick={nextStep} raised>Get Started</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitialStep;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {Button, Select, Option, FormField} from 'coral-ui';
|
||||
|
||||
const InviteTeamMembers = props => {
|
||||
const {nextStep} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<h3>Invite Team Members </h3>
|
||||
<p>
|
||||
Once registered, new team members will receive an email to Create
|
||||
their password.
|
||||
</p>
|
||||
<div className={styles.form}>
|
||||
<form>
|
||||
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
id="email"
|
||||
type="email"
|
||||
label='Email address' required/>
|
||||
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
id="username"
|
||||
type="text"
|
||||
label='Username' required/>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label htmlFor='role'>Assing a role</label>
|
||||
<Select id='role' label='Select Role'>
|
||||
<Option>Admin</Option>
|
||||
<Option>Moderator</Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button cStyle='black' onClick={nextStep} icon='arrow_forward' full>Invite team member</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteTeamMembers;
|
||||
@@ -0,0 +1,77 @@
|
||||
.step {
|
||||
padding: 20px 0;
|
||||
|
||||
h3 {
|
||||
max-width: 450px;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
font-size: 1.4em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 450px;
|
||||
margin: 0 auto 30px;
|
||||
font-size: 1.1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
> button {
|
||||
min-width: 145px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
max-width: 300px;
|
||||
margin: 30px auto;
|
||||
text-align: left;
|
||||
|
||||
form > button {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.formField {
|
||||
text-align: left;
|
||||
|
||||
label {
|
||||
text-align: left;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
font-weight: 400;
|
||||
font-size: 1.08em;
|
||||
}
|
||||
|
||||
> input {
|
||||
border: solid 1px rgba(0, 0, 0, 0.23);
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s ease;
|
||||
font-size: 1em;
|
||||
|
||||
&:focus {
|
||||
border-color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.finalStep {
|
||||
button {
|
||||
width: 225px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.Install {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding: 50px 0;
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {render} from 'react-dom';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
|
||||
import {client} from './services/client';
|
||||
import store from './services/store';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
// Render the application into the DOM
|
||||
ReactDOM.render(<App />, document.querySelector('#root'));
|
||||
render(
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
, document.querySelector('#root')
|
||||
);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import {combineReducers} from 'redux';
|
||||
import auth from 'reducers/auth';
|
||||
import users from 'reducers/users';
|
||||
import assets from 'reducers/assets';
|
||||
import actions from 'reducers/actions';
|
||||
import install from 'reducers/install';
|
||||
import comments from 'reducers/comments';
|
||||
import settings from 'reducers/settings';
|
||||
import community from 'reducers/community';
|
||||
import users from 'reducers/users';
|
||||
import auth from 'reducers/auth';
|
||||
import actions from 'reducers/actions';
|
||||
import assets from 'reducers/assets';
|
||||
|
||||
// Combine all reducers into a main one
|
||||
export default combineReducers({
|
||||
export default {
|
||||
settings,
|
||||
comments,
|
||||
community,
|
||||
auth,
|
||||
actions,
|
||||
assets,
|
||||
users
|
||||
});
|
||||
users,
|
||||
install
|
||||
};
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import {Map} from 'immutable';
|
||||
|
||||
import * as actions from '../constants/install';
|
||||
|
||||
const initialState = Map({
|
||||
isLoading: false,
|
||||
data: Map({
|
||||
settings: Map({
|
||||
organizationName: ''
|
||||
}),
|
||||
user: Map({
|
||||
displayName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
}),
|
||||
errors: Map({
|
||||
organizationName: '',
|
||||
displayName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
}),
|
||||
showErrors: false,
|
||||
hasError: false,
|
||||
error: null,
|
||||
step: 0,
|
||||
navItems: [{
|
||||
text: '1. Add Organization Name',
|
||||
step: 1
|
||||
},
|
||||
{
|
||||
text: '2. Create your account',
|
||||
step: 2
|
||||
}],
|
||||
installRequest: null,
|
||||
installRequestError: null,
|
||||
alreadyInstalled: false
|
||||
});
|
||||
|
||||
export default function install (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.NEXT_STEP:
|
||||
return state
|
||||
.set('step', state.get('step') + 1);
|
||||
case actions.PREVIOUS_STEP:
|
||||
return state
|
||||
.set('step', state.get('step') - 1);
|
||||
case actions.GO_TO_STEP:
|
||||
return state
|
||||
.set('step', action.step);
|
||||
case actions.UPDATE_FORMDATA_SETTINGS:
|
||||
return state
|
||||
.setIn(['data', 'settings', action.name], action.value);
|
||||
case actions.UPDATE_FORMDATA_USER:
|
||||
return state
|
||||
.setIn(['data', 'user', action.name], action.value);
|
||||
case actions.HAS_ERROR:
|
||||
return state
|
||||
.merge({
|
||||
hasError: true,
|
||||
showErrors: true
|
||||
});
|
||||
case actions.ADD_ERROR:
|
||||
return state
|
||||
.setIn(['errors', action.name], action.error);
|
||||
case actions.CLEAR_ERRORS:
|
||||
return state
|
||||
.set('errors', Map());
|
||||
case actions.INSTALL_REQUEST:
|
||||
return state
|
||||
.set('isLoading', true);
|
||||
case actions.INSTALL_SUCCESS:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('installRequest', 'SUCCESS');
|
||||
case actions.INSTALL_FAILURE:
|
||||
return state
|
||||
.merge({
|
||||
isLoading: false,
|
||||
installRequest: 'FAILURE',
|
||||
installRequestError: action.error
|
||||
});
|
||||
case actions.CHECK_INSTALL_SUCCESS:
|
||||
return state
|
||||
.set('alreadyInstalled', action.installed);
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ const initialState = Map({
|
||||
wordlist: Map({
|
||||
banned: List(),
|
||||
suspect: List()
|
||||
}),
|
||||
domains: Map({
|
||||
whitelist: List()
|
||||
})
|
||||
}),
|
||||
saveSettingsError: null,
|
||||
@@ -24,6 +27,7 @@ export default (state = initialState, action) => {
|
||||
case types.SAVE_SETTINGS_SUCCESS: return saveComplete(state, action);
|
||||
case types.SAVE_SETTINGS_FAILED: return settingsSaveFailed(state, action);
|
||||
case types.WORDLIST_UPDATED: return updateWordlist(state, action);
|
||||
case types.DOMAINLIST_UPDATED: return updateDomainlist(state, action);
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
@@ -40,6 +44,10 @@ const updateWordlist = (state, action) => {
|
||||
return state.setIn(['settings', 'wordlist', action.listName], action.list);
|
||||
};
|
||||
|
||||
const updateDomainlist = (state, action) => {
|
||||
return state.setIn(['settings', 'domains', action.listName], action.list);
|
||||
};
|
||||
|
||||
const saveComplete = (state, action) => {
|
||||
const s = state.set('fetchingSettings', false).set('saveSettingsError', null);
|
||||
const settings = s.get('settings').merge(action.settings);
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
|
||||
import {createStore, applyMiddleware} from 'redux';
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from 'reducers';
|
||||
import mainReducer from '../reducers';
|
||||
import {client} from './client';
|
||||
|
||||
/**
|
||||
* Create the store by merging the app reducers with
|
||||
* the talk adapter. The talk adapter is the wire between
|
||||
* this client and the coral backend. The idea is we can
|
||||
* write different adapters for other platforms if we want
|
||||
*/
|
||||
const middlewares = [
|
||||
applyMiddleware(client.middleware()),
|
||||
applyMiddleware(thunk)
|
||||
];
|
||||
|
||||
if (window.devToolsExtension) {
|
||||
|
||||
// we can't have the last argument of compose() be undefined
|
||||
middlewares.push(window.devToolsExtension());
|
||||
}
|
||||
|
||||
export default createStore(
|
||||
mainReducer,
|
||||
window.devToolsExtension && window.devToolsExtension(),
|
||||
applyMiddleware(thunk)
|
||||
combineReducers({
|
||||
...mainReducer,
|
||||
apollo: client.reducer()
|
||||
}),
|
||||
{},
|
||||
compose(...middlewares)
|
||||
);
|
||||
|
||||
@@ -79,7 +79,9 @@
|
||||
"comment-count-header": "Limit Comment Length",
|
||||
"comment-count-text-pre": "Comments will be limited to ",
|
||||
"comment-count-text-post": " characters.",
|
||||
"comment-count-error": "Please enter a valid number."
|
||||
"comment-count-error": "Please enter a valid number.",
|
||||
"domain-list-title": "Domain Whitelist",
|
||||
"domain-list-text": "Some instructions on how to type the urls."
|
||||
},
|
||||
"bandialog": {
|
||||
"ban_user": "Ban User?",
|
||||
@@ -187,7 +189,12 @@
|
||||
"comment-count-header": "Limitar el largo del comentario",
|
||||
"comment-count-text-pre": "El largo de comentarios será ",
|
||||
"comment-count-text-post": " caracteres",
|
||||
"comment-count-error": "Por favor escribe un número válido."
|
||||
"comment-count-error": "Por favor escribe un número válido.",
|
||||
"domain-list-title": "Lista de Dominios Permitidos",
|
||||
"domain-list-text": "Instrucciones de como ingresar las URLs."
|
||||
},
|
||||
"embedlink": {
|
||||
"copy": "Copiar"
|
||||
},
|
||||
"bandialog": {
|
||||
"ban_user": "Quieres suspender el Usuario?",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {GraphQLDocs} from 'graphql-docs';
|
||||
|
||||
import fetcher from './services/fetcher';
|
||||
|
||||
// Render the application into the DOM
|
||||
ReactDOM.render(<GraphQLDocs fetcher={fetcher} />, document.querySelector('#root'));
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function fetcher(query) {
|
||||
return fetch(`${window.location.host}/api/v1/graph/ql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({query}),
|
||||
}).then((res) => res.json());
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import React, {PropTypes} from 'react';
|
||||
import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton';
|
||||
|
||||
import AuthorName from 'coral-plugin-author-name/AuthorName';
|
||||
|
||||
import TagLabel from 'coral-plugin-tag-label/TagLabel';
|
||||
import Content from 'coral-plugin-commentcontent/CommentContent';
|
||||
import PubDate from 'coral-plugin-pubdate/PubDate';
|
||||
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
|
||||
@@ -18,7 +20,9 @@ import LikeButton from 'coral-plugin-likes/LikeButton';
|
||||
|
||||
import styles from './Comment.css';
|
||||
|
||||
const getAction = (type, comment) => comment.actions.filter((a) => a.type === type)[0];
|
||||
const getActionSummary = (type, comment) => comment.action_summaries
|
||||
.filter((a) => a.__typename === type)[0];
|
||||
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ;
|
||||
|
||||
class Comment extends React.Component {
|
||||
|
||||
@@ -35,7 +39,8 @@ class Comment extends React.Component {
|
||||
setActiveReplyBox: PropTypes.func.isRequired,
|
||||
refetch: PropTypes.func.isRequired,
|
||||
showSignInDialog: PropTypes.func.isRequired,
|
||||
postAction: PropTypes.func.isRequired,
|
||||
postFlag: PropTypes.func.isRequired,
|
||||
postLike: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
@@ -51,15 +56,19 @@ class Comment extends React.Component {
|
||||
}),
|
||||
comment: PropTypes.shape({
|
||||
depth: PropTypes.number,
|
||||
actions: PropTypes.array.isRequired,
|
||||
action_summaries: PropTypes.array.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string
|
||||
})
|
||||
),
|
||||
replies: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
body: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired
|
||||
})
|
||||
),
|
||||
})),
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
@@ -78,14 +87,15 @@ class Comment extends React.Component {
|
||||
refetch,
|
||||
addNotification,
|
||||
showSignInDialog,
|
||||
postAction,
|
||||
postLike,
|
||||
postFlag,
|
||||
setActiveReplyBox,
|
||||
activeReplyBox,
|
||||
deleteAction
|
||||
} = this.props;
|
||||
|
||||
const like = getAction('LIKE', comment);
|
||||
const flag = getAction('FLAG', comment);
|
||||
const like = getActionSummary('LikeActionSummary', comment);
|
||||
const flag = getActionSummary('FlagActionSummary', comment);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -95,13 +105,16 @@ class Comment extends React.Component {
|
||||
<hr aria-hidden={true} />
|
||||
<AuthorName
|
||||
author={comment.user}/>
|
||||
{ isStaff(comment.tags)
|
||||
? <TagLabel isStaff={true}/>
|
||||
: null }
|
||||
<PubDate created_at={comment.created_at} />
|
||||
<Content body={comment.body} />
|
||||
<div className="commentActionsLeft">
|
||||
<LikeButton
|
||||
like={like}
|
||||
id={comment.id}
|
||||
postAction={postAction}
|
||||
postLike={postLike}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
@@ -117,7 +130,7 @@ class Comment extends React.Component {
|
||||
flag={flag}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postAction={postAction}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
@@ -150,7 +163,8 @@ class Comment extends React.Component {
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
currentUser={currentUser}
|
||||
postAction={postAction}
|
||||
postLike={postLike}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
reactKey={reply.id}
|
||||
@@ -158,7 +172,6 @@ class Comment extends React.Component {
|
||||
comment={reply} />;
|
||||
})
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const {addNotification, clearNotification} = notificationActions;
|
||||
const {fetchAssetSuccess} = assetActions;
|
||||
|
||||
import {queryStream} from 'coral-framework/graphql/queries';
|
||||
import {postComment, postAction, deleteAction} from 'coral-framework/graphql/mutations';
|
||||
import {postComment, postFlag, postLike, deleteAction} from 'coral-framework/graphql/mutations';
|
||||
import {editName} from 'coral-framework/actions/user';
|
||||
import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework';
|
||||
|
||||
@@ -105,7 +105,7 @@ class Embed extends Component {
|
||||
minHeight: document.body.scrollHeight + 200
|
||||
} : {};
|
||||
|
||||
if (loading) {
|
||||
if (loading || !asset) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,8 @@ class Embed extends Component {
|
||||
postItem={this.props.postItem}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postAction={this.props.postAction}
|
||||
postLike={this.props.postLike}
|
||||
postFlag={this.props.postFlag}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
comments={asset.comments} />
|
||||
@@ -227,7 +228,8 @@ const mapDispatchToProps = dispatch => ({
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
postComment,
|
||||
postAction,
|
||||
postFlag,
|
||||
postLike,
|
||||
deleteAction,
|
||||
queryStream
|
||||
)(Embed);
|
||||
|
||||
@@ -37,7 +37,8 @@ class Stream extends React.Component {
|
||||
asset,
|
||||
postItem,
|
||||
addNotification,
|
||||
postAction,
|
||||
postFlag,
|
||||
postLike,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
refetch
|
||||
@@ -56,7 +57,8 @@ class Stream extends React.Component {
|
||||
postItem={postItem}
|
||||
asset={asset}
|
||||
currentUser={currentUser}
|
||||
postAction={postAction}
|
||||
postLike={postLike}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={comment.id}
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
|
||||
import {client} from 'coral-framework/client';
|
||||
import store from 'coral-framework/store';
|
||||
import {client} from 'coral-framework/services/client';
|
||||
import store from 'coral-framework/services/store';
|
||||
|
||||
import Embed from './Embed';
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fragment actionSummaryView on ActionSummary {
|
||||
__typename
|
||||
count
|
||||
current_user {
|
||||
id
|
||||
created_at
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
#import "../fragments/actionSummaryView.graphql"
|
||||
|
||||
fragment commentView on Comment {
|
||||
id
|
||||
body
|
||||
created_at
|
||||
status
|
||||
tags {
|
||||
name
|
||||
}
|
||||
user {
|
||||
id
|
||||
name: displayName
|
||||
}
|
||||
actions {
|
||||
type: action_type
|
||||
count
|
||||
current: current_user {
|
||||
id
|
||||
created_at
|
||||
}
|
||||
action_summaries {
|
||||
...actionSummaryView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
mutation deleteAction ($id: ID!) {
|
||||
deleteAction(id:$id)
|
||||
deleteAction(id:$id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {graphql} from 'react-apollo';
|
||||
import POST_COMMENT from './postComment.graphql';
|
||||
import POST_ACTION from './postAction.graphql';
|
||||
import POST_FLAG from './postFlag.graphql';
|
||||
import POST_LIKE from './postLike.graphql';
|
||||
import DELETE_ACTION from './deleteAction.graphql';
|
||||
|
||||
import commentView from '../fragments/commentView.graphql';
|
||||
@@ -21,12 +22,23 @@ export const postComment = graphql(POST_COMMENT, {
|
||||
}}),
|
||||
});
|
||||
|
||||
export const postAction = graphql(POST_ACTION, {
|
||||
export const postLike = graphql(POST_LIKE, {
|
||||
props: ({mutate}) => ({
|
||||
postAction: (action) => {
|
||||
postLike: (like) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
action
|
||||
like
|
||||
}
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
export const postFlag = graphql(POST_FLAG, {
|
||||
props: ({mutate}) => ({
|
||||
postFlag: (flag) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
flag
|
||||
}
|
||||
});
|
||||
}}),
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
mutation CreateAction ($action: CreateActionInput!) {
|
||||
createAction(action:$action) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
mutation CreateComment ($asset_id: ID!, $parent_id: ID, $body: String!) {
|
||||
createComment(asset_id:$asset_id, parent_id:$parent_id, body:$body) {
|
||||
...commentView
|
||||
comment {
|
||||
...commentView
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
mutation CreateFlag($flag: CreateFlagInput!) {
|
||||
createFlag(flag:$flag) {
|
||||
flag {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
mutation CreateLike ($like: CreateLikeInput!) {
|
||||
createLike(like:$like) {
|
||||
like {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ function getQueryVariable(variable) {
|
||||
}
|
||||
|
||||
// If no query is included, return a default string for development
|
||||
return 'http://dev.default.stream';
|
||||
return 'http://localhost/default/stream';
|
||||
}
|
||||
|
||||
export const queryStream = graphql(STREAM_QUERY, {
|
||||
|
||||
@@ -6,5 +6,6 @@ export default {
|
||||
email: lang.t('error.email'),
|
||||
password: lang.t('error.password'),
|
||||
displayName: lang.t('error.displayName'),
|
||||
confirmPassword: lang.t('error.confirmPassword')
|
||||
confirmPassword: lang.t('error.confirmPassword'),
|
||||
organizationName: lang.t('error.organizationName'),
|
||||
};
|
||||
|
||||
@@ -2,5 +2,6 @@ export default {
|
||||
email: email => (/^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
|
||||
password: pass => (/^(?=.{8,}).*$/.test(pass)),
|
||||
confirmPassword: () => true,
|
||||
displayName: displayName => (/^[a-zA-Z0-9_]+$/.test(displayName))
|
||||
displayName: displayName => (/^[a-zA-Z0-9_]+$/.test(displayName)),
|
||||
organizationName: org => (/^[a-zA-Z0-9_ ]+$/).test(org)
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import store from './store';
|
||||
import pym from './PymConnection';
|
||||
import store from './services/store';
|
||||
import pym from './services/PymConnection';
|
||||
import I18n from './modules/i18n/i18n';
|
||||
import * as authActions from './actions/auth';
|
||||
import * as assetActions from './actions/asset';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import Pym from '../../node_modules/pym.js';
|
||||
import Pym from '../../../node_modules/pym.js';
|
||||
|
||||
const pym = new Pym.Child({polling: 100});
|
||||
export default pym;
|
||||
@@ -0,0 +1,14 @@
|
||||
import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import getNetworkInterface from './transport';
|
||||
|
||||
export const client = new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
return result.__typename + result.id; // eslint-disable-line no-underscore-dangle
|
||||
}
|
||||
return null;
|
||||
},
|
||||
networkInterface: getNetworkInterface()
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from './reducers';
|
||||
import mainReducer from '../reducers';
|
||||
import {client} from './client';
|
||||
|
||||
const middlewares = [
|
||||
@@ -0,0 +1,16 @@
|
||||
import {print} from 'graphql-tag/printer';
|
||||
|
||||
// quick way to add the subscribe and unsubscribe functions to the network interface
|
||||
const addGraphQLSubscriptions = (networkInterface, wsClient) => {
|
||||
return Object.assign(networkInterface, {
|
||||
subscribe: (request, handler) => wsClient.subscribe({
|
||||
query: print(request.query),
|
||||
variables: request.variables,
|
||||
}, handler),
|
||||
unsubscribe: (id) => {
|
||||
wsClient.unsubscribe(id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default addGraphQLSubscriptions;
|
||||
@@ -0,0 +1,11 @@
|
||||
import {createNetworkInterface} from 'apollo-client';
|
||||
|
||||
export default function getNetworkInterface(apiUrl = '/api/v1/graph/ql', headers = {}) {
|
||||
return new createNetworkInterface({
|
||||
uri: apiUrl,
|
||||
opts: {
|
||||
credentials: 'same-origin',
|
||||
headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
"password": "Password must be at least 8 characters",
|
||||
"displayName": "Display names can contain letters, numbers and _ only",
|
||||
"confirmPassword": "Passwords don't match. Please, check again",
|
||||
"organizationName": "Organization name must only contain letters or numbers.",
|
||||
"emailPasswordError": "Email and/or password combination incorrect.",
|
||||
"EMAIL_REQUIRED": "An email address is required",
|
||||
"PASSWORD_REQUIRED": "Must input a password",
|
||||
@@ -39,6 +40,7 @@
|
||||
"email": "No es un email válido",
|
||||
"password": "La contraseña debe tener por lo menos 8 caracteres",
|
||||
"displayName": "Los nombres pueden contener letras, números y _",
|
||||
"organizationName": "El nombre de la organización debe contener letras y/o números.",
|
||||
"confirmPassword": "Las contraseñas no coinciden",
|
||||
"emailPasswordError": "Email y/o contraseña incorrecta.",
|
||||
"EMAIL_REQUIRED": "Se requiere una dirección de correo electrónico",
|
||||
|
||||
@@ -8,9 +8,6 @@ const name = 'coral-plugin-commentbox';
|
||||
class CommentBox extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
// updateItem: PropTypes.func,
|
||||
// comments: PropTypes.array,
|
||||
commentPostedHandler: PropTypes.func,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
cancelButtonClicked: PropTypes.func,
|
||||
@@ -29,10 +26,6 @@ class CommentBox extends Component {
|
||||
|
||||
postComment = () => {
|
||||
const {
|
||||
|
||||
// child_id,
|
||||
// updateItem,
|
||||
// appendItemArray,
|
||||
commentPostedHandler,
|
||||
postItem,
|
||||
assetId,
|
||||
@@ -48,28 +41,13 @@ class CommentBox extends Component {
|
||||
parent_id: parentId
|
||||
};
|
||||
|
||||
// let related;
|
||||
// let parent_type;
|
||||
// if (parent_id) {
|
||||
// comment.parent_id = parent_id;
|
||||
// related = 'children';
|
||||
// parent_type = 'comments';
|
||||
// } else {
|
||||
// related = 'comments';
|
||||
// parent_type = 'assets';
|
||||
// }
|
||||
// if (child_id || parent_id) {
|
||||
// updateItem(child_id || parent_id, 'showReply', false, 'comments');
|
||||
// }
|
||||
|
||||
if (this.props.charCount && this.state.body.length > this.props.charCount) {
|
||||
return;
|
||||
}
|
||||
postItem(comment, 'comments')
|
||||
.then(({data}) => {
|
||||
const postedComment = data.createComment;
|
||||
const postedComment = data.createComment.comment;
|
||||
|
||||
// const commentId = postedComment.id;
|
||||
if (postedComment.status === 'REJECTED') {
|
||||
addNotification('error', lang.t('comment-post-banned-word'));
|
||||
} else if (postedComment.status === 'PREMOD') {
|
||||
|
||||
@@ -12,7 +12,7 @@ class FlagButton extends Component {
|
||||
showMenu: false,
|
||||
itemType: '',
|
||||
reason: '',
|
||||
note: '',
|
||||
message: '',
|
||||
step: 0,
|
||||
posted: false,
|
||||
localPost: null,
|
||||
@@ -23,7 +23,7 @@ class FlagButton extends Component {
|
||||
onReportClick = () => {
|
||||
const {currentUser, flag, deleteAction} = this.props;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const flagged = (flag && flag.current && !localDelete) || localPost;
|
||||
const flagged = (flag && flag.current_user && !localDelete) || localPost;
|
||||
if (!currentUser) {
|
||||
const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
@@ -31,15 +31,15 @@ class FlagButton extends Component {
|
||||
}
|
||||
if (flagged) {
|
||||
this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true});
|
||||
deleteAction(localPost || flag.current.id);
|
||||
deleteAction(localPost || flag.current_user.id);
|
||||
} else {
|
||||
this.setState({showMenu: !this.state.showMenu});
|
||||
}
|
||||
}
|
||||
|
||||
onPopupContinue = () => {
|
||||
const {postAction, id, author_id} = this.props;
|
||||
const {itemType, reason, step, posted} = this.state;
|
||||
const {postFlag, id, author_id} = this.props;
|
||||
const {itemType, reason, step, posted, message} = this.state;
|
||||
|
||||
// Proceed to the next step or close the menu if we've reached the end
|
||||
if (step + 1 >= this.props.getPopupMenu.length) {
|
||||
@@ -67,13 +67,14 @@ class FlagButton extends Component {
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({localPost: 'temp'});
|
||||
}
|
||||
postAction({
|
||||
postFlag({
|
||||
item_id,
|
||||
item_type: itemType,
|
||||
action_type: 'FLAG'
|
||||
reason,
|
||||
message
|
||||
}).then(({data}) => {
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({localPost: data.createAction.id});
|
||||
this.setState({localPost: data.createFlag.flag.id});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -99,7 +100,7 @@ class FlagButton extends Component {
|
||||
}
|
||||
|
||||
onNoteTextChange = (e) => {
|
||||
this.setState({note: e.target.value});
|
||||
this.setState({message: e.target.value});
|
||||
}
|
||||
|
||||
handleClickOutside () {
|
||||
@@ -109,7 +110,7 @@ class FlagButton extends Component {
|
||||
render () {
|
||||
const {flag, getPopupMenu} = this.props;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const flagged = (flag && flag.current && !localDelete) || localPost;
|
||||
const flagged = (flag && flag.current_user && !localDelete) || localPost;
|
||||
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
@@ -150,15 +151,15 @@ class FlagButton extends Component {
|
||||
}
|
||||
{
|
||||
this.state.reason && <div>
|
||||
<label htmlFor={'note'} className={`${name}-popup-radio-label`}>
|
||||
<label htmlFor={'message'} className={`${name}-popup-radio-label`}>
|
||||
{lang.t('flag-reason')}
|
||||
</label><br/>
|
||||
<textarea
|
||||
className={`${name}-reason-text`}
|
||||
id="note"
|
||||
id="message"
|
||||
rows={4}
|
||||
onChange={this.onNoteTextChange}
|
||||
value={this.state.note}/>
|
||||
value={this.state.message}/>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
|
||||
@@ -12,7 +12,7 @@ class LikeButton extends Component {
|
||||
count: PropTypes.number
|
||||
}),
|
||||
id: PropTypes.string,
|
||||
postAction: PropTypes.func.isRequired,
|
||||
postLike: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
showSignInDialog: PropTypes.func.isRequired,
|
||||
currentUser: PropTypes.shape({
|
||||
@@ -26,9 +26,9 @@ class LikeButton extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {like, id, postAction, deleteAction, showSignInDialog, currentUser} = this.props;
|
||||
const {like, id, postLike, deleteAction, showSignInDialog, currentUser} = this.props;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const liked = (like && like.current && !localDelete) || localPost;
|
||||
const liked = (like && like.current_user && !localDelete) || localPost;
|
||||
let count = like ? like.count : 0;
|
||||
if (localPost) {count += 1;}
|
||||
if (localDelete) {count -= 1;}
|
||||
@@ -42,28 +42,23 @@ class LikeButton extends Component {
|
||||
if (currentUser.banned) {
|
||||
return;
|
||||
}
|
||||
if (!liked) {
|
||||
this.setState({localPost: 'temp', localDelete: false});
|
||||
postAction({
|
||||
if (!liked) { // this comment has not yet been liked by this user.
|
||||
this.setState({localPost: 'temp'});
|
||||
postLike({
|
||||
item_id: id,
|
||||
item_type: 'COMMENTS',
|
||||
action_type: 'LIKE'
|
||||
item_type: 'COMMENTS'
|
||||
}).then(({data}) => {
|
||||
this.setState({localPost: data.createAction.id});
|
||||
this.setState({localPost: data.createLike.like.id});
|
||||
});
|
||||
} else {
|
||||
this.setState((prev) => prev.localPost ? {...prev, localPost: null} : {...prev, localDelete: true});
|
||||
deleteAction(localPost || like.current.id);
|
||||
deleteAction(localPost || like.current_user.id);
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
|
||||
{
|
||||
liked
|
||||
? <span className={`${name}-button-text`}>{lang.t('liked')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('like')}</span>
|
||||
}
|
||||
<span className={`${name}-button-text`}>{lang.t(liked ? 'liked' : 'like')}</span>
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>thumb_up</i>
|
||||
<span className={`${name}-like-count`}>{count > 0 && count}</span>
|
||||
|
||||
@@ -57,6 +57,9 @@ const StreamQuery = gql`fragment commentView on Comment {
|
||||
user {
|
||||
name: displayName
|
||||
}
|
||||
tags {
|
||||
name
|
||||
}
|
||||
actions {
|
||||
type: action_type
|
||||
count
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './styles.css';
|
||||
|
||||
const TagLabel = ({isStaff}) => <div className={`${styles.staff}`}>
|
||||
{isStaff ? 'Staff' : ''}
|
||||
</div>;
|
||||
|
||||
export default TagLabel;
|
||||
@@ -0,0 +1,7 @@
|
||||
.staff {
|
||||
background-color: #4C1066;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
margin: 10px 10px;
|
||||
padding: 5px 5px;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
import {myCommentHistory} from 'coral-framework/graphql/queries';
|
||||
|
||||
import {link} from 'coral-framework/PymConnection';
|
||||
import {link} from 'coral-framework/services/PymConnection';
|
||||
import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #E0E0E0;
|
||||
color: #4f5c67;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.type--black {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.Option {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import {Option as OptionMDL} from 'react-mdl-selectfield';
|
||||
import styles from './Option.css';
|
||||
|
||||
const Option = props => {
|
||||
const {children, ...attrs} = props;
|
||||
return (
|
||||
<OptionMDL className={styles.Option} {...attrs}>
|
||||
{children}
|
||||
</OptionMDL>
|
||||
);
|
||||
};
|
||||
|
||||
export default Option;
|
||||
@@ -0,0 +1,33 @@
|
||||
.Select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: #2c2c2c;
|
||||
padding: 10px 15px;
|
||||
box-sizing: border-box;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
|
||||
> div {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.7px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import {SelectField} from 'react-mdl-selectfield';
|
||||
import styles from './Select.css';
|
||||
|
||||
const Select = props => {
|
||||
const {children, ...attrs} = props;
|
||||
return (
|
||||
<SelectField className={styles.Select} {...attrs}>
|
||||
{children}
|
||||
</SelectField>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
const Wizard = (props) => {
|
||||
const {children, currentStep, ...rest} = props;
|
||||
return (
|
||||
<section>
|
||||
{React.Children.toArray(children)
|
||||
.filter((child, i) => i === currentStep)
|
||||
.map((child, i) =>
|
||||
React.cloneElement(child, {
|
||||
i,
|
||||
currentStep,
|
||||
...rest
|
||||
})
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Wizard.propTypes = {
|
||||
currentStep: PropTypes.number.isRequired,
|
||||
nextStep: PropTypes.func.isRequired,
|
||||
previousStep: PropTypes.func.isRequired,
|
||||
goToStep: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Wizard;
|
||||
@@ -0,0 +1,107 @@
|
||||
.WizardNav {
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
border: solid 1px #DFDFDF;
|
||||
background-color: #F0F0F0;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
color: #A7A7A7;
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
border-radius: 1px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.done {
|
||||
border-color: #00796B;
|
||||
background-color: #00796B;
|
||||
color: #ffffff;
|
||||
span {
|
||||
&::after {
|
||||
border-color: transparent transparent transparent #00796B;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #FFFFFF;
|
||||
color: #636363;
|
||||
font-weight: 500;
|
||||
|
||||
span {
|
||||
&::after {
|
||||
border-color: transparent transparent transparent #FFFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
transition: opacity 0.2s ease;
|
||||
opacity: 0;
|
||||
vertical-align: middle;
|
||||
font-size: 16px;
|
||||
margin-top: -5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 10px 20px;
|
||||
margin-right: 10px;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -51px;
|
||||
z-index: 10;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
border: 23px solid #DFDFDF;
|
||||
border-color: transparent transparent transparent #DFDFDF;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
border: 23px solid white;
|
||||
border-color: transparent transparent transparent #f0f0f0;
|
||||
top: 0;
|
||||
left: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
border: 23px solid #DFDFDF;
|
||||
border-color: transparent transparent transparent #DFDFDF;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
border: 23px solid white;
|
||||
border-color: transparent transparent transparent #fafafa;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './WizardNav.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
const WizardNav = props => {
|
||||
const {goToStep = () => {}, currentStep, items, icon} = props;
|
||||
return (
|
||||
<nav className={styles.WizardNav}>
|
||||
<ul>
|
||||
{
|
||||
items.map((item, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className={`${currentStep === item.step ? styles.active : ''} ${item.step < currentStep ? styles.done : ''}`}
|
||||
onClick={() => goToStep(item.step)}>
|
||||
{item.text}
|
||||
{icon && <Icon name={icon} />}
|
||||
<span/>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
WizardNav.propTypes = {
|
||||
currentStep: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default WizardNav;
|
||||
@@ -16,4 +16,8 @@ export {default as Card} from './components/Card';
|
||||
export {default as FormField} from './components/FormField';
|
||||
export {default as Success} from './components/Success';
|
||||
export {default as Pager} from './components/Pager';
|
||||
export {default as Wizard} from './components/Wizard';
|
||||
export {default as WizardNav} from './components/WizardNav';
|
||||
export {default as Select} from './components/Select';
|
||||
export {default as Option} from './components/Option';
|
||||
export {default as SnackBar} from './components/SnackBar';
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
version: '2'
|
||||
services:
|
||||
talk:
|
||||
image: coralproject/talk:latest
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
mongo:
|
||||
image: mongo:3.2
|
||||
restart: always
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
redis:
|
||||
image: redis:3.2
|
||||
restart: always
|
||||
volumes:
|
||||
- redis:/data
|
||||
volumes:
|
||||
mongo:
|
||||
external: false
|
||||
redis:
|
||||
external: false
|
||||
@@ -121,6 +121,7 @@ const ErrInvalidAssetURL = new APIError('asset_url is invalid', {
|
||||
// ErrNotAuthorized is an error that is returned in the event an operation is
|
||||
// deemed not authorized.
|
||||
const ErrNotAuthorized = new APIError('not authorized', {
|
||||
translation_key: 'NOT_AUTHORIZED',
|
||||
status: 401
|
||||
});
|
||||
|
||||
@@ -128,6 +129,18 @@ const ErrNotAuthorized = new APIError('not authorized', {
|
||||
// initialized.
|
||||
const ErrSettingsNotInit = new Error('settings not initialized, run `./bin/cli setup` to setup the application first');
|
||||
|
||||
// ErrSettingsInit is returned when the setup endpoint is hit and we are already
|
||||
// initialized.
|
||||
const ErrSettingsInit = new APIError('settings are already initialized', {
|
||||
status: 500
|
||||
});
|
||||
|
||||
// ErrInstallLock is returned when the setup endpoint is hit and the install
|
||||
// lock is present.
|
||||
const ErrInstallLock = new APIError('install lock active', {
|
||||
status: 500
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
ExtendableError,
|
||||
APIError,
|
||||
@@ -145,5 +158,7 @@ module.exports = {
|
||||
ErrNotFound,
|
||||
ErrInvalidAssetURL,
|
||||
ErrAuthentication,
|
||||
ErrNotAuthorized
|
||||
ErrNotAuthorized,
|
||||
ErrSettingsInit,
|
||||
ErrInstallLock
|
||||
};
|
||||
|
||||
@@ -5,6 +5,15 @@ const util = require('./util');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const ActionModel = require('../../models/action');
|
||||
|
||||
/**
|
||||
* Gets actions based on their item id's.
|
||||
*/
|
||||
const genActionsByItemID = (_, item_ids) => {
|
||||
return ActionsService
|
||||
.findByItemIdArray(item_ids)
|
||||
.then(util.arrayJoinBy(item_ids, 'item_id'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Looks up actions based on the requested id's all bounded by the user.
|
||||
* @param {Object} context the context of the request
|
||||
@@ -35,6 +44,7 @@ const getItemIdsByActionTypeAndItemType = (_, action_type, item_type) => {
|
||||
*/
|
||||
module.exports = (context) => ({
|
||||
Actions: {
|
||||
getByID: new DataLoader((ids) => genActionsByItemID(context, ids)),
|
||||
getSummariesByItemID: new DataLoader((ids) => genActionSummariessByItemID(context, ids)),
|
||||
getByTypes: ({action_type, item_type}) => getItemIdsByActionTypeAndItemType(context, action_type, item_type)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const util = require('./util');
|
||||
const DataLoader = require('dataloader');
|
||||
|
||||
const CommentModel = require('../../models/comment');
|
||||
|
||||
@@ -135,6 +136,95 @@ const getCommentsByQuery = ({user}, {ids, statuses, asset_id, parent_id, author_
|
||||
.limit(limit);
|
||||
};
|
||||
|
||||
const genRecentReplies = (_, ids) => {
|
||||
return CommentModel.aggregate([
|
||||
|
||||
// get all the replies for the comments in question
|
||||
{$match: {
|
||||
parent_id: {
|
||||
$in: ids
|
||||
}
|
||||
}},
|
||||
|
||||
// sort these by their created at timestamp, CHRONOLOGICAL'y as these are
|
||||
// replies
|
||||
{$sort: {
|
||||
created_at: 1
|
||||
}},
|
||||
|
||||
// group these replies by their parent_id
|
||||
{$group: {
|
||||
_id: '$parent_id',
|
||||
replies: {
|
||||
$push: '$$ROOT'
|
||||
}
|
||||
}},
|
||||
|
||||
// project it so that we only retain the first 3 replies of each parent
|
||||
// comment
|
||||
{$project: {
|
||||
_id: '$_id',
|
||||
replies: {
|
||||
$slice: [
|
||||
'$replies',
|
||||
0,
|
||||
3
|
||||
]
|
||||
}
|
||||
}},
|
||||
|
||||
{$unwind: '$replies'},
|
||||
|
||||
])
|
||||
.then((replies) => replies.map((reply) => reply.replies))
|
||||
.then(util.arrayJoinBy(ids, 'parent_id'));
|
||||
};
|
||||
|
||||
const genRecentComments = (_, ids) => {
|
||||
return CommentModel.aggregate([
|
||||
|
||||
// get all the replies for the comments in question
|
||||
{$match: {
|
||||
asset_id: {
|
||||
$in: ids
|
||||
}
|
||||
}},
|
||||
|
||||
// sort these by their created at timestamp, CHRONOLOGICAL'y as these are
|
||||
// replies
|
||||
{$sort: {
|
||||
created_at: 1
|
||||
}},
|
||||
|
||||
// group these replies by their parent_id
|
||||
{$group: {
|
||||
_id: '$asset_id',
|
||||
comments: {
|
||||
$push: '$$ROOT'
|
||||
}
|
||||
}},
|
||||
|
||||
// project it so that we only retain the first 3 replies of each parent
|
||||
// comment
|
||||
{$project: {
|
||||
_id: '$_id',
|
||||
comments: {
|
||||
$slice: [
|
||||
'$comments',
|
||||
0,
|
||||
3
|
||||
]
|
||||
}
|
||||
}},
|
||||
|
||||
// Unwind these comments.
|
||||
{$unwind: '$comments'},
|
||||
|
||||
])
|
||||
.then((replies) => replies.map((reply) => reply.comments))
|
||||
.then(util.arrayJoinBy(ids, 'asset_id'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
@@ -144,6 +234,8 @@ module.exports = (context) => ({
|
||||
Comments: {
|
||||
getByQuery: (query) => getCommentsByQuery(context, query),
|
||||
countByAssetID: new util.SharedCacheDataLoader('Comments.countByAssetID', 3600, (ids) => getCountsByAssetID(context, ids)),
|
||||
countByParentID: new util.SharedCacheDataLoader('Comments.countByParentID', 3600, (ids) => getCountsByParentID(context, ids))
|
||||
countByParentID: new util.SharedCacheDataLoader('Comments.countByParentID', 3600, (ids) => getCountsByParentID(context, ids)),
|
||||
genRecentReplies: new DataLoader((ids) => genRecentReplies(context, ids)),
|
||||
genRecentComments: new DataLoader((ids) => genRecentComments(context, ids))
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const ActionModel = require('../../models/action');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const UsersService = require('../../services/users');
|
||||
const errors = require('../../errors');
|
||||
|
||||
/**
|
||||
* Creates an action on a item. If the item is a user flag, sets the user's status to
|
||||
@@ -11,11 +12,12 @@ const UsersService = require('../../services/users');
|
||||
* @param {String} action_type type of the action
|
||||
* @return {Promise} resolves to the action created
|
||||
*/
|
||||
const createAction = ({user = {}}, {item_id, item_type, action_type, metadata = {}}) => {
|
||||
const createAction = ({user = {}}, {item_id, item_type, action_type, group_id, metadata = {}}) => {
|
||||
return ActionsService.insertUserAction({
|
||||
item_id,
|
||||
item_type,
|
||||
user_id: user.id,
|
||||
group_id,
|
||||
action_type,
|
||||
metadata
|
||||
}).then((action) => {
|
||||
@@ -58,8 +60,8 @@ module.exports = (context) => {
|
||||
|
||||
return {
|
||||
Action: {
|
||||
create: () => {},
|
||||
delete: () => {}
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
delete: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,6 +36,12 @@ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id =
|
||||
}
|
||||
}
|
||||
|
||||
if (user.hasRoles('ADMIN')) {
|
||||
return CommentsService
|
||||
.addTag(comment.id, 'STAFF', user.id)
|
||||
.then(() => comment);
|
||||
}
|
||||
|
||||
return comment;
|
||||
});
|
||||
};
|
||||
@@ -171,7 +177,7 @@ module.exports = (context) => {
|
||||
|
||||
return {
|
||||
Comment: {
|
||||
create: () => {}
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
const Action = {
|
||||
__resolveType({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'FLAG':
|
||||
return 'FlagAction';
|
||||
case 'LIKE':
|
||||
return 'LikeAction';
|
||||
}
|
||||
},
|
||||
|
||||
// This will load the user for the specific action. We'll limit this to the
|
||||
// admin users only or the current logged in user.
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
const ActionSummary = {};
|
||||
const ActionSummary = {
|
||||
__resolveType({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'FLAG':
|
||||
return 'FlagActionSummary';
|
||||
case 'LIKE':
|
||||
return 'LikeActionSummary';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = ActionSummary;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Asset = {
|
||||
recentComments({id}, _, {loaders: {Comments}}) {
|
||||
return Comments.genRecentComments.load(id);
|
||||
},
|
||||
comments({id}, {sort, limit}, {loaders: {Comments}}) {
|
||||
return Comments.getByQuery({
|
||||
asset_id: id,
|
||||
|
||||
@@ -2,6 +2,9 @@ const Comment = {
|
||||
user({author_id}, _, {loaders: {Users}}) {
|
||||
return Users.getByID.load(author_id);
|
||||
},
|
||||
recentReplies({id}, _, {loaders: {Comments}}) {
|
||||
return Comments.genRecentReplies.load(id);
|
||||
},
|
||||
replies({id, asset_id}, {sort, limit}, {loaders: {Comments}}) {
|
||||
return Comments.getByQuery({
|
||||
asset_id,
|
||||
@@ -13,7 +16,16 @@ const Comment = {
|
||||
replyCount({id}, _, {loaders: {Comments}}) {
|
||||
return Comments.countByParentID.load(id);
|
||||
},
|
||||
actions({id}, _, {loaders: {Actions}}) {
|
||||
actions({id}, _, {user, loaders: {Actions}}) {
|
||||
|
||||
// Only return the actions if the user is not an admin.
|
||||
if (user && user.hasRoles('ADMIN')) {
|
||||
return Actions.getByID.load(id);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
action_summaries({id}, _, {loaders: {Actions}}) {
|
||||
return Actions.getSummariesByItemID.load(id);
|
||||
},
|
||||
asset({asset_id}, _, {loaders: {Assets}}) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const FlagAction = {
|
||||
|
||||
// Stored in the metadata, extract and return.
|
||||
reason({metadata: {reason}}) {
|
||||
return reason;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = FlagAction;
|
||||
@@ -0,0 +1,7 @@
|
||||
const FlagActionSummary = {
|
||||
reason({group_id}) {
|
||||
return group_id;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = FlagActionSummary;
|
||||
@@ -0,0 +1,3 @@
|
||||
const GenericUserError = {};
|
||||
|
||||
module.exports = GenericUserError;
|
||||
@@ -1,21 +1,33 @@
|
||||
const Action = require('./action');
|
||||
const ActionSummary = require('./action_summary');
|
||||
const Action = require('./action');
|
||||
const Asset = require('./asset');
|
||||
const Comment = require('./comment');
|
||||
const Date = require('./date');
|
||||
const FlagActionSummary = require('./flag_action_summary');
|
||||
const FlagAction = require('./flag_action');
|
||||
const GenericUserError = require('./generic_user_error');
|
||||
const LikeAction = require('./like_action');
|
||||
const RootMutation = require('./root_mutation');
|
||||
const RootQuery = require('./root_query');
|
||||
const Settings = require('./settings');
|
||||
const UserError = require('./user_error');
|
||||
const User = require('./user');
|
||||
const ValidationUserError = require('./validation_user_error');
|
||||
|
||||
module.exports = {
|
||||
Action,
|
||||
ActionSummary,
|
||||
Action,
|
||||
Asset,
|
||||
Comment,
|
||||
Date,
|
||||
FlagActionSummary,
|
||||
FlagAction,
|
||||
GenericUserError,
|
||||
LikeAction,
|
||||
RootMutation,
|
||||
RootQuery,
|
||||
Settings,
|
||||
User
|
||||
UserError,
|
||||
User,
|
||||
ValidationUserError,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const LikeAction = {
|
||||
|
||||
};
|
||||
|
||||
module.exports = LikeAction;
|
||||
@@ -1,12 +1,31 @@
|
||||
/**
|
||||
* Wraps up a promise to return an object with the resolution of the promise
|
||||
* keyed at `key` or an error caught at `errors`.
|
||||
*/
|
||||
const wrapResponse = (key) => (promise) => {
|
||||
return promise.then((value) => {
|
||||
let res = {};
|
||||
if (key) {
|
||||
res[key] = value;
|
||||
}
|
||||
return res;
|
||||
}).catch((err) => ({
|
||||
errors: [err]
|
||||
}));
|
||||
};
|
||||
|
||||
const RootMutation = {
|
||||
createComment(_, {asset_id, parent_id, body}, {mutators: {Comment}}) {
|
||||
return Comment.create({asset_id, parent_id, body});
|
||||
return wrapResponse('comment')(Comment.create({asset_id, parent_id, body}));
|
||||
},
|
||||
createAction(_, {action}, {mutators: {Action}}) {
|
||||
return Action.create(action);
|
||||
createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) {
|
||||
return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'}));
|
||||
},
|
||||
createFlag(_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) {
|
||||
return wrapResponse('flag')(Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}}));
|
||||
},
|
||||
deleteAction(_, {id}, {mutators: {Action}}) {
|
||||
return Action.delete({id});
|
||||
return wrapResponse(null)(Action.delete({id}));
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
const User = {
|
||||
actions({id}, _, {loaders: {Actions}}) {
|
||||
action_summaries({id}, _, {loaders: {Actions}}) {
|
||||
return Actions.getSummariesByItemID.load(id);
|
||||
},
|
||||
actions({id}, _, {user, loaders: {Actions}}) {
|
||||
|
||||
// Only return the actions if the user is not an admin.
|
||||
if (user && user.hasRoles('ADMIN')) {
|
||||
return Actions.getByID.load(id);
|
||||
}
|
||||
|
||||
},
|
||||
comments({id}, _, {loaders: {Comments}, user}) {
|
||||
|
||||
// If the user is not an admin, only return comment list for the owner of
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const UserError = {
|
||||
__resolveType({field_name}) {
|
||||
if (field_name) {
|
||||
return 'ValidationUserError';
|
||||
}
|
||||
|
||||
return 'GenericUserError';
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = UserError;
|
||||
@@ -0,0 +1,3 @@
|
||||
const ValidationUserError = {};
|
||||
|
||||
module.exports = ValidationUserError;
|
||||
@@ -1,8 +1,15 @@
|
||||
const tools = require('graphql-tools');
|
||||
const maskErrors = require('graphql-errors').maskErrors;
|
||||
|
||||
const resolvers = require('./resolvers');
|
||||
const typeDefs = require('./typeDefs');
|
||||
|
||||
const schema = tools.makeExecutableSchema({typeDefs, resolvers});
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
// Mask errors that are thrown if we are in a production environment.
|
||||
maskErrors(schema);
|
||||
}
|
||||
|
||||
module.exports = schema;
|
||||
|
||||
+357
-83
@@ -1,16 +1,94 @@
|
||||
# Establishes the ordering of the content by their created_at time stamp.
|
||||
enum SORT_ORDER {
|
||||
# newest to oldest order.
|
||||
REVERSE_CHRONOLOGICAL
|
||||
|
||||
# oldest to newer order.
|
||||
CHRONOLOGICAL
|
||||
}
|
||||
################################################################################
|
||||
## Custom Scalar Types
|
||||
################################################################################
|
||||
|
||||
# Date represented as an ISO8601 string.
|
||||
scalar Date
|
||||
|
||||
|
||||
################################################################################
|
||||
## Users
|
||||
################################################################################
|
||||
|
||||
# Roles that a user can have, these can be combined.
|
||||
enum USER_ROLES {
|
||||
|
||||
# an administrator of the site
|
||||
ADMIN
|
||||
|
||||
# a moderator of the site
|
||||
MODERATOR
|
||||
}
|
||||
|
||||
# Any person who can author comments, create actions, and view comments on a
|
||||
# stream.
|
||||
type User {
|
||||
|
||||
# The ID of the User.
|
||||
id: ID!
|
||||
|
||||
# display name of a user.
|
||||
displayName: String!
|
||||
|
||||
# Action summaries against the user.
|
||||
action_summaries: [ActionSummary]
|
||||
|
||||
# Actions completed on the parent.
|
||||
actions: [Action]
|
||||
|
||||
# the current roles of the user.
|
||||
roles: [USER_ROLES]
|
||||
|
||||
# determines whether the user can edit their username
|
||||
canEditName: Boolean
|
||||
|
||||
# returns all comments based on a query.
|
||||
comments(query: CommentsQuery): [Comment]
|
||||
}
|
||||
|
||||
type Tag {
|
||||
# the actual tag for the comment.
|
||||
name: String!
|
||||
|
||||
# the user that assigned the tag. If NULL then the system automatically tagged it.
|
||||
assigned_by: String
|
||||
|
||||
# the time when the tag was assigned.
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Comments
|
||||
################################################################################
|
||||
|
||||
# The statuses that a comment may have.
|
||||
enum COMMENT_STATUS {
|
||||
|
||||
# The comment has been accepted by a moderator.
|
||||
ACCEPTED
|
||||
|
||||
# The comment has been rejected by a moderator.
|
||||
REJECTED
|
||||
|
||||
# The comment was created while the asset's premoderation option was on, and
|
||||
# new comments that haven't been moderated yet are referred to as
|
||||
# "premoderated" or "premod" comments.
|
||||
PREMOD
|
||||
}
|
||||
|
||||
# The types of action there are as enum's.
|
||||
enum ACTION_TYPE {
|
||||
|
||||
# Represents a LikeAction.
|
||||
LIKE
|
||||
|
||||
# Represents a FlagAction.
|
||||
FLAG
|
||||
}
|
||||
|
||||
# CommentsQuery allows the ability to query comments by a specific methods.
|
||||
input CommentsQuery {
|
||||
|
||||
# current status of a comment.
|
||||
statuses: [COMMENT_STATUS]
|
||||
|
||||
@@ -30,113 +108,181 @@ input CommentsQuery {
|
||||
# skip results from the last created_at timestamp.
|
||||
cursor: Date
|
||||
|
||||
# filter by a specific tag name.
|
||||
tag: [String]
|
||||
|
||||
# sort the results by created_at.
|
||||
sort: SORT_ORDER = REVERSE_CHRONOLOGICAL
|
||||
}
|
||||
|
||||
enum USER_ROLES {
|
||||
# an administrator of the site
|
||||
ADMIN
|
||||
|
||||
# a moderator of the site
|
||||
MODERATOR
|
||||
}
|
||||
|
||||
# Any person who can author comments, create actions, and view comments on a
|
||||
# stream.
|
||||
type User {
|
||||
id: ID!
|
||||
|
||||
# display name of a user.
|
||||
displayName: String!
|
||||
|
||||
# actions against a specific user.
|
||||
actions: [ActionSummary]
|
||||
|
||||
# the current roles of the user.
|
||||
roles: [USER_ROLES]
|
||||
|
||||
# determines whether the user can edit their username
|
||||
canEditName: Boolean
|
||||
|
||||
# returns all comments based on a query.
|
||||
comments(query: CommentsQuery): [Comment]
|
||||
}
|
||||
|
||||
# Comment is the base representation of user interaction in Talk.
|
||||
type Comment {
|
||||
|
||||
# The ID of the comment.
|
||||
id: ID!
|
||||
|
||||
# the actual comment data.
|
||||
# The actual comment data.
|
||||
body: String!
|
||||
|
||||
# the tags on the comment
|
||||
tags: [Tag]
|
||||
|
||||
# the user who authored the comment.
|
||||
user: User
|
||||
|
||||
# the recent replies made against this comment.
|
||||
recentReplies: [Comment]
|
||||
|
||||
# the replies that were made to the comment.
|
||||
replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3): [Comment]
|
||||
|
||||
# the count of replies on a comment
|
||||
# The count of replies on a comment.
|
||||
replyCount: Int
|
||||
|
||||
# the actions made against a comment.
|
||||
actions: [ActionSummary]
|
||||
# Actions completed on the parent.
|
||||
actions: [Action]
|
||||
|
||||
# the asset that a comment was made on.
|
||||
# Action summaries against a comment.
|
||||
action_summaries: [ActionSummary]
|
||||
|
||||
# The asset that a comment was made on.
|
||||
asset: Asset
|
||||
|
||||
# the current status of a comment.
|
||||
# The current status of a comment.
|
||||
status: COMMENT_STATUS
|
||||
|
||||
# the time when the comment was created
|
||||
# The time when the comment was created
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
enum ITEM_TYPE {
|
||||
ASSETS
|
||||
COMMENTS
|
||||
USERS
|
||||
}
|
||||
################################################################################
|
||||
## Actions
|
||||
################################################################################
|
||||
|
||||
enum ACTION_TYPE {
|
||||
LIKE
|
||||
FLAG
|
||||
}
|
||||
# An action rendered against a parent enity item.
|
||||
interface Action {
|
||||
|
||||
type Action {
|
||||
# The ID of the action.
|
||||
id: ID!
|
||||
action_type: ACTION_TYPE!
|
||||
|
||||
item_id: ID!
|
||||
item_type: ITEM_TYPE!
|
||||
# The author of the action.
|
||||
user: User
|
||||
|
||||
user: User!
|
||||
# The time when the Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Action was created.
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
type ActionSummary {
|
||||
action_type: ACTION_TYPE!
|
||||
item_type: ITEM_TYPE!
|
||||
# A summary of actions based on the specific grouping of the group_id.
|
||||
interface ActionSummary {
|
||||
|
||||
# The count of actions with this group.
|
||||
count: Int
|
||||
|
||||
# The current user's action.
|
||||
current_user: Action
|
||||
}
|
||||
|
||||
# LikeAction is used by users who "like" a specific entity.
|
||||
type LikeAction implements Action {
|
||||
|
||||
# The ID of the action.
|
||||
id: ID!
|
||||
|
||||
# The author of the action.
|
||||
user: User
|
||||
|
||||
# The time when the Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Action was created.
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
# LikeActionSummary is counts the amount of "likes" that a specific entity has.
|
||||
type LikeActionSummary implements ActionSummary {
|
||||
|
||||
# The count of likes against the parent entity.
|
||||
count: Int!
|
||||
|
||||
current_user: LikeAction
|
||||
}
|
||||
|
||||
# A FLAG action that contains flag metadata.
|
||||
type FlagAction implements Action {
|
||||
|
||||
# The ID of the Flag Action.
|
||||
id: ID!
|
||||
|
||||
# The reason for which the Flag Action was created.
|
||||
reason: String
|
||||
|
||||
# An optional message sent with the flagging action by the user.
|
||||
message: String
|
||||
|
||||
# The user who created the action.
|
||||
user: User
|
||||
|
||||
# The time when the Flag Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Flag Action was created.
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
# Summary for Flag Action with a a unique reason.
|
||||
type FlagActionSummary implements ActionSummary {
|
||||
|
||||
# The total count of flags with this reason.
|
||||
count: Int!
|
||||
|
||||
# The reason for which the Flag Action was created.
|
||||
reason: String
|
||||
|
||||
# The flag by the current user against the parent entity with this reason.
|
||||
current_user: FlagAction
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Settings
|
||||
################################################################################
|
||||
|
||||
# The moderation mode of the site.
|
||||
enum MODERATION_MODE {
|
||||
|
||||
# Comments posted while in `PRE` mode will be labeled with a `PREMOD`
|
||||
# status and will require a moderator decision before being visible.
|
||||
PRE
|
||||
|
||||
# Comments posted while in `POST` will be visible immediately.
|
||||
POST
|
||||
}
|
||||
|
||||
# Site wide global settings.
|
||||
type Settings {
|
||||
|
||||
# Moderation mode for the site.
|
||||
moderation: MODERATION_MODE!
|
||||
|
||||
# Enables a requirement for email confirmation before a user can login.
|
||||
requireEmailConfirmation: Boolean
|
||||
|
||||
infoBoxEnable: Boolean
|
||||
infoBoxContent: String
|
||||
closeTimeout: Int
|
||||
closedMessage: String
|
||||
charCountEnable: Boolean
|
||||
charCount: Int
|
||||
requireEmailConfirmation: Boolean
|
||||
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Assets
|
||||
################################################################################
|
||||
|
||||
# Where comments are made on.
|
||||
type Asset {
|
||||
|
||||
# The current ID of the asset.
|
||||
@@ -148,6 +294,9 @@ type Asset {
|
||||
# The URL that the asset is located on.
|
||||
url: String
|
||||
|
||||
# Returns recent comments
|
||||
recentComments: [Comment]
|
||||
|
||||
# The top level comments that are attached to the asset.
|
||||
comments(sort: SORT_ORDER = REVERSE_CHRONOLOGICAL, limit: Int = 10): [Comment]
|
||||
|
||||
@@ -165,53 +314,178 @@ type Asset {
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
enum COMMENT_STATUS {
|
||||
ACCEPTED
|
||||
REJECTED
|
||||
PREMOD
|
||||
################################################################################
|
||||
## Errors
|
||||
################################################################################
|
||||
|
||||
# Any error rendered due to the user's input.
|
||||
interface UserError {
|
||||
|
||||
# Translation key relating to a translatable string containing details to be
|
||||
# displayed to the end user.
|
||||
translation_key: String!
|
||||
}
|
||||
|
||||
# A generic error not related to validation reasons.
|
||||
type GenericUserError implements UserError {
|
||||
|
||||
# Translation key relating to a translatable string containing details to be
|
||||
# displayed to the end user.
|
||||
translation_key: String!
|
||||
}
|
||||
|
||||
# A validation error that affects the input.
|
||||
type ValidationUserError implements UserError {
|
||||
|
||||
# Translation key relating to a translatable string containing details to be
|
||||
# displayed to the end user.
|
||||
translation_key: String!
|
||||
|
||||
# The field in question that caused the error.
|
||||
field_name: String!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Queries
|
||||
################################################################################
|
||||
|
||||
# Establishes the ordering of the content by their created_at time stamp.
|
||||
enum SORT_ORDER {
|
||||
|
||||
# newest to oldest order.
|
||||
REVERSE_CHRONOLOGICAL
|
||||
|
||||
# oldest to newer order.
|
||||
CHRONOLOGICAL
|
||||
}
|
||||
|
||||
# All queries that can be executed.
|
||||
type RootQuery {
|
||||
|
||||
# retrieves site wide settings and defaults.
|
||||
# Site wide settings and defaults.
|
||||
settings: Settings
|
||||
|
||||
# retrieves all assets.
|
||||
# All assets.
|
||||
assets: [Asset]
|
||||
|
||||
# retrieves a specific asset.
|
||||
# Find or create an asset by url, or just find with the ID.
|
||||
asset(id: ID, url: String): Asset
|
||||
|
||||
# retrieves comments based on the input query.
|
||||
# Comments returned based on a query.
|
||||
comments(query: CommentsQuery!): [Comment]
|
||||
|
||||
# retrieves the current logged in user.
|
||||
# The currently logged in user based on the request.
|
||||
me: User
|
||||
}
|
||||
|
||||
input CreateActionInput {
|
||||
# the type of action.
|
||||
action_type: ACTION_TYPE!
|
||||
################################################################################
|
||||
## Mutations
|
||||
################################################################################
|
||||
|
||||
# the type of the item.
|
||||
item_type: ITEM_TYPE!
|
||||
# Response defines what can be expected from any response to a mutation action.
|
||||
interface Response {
|
||||
|
||||
# the id of the item that is related to the action.
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# CreateCommentResponse is returned with the comment that was created and any
|
||||
# errors that may have occured in the attempt to create it.
|
||||
type CreateCommentResponse implements Response {
|
||||
|
||||
# The comment that was created.
|
||||
comment: Comment
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# Used to represent the item type for an action.
|
||||
enum ACTION_ITEM_TYPE {
|
||||
|
||||
# The action references a entity of type Asset.
|
||||
ASSETS
|
||||
|
||||
# The action references a entity of type Comment.
|
||||
COMMENTS
|
||||
|
||||
# The action references a entity of type User.
|
||||
USERS
|
||||
}
|
||||
|
||||
input CreateLikeInput {
|
||||
|
||||
# The item's id for which we are to create a like.
|
||||
item_id: ID!
|
||||
|
||||
# The type of the item for which we are to create the like.
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
}
|
||||
|
||||
type CreateLikeResponse implements Response {
|
||||
|
||||
# The like that was created.
|
||||
like: LikeAction
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
input CreateFlagInput {
|
||||
|
||||
# The item's id for which we are to create a flag.
|
||||
item_id: ID!
|
||||
|
||||
# The type of the item for which we are to create the flag.
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
|
||||
# The reason for flagging the item.
|
||||
reason: String!
|
||||
|
||||
# An optional message sent with the flagging action by the user.
|
||||
message: String
|
||||
}
|
||||
|
||||
# CreateFlagResponse is the response returned with possibly some errors
|
||||
# relating to the creating the flag action attempt and possibly the flag that
|
||||
# was created.
|
||||
type CreateFlagResponse implements Response {
|
||||
|
||||
# The like that was created.
|
||||
flag: FlagAction
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# DeleteActionResponse is the response returned with possibly some errors
|
||||
# relating to the delete action attempt.
|
||||
type DeleteActionResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# All mutations for the application are defined on this object.
|
||||
type RootMutation {
|
||||
# creates a comment on the asset.
|
||||
createComment(asset_id: ID!, parent_id: ID, body: String!): Comment
|
||||
|
||||
# creates an action based on an input.
|
||||
createAction(action: CreateActionInput!): Action
|
||||
# Creates a comment on the asset.
|
||||
createComment(asset_id: ID!, parent_id: ID, body: String!): CreateCommentResponse
|
||||
|
||||
# delete an action based on the action id.
|
||||
deleteAction(id: ID!): Boolean
|
||||
# Creates a like on an entity.
|
||||
createLike(like: CreateLikeInput!): CreateLikeResponse
|
||||
|
||||
# Creates a flag on an entity.
|
||||
createFlag(flag: CreateFlagInput!): CreateFlagResponse
|
||||
|
||||
# Delete an action based on the action id.
|
||||
deleteAction(id: ID!): DeleteActionResponse
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Schema
|
||||
################################################################################
|
||||
|
||||
schema {
|
||||
query: RootQuery
|
||||
mutation: RootMutation
|
||||
|
||||
@@ -29,6 +29,12 @@ const ActionSchema = new Schema({
|
||||
},
|
||||
item_id: String,
|
||||
user_id: String,
|
||||
|
||||
// The element that summaries will additionally group on in addtion to their action_type, item_type, and
|
||||
// item_id.
|
||||
group_id: String,
|
||||
|
||||
// Additional metadata stored on the field.
|
||||
metadata: Schema.Types.Mixed
|
||||
}, {
|
||||
timestamps: {
|
||||
|
||||
@@ -30,6 +30,24 @@ const StatusSchema = new Schema({
|
||||
_id: false
|
||||
});
|
||||
|
||||
/**
|
||||
* The Mongo schema for a Comment Tag.
|
||||
* @type {Schema}
|
||||
*/
|
||||
const TagSchema = new Schema({
|
||||
name: String,
|
||||
|
||||
// The User ID of the user that assigned the status.
|
||||
assigned_by: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
created_at: Date
|
||||
}, {
|
||||
_id: false
|
||||
});
|
||||
|
||||
/**
|
||||
* The Mongo schema for a Comment.
|
||||
* @type {Schema}
|
||||
@@ -49,6 +67,7 @@ const CommentSchema = new Schema({
|
||||
author_id: String,
|
||||
status_history: [StatusSchema],
|
||||
status: {type: String, default: null},
|
||||
tags: [TagSchema],
|
||||
parent_id: String
|
||||
}, {
|
||||
timestamps: {
|
||||
|
||||
@@ -62,6 +62,12 @@ const SettingSchema = new Schema({
|
||||
requireEmailConfirmation: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
domains: {
|
||||
whitelist: {
|
||||
type: Array,
|
||||
default: ['localhost']
|
||||
}
|
||||
}
|
||||
}, {
|
||||
timestamps: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user