mirror of
https://github.com/wassname/talk.git
synced 2026-07-03 14:20:14 +08:00
Merge master
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"presets": [
|
||||
["es2015", {modules: false}]
|
||||
["es2015", {"modules": false}]
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
|
||||
+35
-27
@@ -16,7 +16,9 @@ integration_job: &integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
# TODO: replace with node:8-browsers when build issues are resolved.
|
||||
# - image: circleci/node:8-browsers
|
||||
- image: coralproject/ci
|
||||
- image: circleci/mongo:3
|
||||
- image: circleci/redis:4-alpine
|
||||
steps:
|
||||
@@ -32,10 +34,14 @@ integration_job: &integration_job
|
||||
- store_test_results:
|
||||
when: always
|
||||
path: /tmp/circleci-test-results
|
||||
- store_artifacts:
|
||||
when: always
|
||||
path: /tmp/circleci-test-results
|
||||
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
# npm_dependencies will install the dependencies used by all other steps.
|
||||
# npm_dependencies will install the dependencies used by all other steps.
|
||||
npm_dependencies:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
@@ -56,7 +62,7 @@ jobs:
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: node_modules
|
||||
|
||||
|
||||
# lint will perform file linting.
|
||||
lint:
|
||||
<<: *job_defaults
|
||||
@@ -67,7 +73,7 @@ jobs:
|
||||
- run:
|
||||
name: Perform linting
|
||||
command: yarn lint
|
||||
|
||||
|
||||
# build_assets will build the static assets.
|
||||
build_assets:
|
||||
<<: *job_defaults
|
||||
@@ -86,11 +92,11 @@ jobs:
|
||||
- save_cache:
|
||||
key: build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
paths:
|
||||
- ./node_modules/.cache/hard-source
|
||||
- ./node_modules/.cache/babel-loader
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: dist
|
||||
|
||||
|
||||
# test_unit will run the unit tests.
|
||||
test_unit:
|
||||
<<: *job_defaults
|
||||
@@ -102,7 +108,7 @@ jobs:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- run:
|
||||
- run:
|
||||
name: Setup the test results directory
|
||||
command: mkdir -p /tmp/circleci-test-results
|
||||
- run:
|
||||
@@ -120,7 +126,7 @@ jobs:
|
||||
- store_test_results:
|
||||
when: always
|
||||
path: /tmp/circleci-test-results
|
||||
|
||||
|
||||
# test_integration_chrome_local will run the integration tests locally with
|
||||
# chrome headless.
|
||||
test_integration_chrome_local:
|
||||
@@ -128,7 +134,7 @@ jobs:
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
E2E_BROWSERS: chrome
|
||||
|
||||
|
||||
# test_integration_firefox_local will run the integration tests locally with
|
||||
# firefox headless.
|
||||
test_integration_firefox_local:
|
||||
@@ -145,7 +151,7 @@ jobs:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: chrome
|
||||
|
||||
|
||||
# test_integration_firefox will run the integration tests with firefox in
|
||||
# browserstack.
|
||||
test_integration_firefox:
|
||||
@@ -154,7 +160,7 @@ jobs:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: firefox
|
||||
|
||||
|
||||
# test_integration_edge will run the integration tests with edge in
|
||||
# browserstack.
|
||||
test_integration_edge:
|
||||
@@ -163,7 +169,7 @@ jobs:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: edge
|
||||
|
||||
|
||||
# test_integration_ie will run the integration tests with ie in
|
||||
# browserstack.
|
||||
test_integration_ie:
|
||||
@@ -174,7 +180,7 @@ jobs:
|
||||
E2E_BROWSERS: ie
|
||||
# TODO: remove when more reliable
|
||||
E2E_MAX_RETRIES: 1
|
||||
|
||||
|
||||
# test_integration_safari will run the integration tests with safari in
|
||||
# browserstack.
|
||||
test_integration_safari:
|
||||
@@ -185,7 +191,7 @@ jobs:
|
||||
E2E_BROWSERS: safari
|
||||
# TODO: remove when more reliable
|
||||
E2E_MAX_RETRIES: 1
|
||||
|
||||
|
||||
# deploy will deploy the application as a docker image.
|
||||
deploy:
|
||||
<<: *job_defaults
|
||||
@@ -236,13 +242,14 @@ workflows:
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_integration_chrome_local:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- build_assets
|
||||
- test_integration_firefox_local:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- build_assets
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_firefox_local:
|
||||
# <<: *filter_develop
|
||||
# requires:
|
||||
# - build_assets
|
||||
deploy-tagged:
|
||||
jobs:
|
||||
- npm_dependencies:
|
||||
@@ -271,14 +278,15 @@ workflows:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
- test_integration_ie:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
- test_integration_safari:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_ie:
|
||||
# <<: *filter_deploy
|
||||
# requires:
|
||||
# - build_assets
|
||||
# - test_integration_safari:
|
||||
# <<: *filter_deploy
|
||||
# requires:
|
||||
# - build_assets
|
||||
- deploy:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
@@ -289,4 +297,4 @@ workflows:
|
||||
- test_integration_edge
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_ie
|
||||
# - test_integration_safari
|
||||
# - test_integration_safari
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ if [[ "$BROWSERSTACK" == "true" && -n "$BROWSERSTACK_KEY" ]]; then
|
||||
echo Testing on browserstack
|
||||
node scripts/e2e.js --reports-folder "$REPORTS_FOLDER" --retries "$E2E_MAX_RETRIES" --timeout "$E2E_WAIT_FOR_TIMEOUT" --browsers "$E2E_BROWSERS" --browserstack
|
||||
else
|
||||
# When browserstack is not available test locally using chrome headless.
|
||||
# When browserstack is not available test locally.
|
||||
echo Testing locally
|
||||
node scripts/e2e.js --reports-folder "$REPORTS_FOLDER" --retries "$E2E_MAX_RETRIES" --timeout "$E2E_WAIT_FOR_TIMEOUT" --browsers "$E2E_BROWSERS" --headless
|
||||
fi
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
**/*.html
|
||||
dist
|
||||
docs
|
||||
node_modules
|
||||
public
|
||||
|
||||
**/*.min.js
|
||||
|
||||
+9
-2
@@ -6,6 +6,7 @@ npm-debug.log*
|
||||
dump.rdb
|
||||
|
||||
client/coral-framework/graphql/introspection.json
|
||||
docs/source/_data/introspection.json
|
||||
|
||||
.env
|
||||
*.cfg
|
||||
@@ -14,6 +15,7 @@ client/coral-framework/graphql/introspection.json
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
.prettierrc.json
|
||||
.vscode
|
||||
|
||||
coverage/
|
||||
test/e2e/reports/
|
||||
@@ -22,13 +24,14 @@ test/e2e/selenium-debug.log
|
||||
browserstack.err
|
||||
|
||||
plugins.json
|
||||
plugins/*
|
||||
|
||||
plugins/*
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-downvote
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-flag-details
|
||||
@@ -43,23 +46,27 @@ plugins/*
|
||||
!plugins/talk-plugin-notifications-category-featured
|
||||
!plugins/talk-plugin-notifications-category-reply
|
||||
!plugins/talk-plugin-notifications-category-staff
|
||||
!plugins/talk-plugin-notifications-digest-daily
|
||||
!plugins/talk-plugin-notifications-digest-hourly
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-profile-settings
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
!plugins/talk-plugin-sort-most-downvoted
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-sort-most-upvoted
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-upvote
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-rich-text
|
||||
!plugins/talk-plugin-rich-text-pell
|
||||
|
||||
**/node_modules/*
|
||||
yarn-error.log
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client", "docs/*"],
|
||||
"ext": "js,json,graphql,yml",
|
||||
"watch": [
|
||||
".",
|
||||
|
||||
+3
-3
@@ -54,12 +54,12 @@ We are looking for _documentarians_ to:
|
||||
* take the lead in making sections, or the over all structure better.
|
||||
|
||||
Our documentation is stored in markdown files in the [docs](docs) directory. We
|
||||
use Jekyll to provide our docs. To preview:
|
||||
use [Hexo](https://hexo.io/) to provide our docs. To preview:
|
||||
|
||||
```shell
|
||||
cd docs
|
||||
bundle install
|
||||
bundle exec jekyll serve
|
||||
yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
Then visit http://127.0.0.1:4000/talk/.
|
||||
|
||||
@@ -18,5 +18,6 @@ ONBUILD COPY . /usr/src/app
|
||||
# clear out the development dependencies again. After this we of course need to
|
||||
# clear out the yarn cache, this saves quite a lot of size.
|
||||
ONBUILD RUN cli plugins reconcile && \
|
||||
yarn && \
|
||||
yarn build && \
|
||||
yarn cache clean
|
||||
@@ -12,19 +12,23 @@ You're just one click away from trying Talk - all you need is a Heroku account a
|
||||
|
||||
## Technical Documentation
|
||||
|
||||
From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://coralproject.github.io/talk/) have everything you need to know.
|
||||
From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://docs.coralproject.net/talk/) have everything you need to know.
|
||||
|
||||
## Product Guide
|
||||
|
||||
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://coralproject.github.io/talk/how-talk-works).
|
||||
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://docs.coralproject.net/talk/how-talk-works).
|
||||
|
||||
## Relevant Links
|
||||
## Pre-Launch Guide
|
||||
|
||||
You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://blog.coralproject.net/youve-installed-talk-now-what/).
|
||||
|
||||
## More Resources
|
||||
|
||||
- [Talk Product Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
|
||||
- [Our Blog](https://blog.coralproject.net/)
|
||||
- [Community Forums](https://community.coralproject.net/)
|
||||
- [Community Guides for Journalism](https://guides.coralproject.net/)
|
||||
- [More About Us](https://coralproject.net/)
|
||||
- [Talk Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
|
||||
|
||||
## End-to-End Testing
|
||||
|
||||
|
||||
+19
-13
@@ -13,6 +13,7 @@ const CommentModel = require('../models/comment');
|
||||
const AssetsService = require('../services/assets');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const Context = require('../graph/context');
|
||||
const inquirer = require('inquirer');
|
||||
const { URL } = require('url');
|
||||
|
||||
@@ -52,22 +53,27 @@ async function refreshAssets(ageString) {
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
|
||||
let assets = await AssetModel.find({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age,
|
||||
let assets = await AssetModel.find(
|
||||
{
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
scraped: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
{
|
||||
scraped: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 1 }
|
||||
);
|
||||
|
||||
// Create a graph context.
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
// Queue all the assets for scraping.
|
||||
await Promise.all(assets.map(scraper.create));
|
||||
|
||||
await Promise.all(assets.map(({ id }) => scraper.create(ctx, id)));
|
||||
console.log('Assets were queued to be scraped');
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
|
||||
+7
-1
@@ -10,6 +10,12 @@ const serve = require('../serve');
|
||||
|
||||
program
|
||||
.option('-j, --jobs', 'enable job processing on this thread')
|
||||
.option(
|
||||
'--disabled-jobs <jobs>',
|
||||
'disable jobs specified if the -j option is passed, specified as a comma separated list',
|
||||
val => val.split(','),
|
||||
[]
|
||||
)
|
||||
.option(
|
||||
'-w, --websockets',
|
||||
'enable the websocket (subscriptions) handler on this thread'
|
||||
@@ -17,7 +23,7 @@ program
|
||||
.parse(process.argv);
|
||||
|
||||
// Start serving.
|
||||
serve({ jobs: program.jobs, websockets: program.websockets }).catch(err => {
|
||||
serve(program).catch(err => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
|
||||
+3
-1
@@ -15,6 +15,7 @@ const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const MigrationService = require('../services/migration');
|
||||
const errors = require('../errors');
|
||||
const Context = require('../graph/context');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
@@ -184,7 +185,8 @@ const performSetup = async () => {
|
||||
},
|
||||
]);
|
||||
|
||||
let { user: newUser } = await SetupService.setup({
|
||||
const ctx = Context.forSystem();
|
||||
let { user: newUser } = await SetupService.setup(ctx, {
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email: user.email,
|
||||
|
||||
+69
-75
@@ -7,8 +7,6 @@
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const { graphql } = require('graphql');
|
||||
const helpers = require('../services/migration/helpers');
|
||||
const { stripIndent } = require('common-tags');
|
||||
const Table = require('cli-table');
|
||||
|
||||
@@ -21,31 +19,15 @@ inquirer.registerPrompt(
|
||||
require('inquirer-autocomplete-prompt')
|
||||
);
|
||||
|
||||
const schema = require('../graph/schema');
|
||||
const Context = require('../graph/context');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const CommentModel = require('../models/comment');
|
||||
const ActionModel = require('../models/action');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
const mongoose = require('../services/mongoose');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
/**
|
||||
* transforms a specific action to a removal action on the target model.
|
||||
*/
|
||||
const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({
|
||||
query: { id: item_id },
|
||||
update: {
|
||||
$inc: {
|
||||
[`action_counts.${action_type.toLowerCase()}`]: -1,
|
||||
[`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a user and cleans up their associated verifications.
|
||||
*/
|
||||
@@ -76,66 +58,29 @@ async function deleteUser(userID) {
|
||||
return util.shutdown();
|
||||
}
|
||||
|
||||
const { transformSingleWithCursor } = helpers({
|
||||
queryBatchSize: 10000,
|
||||
updateBatchSize: 10000,
|
||||
});
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
console.warn("Removing user's actions");
|
||||
|
||||
// Remove all actions against comments.
|
||||
await transformSingleWithCursor(
|
||||
ActionModel.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
|
||||
actionDecrTransformer,
|
||||
CommentModel
|
||||
const { data, errors } = await ctx.graphql(
|
||||
`
|
||||
mutation DeleteUser($user_id: ID!) {
|
||||
delUser(id: $user_id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ user_id: user.id }
|
||||
);
|
||||
if (errors) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
// Remove all actions against users.
|
||||
await transformSingleWithCursor(
|
||||
ActionModel.collection.find({ user_id: user.id, item_type: 'USERS' }),
|
||||
actionDecrTransformer,
|
||||
UserModel
|
||||
);
|
||||
if (data.errors) {
|
||||
throw data.errors;
|
||||
}
|
||||
|
||||
// Remove all the user's actions.
|
||||
await ActionModel.where({ user_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
console.warn("Removing user's comments");
|
||||
|
||||
// Removes all the user's reply counts on each of the comments that they
|
||||
// have commented on.
|
||||
await transformSingleWithCursor(
|
||||
CommentModel.collection.aggregate([
|
||||
{ $match: { author_id: user.id } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
]),
|
||||
({ _id: parent_id, count }) => ({
|
||||
query: { id: parent_id },
|
||||
update: {
|
||||
$inc: {
|
||||
reply_count: -1 * count,
|
||||
},
|
||||
},
|
||||
}),
|
||||
CommentModel
|
||||
);
|
||||
|
||||
// Remove all the user's comments.
|
||||
await CommentModel.where({ author_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
console.warn('Removing the user');
|
||||
|
||||
// Remove the user.
|
||||
await user.remove();
|
||||
console.log('User was deleted.');
|
||||
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
@@ -197,7 +142,7 @@ async function searchUsers() {
|
||||
value = '';
|
||||
}
|
||||
|
||||
const { data, errors } = await graphql(schema, searchQuery, {}, ctx, {
|
||||
const { data, errors } = await ctx.graphql(searchQuery, {
|
||||
value,
|
||||
});
|
||||
if (errors && errors.length > 0) {
|
||||
@@ -312,10 +257,59 @@ async function verifyUserEmail(userID, email) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createUser will prompt the user for the user information when creating a
|
||||
* local user.
|
||||
*/
|
||||
async function createUser() {
|
||||
try {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
message: 'Role',
|
||||
type: 'list',
|
||||
choices: USER_ROLES,
|
||||
},
|
||||
]);
|
||||
|
||||
const { email, username, password, role } = answers;
|
||||
|
||||
// Create the user.
|
||||
const user = await UsersService.createLocalUser(email, password, username);
|
||||
|
||||
// Set the role.
|
||||
await UsersService.setRole(user.id, role);
|
||||
|
||||
console.log(`Created User[${user.id}]`);
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('create')
|
||||
.description('creates a local user')
|
||||
.action(createUser);
|
||||
|
||||
program
|
||||
.command('delete <userID>')
|
||||
.description('delete a user')
|
||||
|
||||
@@ -12,7 +12,7 @@ class MyPluginComponent extends React.Component {
|
||||
|
||||
<small>
|
||||
To read more about plugins check{' '}
|
||||
<a href="https://coralproject.github.io/talk/plugins-client.html">
|
||||
<a href="https://docs.coralproject.net/talk/plugins-client">
|
||||
our docs and guides!
|
||||
</a>
|
||||
</small>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
};
|
||||
```
|
||||
|
||||
To read more info on how to build client plugins. Please, go to: https://coralproject.github.io/talk/plugins-client.html
|
||||
To read more info on how to build client plugins. Please, go to: https://docs.coralproject.net/talk/plugins-client
|
||||
*/
|
||||
|
||||
import MyPluginComponent from './components/MyPluginComponent';
|
||||
|
||||
+5
-8
@@ -2,6 +2,7 @@
|
||||
require('../services/env');
|
||||
|
||||
const debug = require('debug')('talk:util');
|
||||
const { uniq } = require('lodash');
|
||||
|
||||
const util = (module.exports = {});
|
||||
|
||||
@@ -23,11 +24,7 @@ util.shutdown = (defaultCode = 0, signal = null) => {
|
||||
|
||||
debug(`${util.toshutdown.length} jobs now being called`);
|
||||
|
||||
Promise.all(
|
||||
util.toshutdown
|
||||
.map(func => (func ? func(signal) : null))
|
||||
.filter(func => func)
|
||||
)
|
||||
Promise.all(util.toshutdown.map(func => (func ? func(signal) : null)))
|
||||
.then(() => {
|
||||
debug('Shutdown complete, now exiting');
|
||||
process.exit(defaultCode);
|
||||
@@ -49,14 +46,14 @@ util.onshutdown = jobs => {
|
||||
debug(`${jobs.length} jobs registered to be called during shutdown`);
|
||||
|
||||
// Add the new jobs to shutdown to the object reference.
|
||||
util.toshutdown = util.toshutdown.concat(jobs);
|
||||
util.toshutdown = uniq(util.toshutdown.concat(jobs));
|
||||
};
|
||||
|
||||
// Attach to the SIGTERM + SIGINT handles to ensure a clean shutdown in the
|
||||
// event that we have an external event. SIGUSR2 is called when the app is asked
|
||||
// to be 'killed', same procedure here.
|
||||
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
|
||||
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
|
||||
process.once('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
|
||||
process.once('SIGINT', () => util.shutdown(0, 'SIGINT'));
|
||||
process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
|
||||
@@ -2,10 +2,15 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import Community from 'routes/Community';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import StreamSettings from './routes/Configure/containers/StreamSettings';
|
||||
import ModerationSettings from './routes/Configure/containers/ModerationSettings';
|
||||
import TechSettings from './routes/Configure/containers/TechSettings';
|
||||
|
||||
import { ModerationLayout, Moderation } from 'routes/Moderation';
|
||||
|
||||
import Layout from 'containers/Layout';
|
||||
@@ -15,7 +20,14 @@ const routes = (
|
||||
<Route exact path="/admin/install" component={Install} />
|
||||
<Route path="/admin" component={Layout}>
|
||||
<IndexRedirect to="/admin/moderate" />
|
||||
<Route path="configure" component={Configure} />
|
||||
|
||||
<Route path="configure" component={Configure}>
|
||||
<Route path="stream" component={StreamSettings} />
|
||||
<Route path="moderation" component={ModerationSettings} />
|
||||
<Route path="tech" component={TechSettings} />
|
||||
<IndexRedirect to="stream" />
|
||||
</Route>
|
||||
|
||||
<Route path="stories" component={Stories} />
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
@@ -8,6 +8,10 @@ export const clearPending = () => {
|
||||
return { type: actions.CLEAR_PENDING };
|
||||
};
|
||||
|
||||
export const setActiveSection = section => {
|
||||
return { type: actions.SET_ACTIVE_SECTION, section };
|
||||
export const showSaveDialog = () => {
|
||||
return { type: actions.SHOW_SAVE_DIALOG };
|
||||
};
|
||||
|
||||
export const hideSaveDialog = () => {
|
||||
return { type: actions.HIDE_SAVE_DIALOG };
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ const buildUserHistory = (userState = {}) => {
|
||||
return orderBy(
|
||||
flatten(
|
||||
Object.keys(userState.status)
|
||||
.filter(k => k !== '__typename')
|
||||
.filter(k => !k.startsWith('__'))
|
||||
.map(k => userState.status[k].history)
|
||||
),
|
||||
'created_at',
|
||||
|
||||
@@ -25,37 +25,29 @@ class CommentDetails extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, root, comment, clearHeightCache } = this.props;
|
||||
const { root, comment, clearHeightCache } = this.props;
|
||||
const { showDetail } = this.state;
|
||||
const queryData = {
|
||||
|
||||
const slotPassthrough = {
|
||||
clearHeightCache,
|
||||
root,
|
||||
comment,
|
||||
more: showDetail,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<IfSlotIsNotEmpty
|
||||
queryData={queryData}
|
||||
slot={['adminCommentMoreDetails', 'adminCommentMoreFlagDetails']}
|
||||
passthrough={slotPassthrough}
|
||||
>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>
|
||||
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}
|
||||
</a>
|
||||
</IfSlotIsNotEmpty>
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
more={showDetail}
|
||||
/>
|
||||
<Slot fill="adminCommentDetailArea" passthrough={slotPassthrough} />
|
||||
{showDetail && (
|
||||
<Slot
|
||||
fill="adminCommentMoreDetails"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
<Slot fill="adminCommentMoreDetails" passthrough={slotPassthrough} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -63,7 +55,6 @@ class CommentDetails extends Component {
|
||||
}
|
||||
|
||||
CommentDetails.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
clearHeightCache: PropTypes.func,
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { matchLinks } from '../utils';
|
||||
import memoize from 'lodash/memoize';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
// generate a regulare expression that catches the `phrases`.
|
||||
function generateRegExp(phrases) {
|
||||
const inner = phrases
|
||||
.map(phrase =>
|
||||
phrase
|
||||
.split(/\s+/)
|
||||
.map(word => escapeRegExp(word))
|
||||
.join('[\\s"?!.]+')
|
||||
)
|
||||
.join('|');
|
||||
|
||||
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
|
||||
try {
|
||||
return new RegExp(pattern, 'iu');
|
||||
} catch (_err) {
|
||||
// IE does not support unicode support, so we'll create one without.
|
||||
return new RegExp(pattern, 'i');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
|
||||
function getPhrasesRegexp(suspectWords, bannedWords) {
|
||||
return generateRegExp([...suspectWords, ...bannedWords]);
|
||||
}
|
||||
|
||||
// Memoized version as arguments rarely change.
|
||||
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
|
||||
|
||||
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
|
||||
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
|
||||
const tokens = body.split(regexp);
|
||||
return tokens.map(
|
||||
(token, i) =>
|
||||
i % 3 === 2 ? <mark key={`${keyPrefix}_${i}`}>{token}</mark> : token
|
||||
);
|
||||
}
|
||||
|
||||
// markLinks looks for links inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markLinks(body) {
|
||||
const matches = matchLinks(body);
|
||||
const content = [];
|
||||
let index = 0;
|
||||
if (matches) {
|
||||
matches.forEach((match, i) => {
|
||||
content.push(body.substring(index, match.index));
|
||||
content.push(<mark key={i}>{match.text}</mark>);
|
||||
index = match.lastIndex;
|
||||
});
|
||||
}
|
||||
content.push(body.substring(index));
|
||||
return content;
|
||||
}
|
||||
|
||||
const CommentFormatter = ({
|
||||
body,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
className = 'comment',
|
||||
...rest
|
||||
}) => {
|
||||
// Breaking the body by line break
|
||||
const textbreaks = body.split('\n');
|
||||
|
||||
return (
|
||||
<span className={`${className}-text`} {...rest}>
|
||||
{textbreaks.map((line, i) => {
|
||||
const content = markLinks(line).map((element, index) => {
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
|
||||
return (
|
||||
<span key={i} className={`${className}-line`}>
|
||||
{content}
|
||||
{i !== textbreaks.length - 1 && (
|
||||
<br className={`${className}-linebreak`} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
CommentFormatter.propTypes = {
|
||||
className: PropTypes.string,
|
||||
bannedWords: PropTypes.array,
|
||||
suspectWords: PropTypes.array,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentFormatter;
|
||||
@@ -43,6 +43,9 @@ const CommentLabels = ({
|
||||
comment,
|
||||
comment: { className, status, actions, hasParent },
|
||||
}) => {
|
||||
const slotPassthrough = {
|
||||
comment,
|
||||
};
|
||||
return (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<div className={styles.coreLabels}>
|
||||
@@ -69,7 +72,7 @@ const CommentLabels = ({
|
||||
<Slot
|
||||
className={styles.slot}
|
||||
fill="adminCommentLabels"
|
||||
queryData={{ comment }}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.copy {
|
||||
padding: 20px 0;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './Forbidden.css';
|
||||
|
||||
const Forbidden = () => (
|
||||
<div className={styles.container}>
|
||||
<p className={styles.copy}>
|
||||
This page is for team use only. Please contact an administrator if you
|
||||
want to join this team.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Forbidden;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { matchLinks } from '../utils';
|
||||
import matchLinks from 'coral-framework/utils/matchLinks';
|
||||
|
||||
export default ({ text, children }) => {
|
||||
const hasLinks = !!matchLinks(text);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.layout {
|
||||
margin: 0 auto;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
height: inherit;
|
||||
min-height: calc(100vh - 58px);
|
||||
}
|
||||
@@ -98,7 +98,6 @@ class UserDetail extends React.Component {
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: { me, user, totalComments, rejectedComments },
|
||||
activeTab,
|
||||
@@ -123,6 +122,11 @@ class UserDetail extends React.Component {
|
||||
const banned = isBanned(user);
|
||||
const suspended = isSuspended(user);
|
||||
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
user,
|
||||
};
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
|
||||
<Drawer
|
||||
@@ -244,11 +248,7 @@ class UserDetail extends React.Component {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
queryData={{ root, user }}
|
||||
/>
|
||||
<Slot fill="userProfile" passthrough={slotPassthrough} />
|
||||
|
||||
<hr />
|
||||
|
||||
@@ -301,7 +301,6 @@ class UserDetail extends React.Component {
|
||||
<UserDetailCommentList
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
@@ -320,7 +319,6 @@ class UserDetail extends React.Component {
|
||||
<UserDetailCommentList
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
@@ -368,9 +366,7 @@ UserDetail.propTypes = {
|
||||
bulkReject: PropTypes.func.isRequired,
|
||||
toggleSelectAll: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
data: PropTypes.shape({
|
||||
refetch: PropTypes.func.isRequired,
|
||||
}),
|
||||
data: PropTypes.object,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Link } from 'react-router';
|
||||
import { Icon } from 'coral-ui';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import styles from './UserDetailComment.css';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import AdminCommentContent from 'coral-framework/components/AdminCommentContent';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
@@ -32,13 +32,12 @@ class UserDetailComment extends React.Component {
|
||||
selected,
|
||||
toggleSelect,
|
||||
className,
|
||||
data,
|
||||
root: { settings },
|
||||
} = this.props;
|
||||
|
||||
const queryData = { root, comment };
|
||||
|
||||
const formatterSettings = {
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
comment,
|
||||
suspectWords: settings.wordlist.suspect,
|
||||
bannedWords: settings.wordlist.banned,
|
||||
body: comment.body,
|
||||
@@ -76,7 +75,8 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.story}>
|
||||
Story: {comment.asset.title}
|
||||
{t('common.story')}:{' '}
|
||||
{comment.asset.title ? comment.asset.title : comment.asset.url}
|
||||
{
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>
|
||||
{t('modqueue.moderate')}
|
||||
@@ -88,15 +88,13 @@ class UserDetailComment extends React.Component {
|
||||
<div className={styles.body}>
|
||||
<Slot
|
||||
fill="userDetailCommentContent"
|
||||
data={data}
|
||||
className={cn(
|
||||
styles.commentContent,
|
||||
'talk-admin-user-detail-comment'
|
||||
)}
|
||||
queryData={queryData}
|
||||
slotSize={1}
|
||||
defaultComponent={CommentFormatter}
|
||||
{...formatterSettings}
|
||||
size={1}
|
||||
defaultComponent={AdminCommentContent}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
<a
|
||||
className={styles.external}
|
||||
@@ -109,6 +107,7 @@ class UserDetailComment extends React.Component {
|
||||
<div className={styles.sideActions}>
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
{/* TODO: translate string */}
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
</span>
|
||||
</IfHasLink>
|
||||
@@ -128,7 +127,7 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
<CommentDetails data={data} root={root} comment={comment} />
|
||||
<CommentDetails root={root} comment={comment} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -136,7 +135,6 @@ class UserDetailComment extends React.Component {
|
||||
|
||||
UserDetailComment.propTypes = {
|
||||
selected: PropTypes.bool,
|
||||
data: PropTypes.object,
|
||||
user: PropTypes.object.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
|
||||
@@ -9,7 +9,6 @@ import ApproveButton from './ApproveButton';
|
||||
|
||||
const UserDetailCommentList = props => {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: { user, comments: { nodes, hasNextPage } },
|
||||
acceptComment,
|
||||
@@ -70,7 +69,6 @@ const UserDetailCommentList = props => {
|
||||
key={comment.id}
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
acceptComment={acceptComment}
|
||||
rejectComment={rejectComment}
|
||||
@@ -93,7 +91,6 @@ UserDetailCommentList.propTypes = {
|
||||
root: PropTypes.object.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
|
||||
@@ -2,4 +2,6 @@ const prefix = 'TALK_ADMIN_CONFIGURE';
|
||||
|
||||
export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
|
||||
export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
|
||||
export const SET_ACTIVE_SECTION = `${prefix}_SET_ACTIVE_SECTION`;
|
||||
|
||||
export const SHOW_SAVE_DIALOG = `${prefix}_SHOW_SAVE_DIALOG`;
|
||||
export const HIDE_SAVE_DIALOG = `${prefix}_HIDE_SAVE_DIALOG`;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { logout } from 'coral-framework/actions/auth';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
import PropTypes from 'prop-types';
|
||||
import Forbidden from '../components/Forbidden';
|
||||
|
||||
class LayoutContainer extends React.Component {
|
||||
render() {
|
||||
@@ -47,10 +48,7 @@ class LayoutContainer extends React.Component {
|
||||
} else {
|
||||
return (
|
||||
<Layout {...this.props} handleLogout={logout}>
|
||||
<p>
|
||||
This page is for team use only. Please contact an administrator if
|
||||
you want to join this team.
|
||||
</p>
|
||||
<Forbidden />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,23 @@ const initialState = {
|
||||
canSave: false,
|
||||
pending: {},
|
||||
errors: {},
|
||||
activeSection: 'stream',
|
||||
saveDialog: false,
|
||||
};
|
||||
|
||||
export default function configure(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SHOW_SAVE_DIALOG: {
|
||||
return {
|
||||
...state,
|
||||
saveDialog: true,
|
||||
};
|
||||
}
|
||||
case actions.HIDE_SAVE_DIALOG: {
|
||||
return {
|
||||
...state,
|
||||
saveDialog: false,
|
||||
};
|
||||
}
|
||||
case actions.UPDATE_PENDING: {
|
||||
let next = state;
|
||||
if (action.updater) {
|
||||
@@ -40,11 +52,8 @@ export default function configure(state = initialState, action) {
|
||||
pending: {},
|
||||
canSave: false,
|
||||
};
|
||||
case actions.SET_ACTIVE_SECTION:
|
||||
return {
|
||||
...state,
|
||||
activeSection: action.section,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -130,3 +130,12 @@ th.header:nth-child(2), th.header:nth-child(3) {
|
||||
.loadMore {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.roleDropdown {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.roleOption {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
|
||||
@@ -200,24 +200,31 @@ class People extends React.Component {
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
containerClassName="talk-admin-community-people-dd-role"
|
||||
className={cn(
|
||||
'talk-admin-community-people-dd-role',
|
||||
styles.roleDropdown
|
||||
)}
|
||||
value={user.role}
|
||||
placeholder={t('community.role')}
|
||||
onChange={role => setUserRole(user.id, role)}
|
||||
>
|
||||
<Option
|
||||
className={styles.roleOption}
|
||||
value={COMMENTER}
|
||||
label={t('community.commenter')}
|
||||
/>
|
||||
<Option
|
||||
className={styles.roleOption}
|
||||
value={STAFF}
|
||||
label={t('community.staff')}
|
||||
/>
|
||||
<Option
|
||||
className={styles.roleOption}
|
||||
value={MODERATOR}
|
||||
label={t('community.moderator')}
|
||||
/>
|
||||
<Option
|
||||
className={styles.roleOption}
|
||||
value={ADMIN}
|
||||
label={t('community.admin')}
|
||||
/>
|
||||
|
||||
@@ -1,50 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Button, List, Item } from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import StreamSettings from '../containers/StreamSettings';
|
||||
import ModerationSettings from '../containers/ModerationSettings';
|
||||
import TechSettings from '../containers/TechSettings';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { Button, List, Item } from 'coral-ui';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import styles from './Configure.css';
|
||||
import SaveChangesDialog from './SaveChangesDialog';
|
||||
|
||||
export default class Configure extends Component {
|
||||
getSectionComponent(section) {
|
||||
switch (section) {
|
||||
case 'stream':
|
||||
return StreamSettings;
|
||||
case 'moderation':
|
||||
return ModerationSettings;
|
||||
case 'tech':
|
||||
return TechSettings;
|
||||
}
|
||||
throw new Error(`Unknown section ${section}`);
|
||||
}
|
||||
|
||||
class Configure extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
currentUser,
|
||||
canSave,
|
||||
savePending,
|
||||
setActiveSection,
|
||||
activeSection,
|
||||
} = this.props;
|
||||
const SectionComponent = this.getSectionComponent(activeSection);
|
||||
const { canSave, currentUser, root, savePending, settings } = this.props;
|
||||
|
||||
if (!can(currentUser, 'UPDATE_CONFIG')) {
|
||||
return (
|
||||
<p>
|
||||
You must be an administrator to access config settings. Please find
|
||||
the nearest Admin and ask them to level you up!
|
||||
</p>
|
||||
);
|
||||
return <p>{t('configure.access_message')}</p>;
|
||||
}
|
||||
|
||||
const passProps = {
|
||||
root,
|
||||
settings,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<SaveChangesDialog
|
||||
saveDialog={this.props.saveDialog}
|
||||
hideSaveDialog={this.props.hideSaveDialog}
|
||||
saveChanges={this.props.saveChanges}
|
||||
discardChanges={this.props.discardChanges}
|
||||
/>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={setActiveSection} activeItem={activeSection}>
|
||||
<List
|
||||
onChange={this.props.handleSectionChange}
|
||||
activeItem={this.props.activeSection}
|
||||
>
|
||||
<Item itemId="stream" icon="speaker_notes">
|
||||
{t('configure.stream_settings')}
|
||||
</Item>
|
||||
@@ -74,11 +61,7 @@ export default class Configure extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
<SectionComponent
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
{React.cloneElement(this.props.children, passProps)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -87,11 +70,17 @@ export default class Configure extends Component {
|
||||
|
||||
Configure.propTypes = {
|
||||
savePending: PropTypes.func.isRequired,
|
||||
saveChanges: PropTypes.func.isRequired,
|
||||
discardChanges: PropTypes.func.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
handleSectionChange: PropTypes.func.isRequired,
|
||||
activeSection: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
saveDialog: PropTypes.bool,
|
||||
hideSaveDialog: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Configure;
|
||||
|
||||
@@ -52,7 +52,7 @@ class ModerationSettings extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings, data, root, updatePending, errors } = this.props;
|
||||
const { settings, slotPassthrough } = this.props;
|
||||
|
||||
return (
|
||||
<ConfigurePage title={t('configure.moderation_settings')}>
|
||||
@@ -82,13 +82,7 @@ class ModerationSettings extends React.Component {
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
onChangeWordlist={this.updateWordlist}
|
||||
/>
|
||||
<Slot
|
||||
fill="adminModerationSettings"
|
||||
data={data}
|
||||
queryData={{ root, settings }}
|
||||
updatePending={updatePending}
|
||||
errors={errors}
|
||||
/>
|
||||
<Slot fill="adminModerationSettings" passthrough={slotPassthrough} />
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
@@ -97,9 +91,8 @@ class ModerationSettings extends React.Component {
|
||||
ModerationSettings.propTypes = {
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ModerationSettings;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
.buttonActions {
|
||||
padding-top: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
padding: 25px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
color: #363636;
|
||||
margin-right: 15px;
|
||||
display: inline-block;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Button, Dialog } from 'coral-ui';
|
||||
import styles from './SaveChangesDialog.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const SaveChangesDialog = ({
|
||||
saveDialog,
|
||||
hideSaveDialog,
|
||||
saveChanges,
|
||||
discardChanges,
|
||||
}) => (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-admin-configure-save-dialog')}
|
||||
id="saveDialog"
|
||||
open={saveDialog}
|
||||
onCancel={hideSaveDialog}
|
||||
>
|
||||
<span className={styles.close} onClick={hideSaveDialog}>
|
||||
×
|
||||
</span>
|
||||
<div className={styles.title}>
|
||||
{t('configure.save_changes_dialog.unsaved_changes')}
|
||||
</div>
|
||||
{t('configure.save_changes_dialog.copy')}
|
||||
<div
|
||||
className={cn(
|
||||
styles.buttonActions,
|
||||
'talk-admin-configure-save-dialog-button-actions'
|
||||
)}
|
||||
>
|
||||
<a className={styles.cancel} onClick={hideSaveDialog}>
|
||||
Cancel
|
||||
</a>
|
||||
<Button onClick={discardChanges} className={styles.button}>
|
||||
{t('configure.save_changes_dialog.discard')}
|
||||
</Button>
|
||||
<Button onClick={saveChanges} cStyle="green" className={styles.button}>
|
||||
{t('configure.save_changes_dialog.save_settings')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
SaveChangesDialog.propTypes = {
|
||||
saveDialog: PropTypes.bool.isRequired,
|
||||
hideSaveDialog: PropTypes.func.isRequired,
|
||||
saveChanges: PropTypes.func.isRequired,
|
||||
discardChanges: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SaveChangesDialog;
|
||||
@@ -107,7 +107,7 @@ class StreamSettings extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings, data, root, errors, updatePending } = this.props;
|
||||
const { settings, slotPassthrough, errors } = this.props;
|
||||
|
||||
return (
|
||||
<ConfigurePage title={t('configure.stream_settings')}>
|
||||
@@ -220,13 +220,7 @@ class StreamSettings extends React.Component {
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
{/* the above card should be the last one if at all possible because of z-index issues with the selects */}
|
||||
<Slot
|
||||
fill="adminStreamSettings"
|
||||
data={data}
|
||||
queryData={{ root, settings }}
|
||||
updatePending={updatePending}
|
||||
errors={errors}
|
||||
/>
|
||||
<Slot fill="adminStreamSettings" passthrough={slotPassthrough} />
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
@@ -235,9 +229,8 @@ class StreamSettings extends React.Component {
|
||||
StreamSettings.propTypes = {
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default StreamSettings;
|
||||
|
||||
@@ -34,7 +34,7 @@ class TechSettings extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings, data, root, errors, updatePending } = this.props;
|
||||
const { settings, slotPassthrough } = this.props;
|
||||
return (
|
||||
<ConfigurePage title={t('configure.tech_settings')}>
|
||||
<Domainlist
|
||||
@@ -50,13 +50,7 @@ class TechSettings extends React.Component {
|
||||
onChange={this.updateCustomCssUrl}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
<Slot
|
||||
fill="adminTechSettings"
|
||||
data={data}
|
||||
queryData={{ root, settings }}
|
||||
updatePending={updatePending}
|
||||
errors={errors}
|
||||
/>
|
||||
<Slot fill="adminTechSettings" passthrough={slotPassthrough} />
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
@@ -64,10 +58,9 @@ class TechSettings extends React.Component {
|
||||
|
||||
TechSettings.propTypes = {
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object,
|
||||
};
|
||||
|
||||
export default TechSettings;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
@@ -10,15 +10,70 @@ import { getDefinitionName } from 'coral-framework/utils';
|
||||
import StreamSettings from './StreamSettings';
|
||||
import TechSettings from './TechSettings';
|
||||
import ModerationSettings from './ModerationSettings';
|
||||
import { clearPending, setActiveSection } from '../../../actions/configure';
|
||||
import {
|
||||
clearPending,
|
||||
showSaveDialog,
|
||||
hideSaveDialog,
|
||||
} from '../../../actions/configure';
|
||||
import Configure from '../components/Configure';
|
||||
import { withRouter } from 'react-router';
|
||||
|
||||
class ConfigureContainer extends React.Component {
|
||||
state = { nextRoute: '' };
|
||||
|
||||
class ConfigureContainer extends Component {
|
||||
savePending = async () => {
|
||||
await this.props.updateSettings(this.props.pending);
|
||||
this.props.clearPending();
|
||||
};
|
||||
|
||||
saveChanges = async () => {
|
||||
await this.savePending();
|
||||
this.props.hideSaveDialog();
|
||||
this.gotoNextRoute();
|
||||
};
|
||||
|
||||
discardChanges = async () => {
|
||||
await this.props.clearPending();
|
||||
this.props.hideSaveDialog();
|
||||
this.gotoNextRoute();
|
||||
};
|
||||
|
||||
gotoNextRoute = () => {
|
||||
const { nextRoute } = this.state;
|
||||
if (nextRoute) {
|
||||
this.props.router.push(nextRoute);
|
||||
this.setState({ nextRoute: '' });
|
||||
}
|
||||
};
|
||||
|
||||
handleSectionChange = async section => {
|
||||
const nextRoute = `/admin/configure/${section}`;
|
||||
|
||||
if (this.shouldShowSaveDialog()) {
|
||||
await this.setState({ nextRoute });
|
||||
this.props.showSaveDialog();
|
||||
} else {
|
||||
// Just go to the section
|
||||
this.props.router.push(nextRoute);
|
||||
}
|
||||
};
|
||||
|
||||
shouldShowSaveDialog = () => {
|
||||
return !!Object.keys(this.props.pending).length;
|
||||
};
|
||||
|
||||
routeLeave = ({ pathname }) => {
|
||||
if (this.shouldShowSaveDialog()) {
|
||||
this.setState({ nextRoute: pathname });
|
||||
this.props.showSaveDialog();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.router.setRouteLeaveHook(this.props.route, this.routeLeave);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
@@ -30,15 +85,20 @@ class ConfigureContainer extends Component {
|
||||
|
||||
return (
|
||||
<Configure
|
||||
saveChanges={this.saveChanges}
|
||||
discardChanges={this.discardChanges}
|
||||
saveDialog={this.props.saveDialog}
|
||||
activeSection={this.props.routes[3].path}
|
||||
hideSaveDialog={this.props.hideSaveDialog}
|
||||
canSave={this.props.canSave}
|
||||
currentUser={this.props.currentUser}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.props.mergedSettings}
|
||||
canSave={this.props.canSave}
|
||||
handleSectionChange={this.handleSectionChange}
|
||||
savePending={this.savePending}
|
||||
setActiveSection={this.props.setActiveSection}
|
||||
activeSection={this.props.activeSection}
|
||||
/>
|
||||
>
|
||||
{this.props.children}
|
||||
</Configure>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -75,18 +135,21 @@ const mapStateToProps = state => ({
|
||||
pending: state.configure.pending,
|
||||
canSave: state.configure.canSave,
|
||||
activeSection: state.configure.activeSection,
|
||||
saveDialog: state.configure.saveDialog,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
clearPending,
|
||||
setActiveSection,
|
||||
showSaveDialog,
|
||||
hideSaveDialog,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withUpdateSettings,
|
||||
withConfigureQuery,
|
||||
@@ -94,14 +157,20 @@ export default compose(
|
||||
)(ConfigureContainer);
|
||||
|
||||
ConfigureContainer.propTypes = {
|
||||
activeSection: PropTypes.string,
|
||||
updateSettings: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
showSaveDialog: PropTypes.func.isRequired,
|
||||
hideSaveDialog: PropTypes.func.isRequired,
|
||||
saveDialog: PropTypes.bool.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
pending: PropTypes.object.isRequired,
|
||||
mergedSettings: PropTypes.object.isRequired,
|
||||
activeSection: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
router: PropTypes.object,
|
||||
route: PropTypes.object,
|
||||
routes: PropTypes.array,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import ModerationSettings from '../components/ModerationSettings';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import { updatePending } from '../../../actions/configure';
|
||||
import { mapProps } from 'recompose';
|
||||
|
||||
const slots = ['adminModerationSettings'];
|
||||
|
||||
@@ -41,5 +42,17 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
settings,
|
||||
updatePending,
|
||||
errors,
|
||||
},
|
||||
updatePending,
|
||||
settings,
|
||||
errors,
|
||||
...rest,
|
||||
}))
|
||||
)(ModerationSettings);
|
||||
|
||||
@@ -5,6 +5,7 @@ import StreamSettings from '../components/StreamSettings';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import { updatePending } from '../../../actions/configure';
|
||||
import { mapProps } from 'recompose';
|
||||
|
||||
const slots = ['adminStreamSettings'];
|
||||
|
||||
@@ -42,5 +43,17 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
settings,
|
||||
updatePending,
|
||||
errors,
|
||||
},
|
||||
updatePending,
|
||||
settings,
|
||||
errors,
|
||||
...rest,
|
||||
}))
|
||||
)(StreamSettings);
|
||||
|
||||
@@ -5,6 +5,7 @@ import TechSettings from '../components/TechSettings';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import { updatePending } from '../../../actions/configure';
|
||||
import { mapProps } from 'recompose';
|
||||
|
||||
const slots = ['adminTechSettings'];
|
||||
|
||||
@@ -38,5 +39,17 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
settings,
|
||||
updatePending,
|
||||
errors,
|
||||
},
|
||||
updatePending,
|
||||
settings,
|
||||
errors,
|
||||
...rest,
|
||||
}))
|
||||
)(TechSettings);
|
||||
|
||||
@@ -10,12 +10,14 @@ const InitialStep = () => {
|
||||
return (
|
||||
<div className={cn(styles.step, styles.finalStep, 'talk-install-step-5')}>
|
||||
<p>{t('install.final.description')}</p>
|
||||
<Button raised>
|
||||
<Link to="/admin">{t('install.final.launch')}</Link>
|
||||
</Button>
|
||||
<Button cStyle="black" raised>
|
||||
<a href="http://coralproject.net">{t('install.final.close')}</a>
|
||||
</Button>
|
||||
<Link to="/admin">
|
||||
<Button raised>{t('install.final.launch')}</Button>
|
||||
</Link>
|
||||
<a href="http://coralproject.net">
|
||||
<Button cStyle="black" raised>
|
||||
{t('install.final.close')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import styles from './Comment.css';
|
||||
import CommentLabels from 'coral-admin/src/components/CommentLabels';
|
||||
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import AdminCommentContent from 'coral-framework/components/AdminCommentContent';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
@@ -53,7 +53,6 @@ class Comment extends React.Component {
|
||||
comment,
|
||||
selected,
|
||||
className,
|
||||
data,
|
||||
root,
|
||||
root: { settings },
|
||||
currentAsset,
|
||||
@@ -62,7 +61,6 @@ class Comment extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
const queryData = { root, comment, asset: comment.asset };
|
||||
|
||||
const formatterSettings = {
|
||||
suspectWords: settings.wordlist.suspect,
|
||||
@@ -70,6 +68,13 @@ class Comment extends React.Component {
|
||||
body: comment.body,
|
||||
};
|
||||
|
||||
const slotPassthrough = {
|
||||
clearHeightCache,
|
||||
root,
|
||||
comment,
|
||||
asset: comment.asset,
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
@@ -113,16 +118,15 @@ class Comment extends React.Component {
|
||||
<CommentLabels comment={comment} />
|
||||
<Slot
|
||||
fill="adminCommentInfoBar"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
{t('common.story')}:{' '}
|
||||
{comment.asset.title ? comment.asset.title : comment.asset.url}
|
||||
{!currentAsset && (
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>
|
||||
{t('modqueue.moderate')}
|
||||
@@ -134,13 +138,10 @@ class Comment extends React.Component {
|
||||
<div className={styles.body}>
|
||||
<Slot
|
||||
fill="adminCommentContent"
|
||||
data={data}
|
||||
className={cn(styles.commentContent, 'talk-admin-comment')}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
slotSize={1}
|
||||
defaultComponent={CommentFormatter}
|
||||
{...formatterSettings}
|
||||
size={1}
|
||||
defaultComponent={AdminCommentContent}
|
||||
passthrough={{ ...slotPassthrough, ...formatterSettings }}
|
||||
/>
|
||||
<div className={styles.commentContentFooter}>
|
||||
<a
|
||||
@@ -156,6 +157,7 @@ class Comment extends React.Component {
|
||||
<div className={styles.sideActions}>
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
{/* TODO: translate string */}
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
</span>
|
||||
</IfHasLink>
|
||||
@@ -169,18 +171,12 @@ class Comment extends React.Component {
|
||||
onClick={this.reject}
|
||||
/>
|
||||
</div>
|
||||
<Slot
|
||||
fill="adminSideActions"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
<Slot fill="adminSideActions" passthrough={slotPassthrough} />
|
||||
</div>
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
<CommentDetails
|
||||
data={data}
|
||||
root={root}
|
||||
comment={comment}
|
||||
clearHeightCache={clearHeightCache}
|
||||
@@ -218,7 +214,6 @@ Comment.propTypes = {
|
||||
id: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -126,7 +126,6 @@ class Moderation extends Component {
|
||||
render() {
|
||||
const {
|
||||
root,
|
||||
data,
|
||||
moderation,
|
||||
viewUserDetail,
|
||||
activeTab,
|
||||
@@ -148,6 +147,13 @@ class Moderation extends Component {
|
||||
count: root[`${queue}Count`],
|
||||
}));
|
||||
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
asset,
|
||||
activeTab,
|
||||
handleCommentChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModerationHeader
|
||||
@@ -171,7 +177,6 @@ class Moderation extends Component {
|
||||
/>
|
||||
<ModerationQueue
|
||||
key={`${activeTab}_${this.props.moderation.sortOrder}`}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
currentAsset={asset}
|
||||
comments={comments.nodes}
|
||||
@@ -204,13 +209,7 @@ class Moderation extends Component {
|
||||
closeSearch={this.closeSearch}
|
||||
storySearchChange={this.props.storySearchChange}
|
||||
/>
|
||||
<Slot
|
||||
data={data}
|
||||
queryData={{ root, asset }}
|
||||
activeTab={activeTab}
|
||||
handleCommentChange={handleCommentChange}
|
||||
fill="adminModeration"
|
||||
/>
|
||||
<Slot fill="adminModeration" passthrough={slotPassthrough} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:global(html) {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.list {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -344,7 +344,6 @@ class ModerationQueue extends React.Component {
|
||||
child = (
|
||||
<div style={style}>
|
||||
<Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
comment={comment}
|
||||
dangling={
|
||||
@@ -408,7 +407,6 @@ class ModerationQueue extends React.Component {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
@@ -476,7 +474,6 @@ ModerationQueue.propTypes = {
|
||||
hasNextPage: PropTypes.bool,
|
||||
comments: PropTypes.array,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
@@ -111,14 +111,14 @@ class IndicatorContainer extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const slotPassthrough = {
|
||||
handleCommentChange: this.handleCommentChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Indicator />
|
||||
<Slot
|
||||
data={this.props.data}
|
||||
handleCommentChange={this.handleCommentChange}
|
||||
fill="adminModerationIndicator"
|
||||
/>
|
||||
<Slot fill="adminModerationIndicator" passthrough={slotPassthrough} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,3 +89,12 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.statusDropdown {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.statusDropdownOption {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,20 @@ class Stories extends Component {
|
||||
const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now());
|
||||
return (
|
||||
<Dropdown
|
||||
className={styles.statusDropdown}
|
||||
value={closed}
|
||||
onChange={value => this.props.onStatusChange(value, id)}
|
||||
>
|
||||
<Option value={false} label={t('streams.open')} />
|
||||
<Option value={true} label={t('streams.closed')} />
|
||||
<Option
|
||||
value={false}
|
||||
label={t('streams.open')}
|
||||
className={styles.statusDropdownOption}
|
||||
/>
|
||||
<Option
|
||||
value={true}
|
||||
label={t('streams.closed')}
|
||||
className={styles.statusDropdownOption}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
import LinkifyIt from 'linkify-it';
|
||||
import tlds from 'tlds';
|
||||
const linkify = new LinkifyIt();
|
||||
linkify.tlds(tlds);
|
||||
|
||||
export function matchLinks(text) {
|
||||
return linkify.match(text);
|
||||
}
|
||||
|
||||
export const isPremod = mod => mod === 'PRE';
|
||||
|
||||
export const getModPath = (type = 'all', assetId) =>
|
||||
|
||||
@@ -71,16 +71,19 @@ export const setActiveTab = tab => dispatch => {
|
||||
dispatch({ type: actions.SET_ACTIVE_TAB, tab });
|
||||
};
|
||||
|
||||
// @Deprecated
|
||||
export const addCommentBoxTag = tag => ({
|
||||
type: actions.ADD_COMMENT_BOX_TAG,
|
||||
tag,
|
||||
});
|
||||
|
||||
// @Deprecated
|
||||
export const removeCommentBoxTag = idx => ({
|
||||
type: actions.REMOVE_COMMENT_BOX_TAG,
|
||||
idx,
|
||||
});
|
||||
|
||||
// @Deprecated
|
||||
export const clearCommentBoxTags = () => ({
|
||||
type: actions.CLEAR_COMMENT_BOX_TAGS,
|
||||
});
|
||||
|
||||
@@ -63,6 +63,7 @@ export default class Embed extends React.Component {
|
||||
} = this.props;
|
||||
const hasHighlightedComment = !!commentId;
|
||||
const popupUrl = `login?parentUrl=${encodeURIComponent(parentUrl)}`;
|
||||
const slotPassthrough = { root };
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -84,7 +85,7 @@ export default class Embed extends React.Component {
|
||||
/>
|
||||
</IfSlotIsNotEmpty>
|
||||
|
||||
<Slot data={data} queryData={{ root }} fill="embed" />
|
||||
<Slot passthrough={slotPassthrough} fill="embed" />
|
||||
|
||||
<ExtendableTabPanel
|
||||
className="talk-embed-stream-tab-bar"
|
||||
@@ -94,8 +95,7 @@ export default class Embed extends React.Component {
|
||||
tabSlot="embedStreamTabs"
|
||||
tabSlotPrepend="embedStreamTabsPrepend"
|
||||
tabPaneSlot="embedStreamTabPanes"
|
||||
slotProps={{ data }}
|
||||
queryData={{ root }}
|
||||
slotPassthrough={slotPassthrough}
|
||||
tabs={this.getTabs()}
|
||||
tabPanes={[
|
||||
<TabPane
|
||||
@@ -138,9 +138,5 @@ Embed.propTypes = {
|
||||
commentId: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
activeTab: PropTypes.string,
|
||||
data: PropTypes.shape({
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
refetch: PropTypes.func,
|
||||
}).isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import React from 'react';
|
||||
import ExtendableTabPanel from '../components/ExtendableTabPanel';
|
||||
import { connect } from 'react-redux';
|
||||
import { TabPane } from 'coral-ui';
|
||||
import ExtendableTab from '../components/ExtendableTab';
|
||||
import { getShallowChanges } from 'coral-framework/utils';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withSlotElements } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
|
||||
class ExtendableTabPanelContainer extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.handleFallback();
|
||||
}
|
||||
@@ -20,24 +15,6 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
this.handleFallback(next);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(next) {
|
||||
// Prevent Slot from rerendering when only reduxState has changed and
|
||||
// it does not result in a change of slot children.
|
||||
const changes = getShallowChanges(this.props, next);
|
||||
if (changes.length === 1 && changes[0] === 'reduxState') {
|
||||
const prevKeys = this.getSlotElements(this.props.tabSlot, this.props).map(
|
||||
el => el.key
|
||||
);
|
||||
const nextKeys = this.getSlotElements(next.tabSlot, next).map(
|
||||
el => el.key
|
||||
);
|
||||
return !isEqual(prevKeys, nextKeys);
|
||||
}
|
||||
|
||||
// Prevent Slot from rerendering when no props has shallowly changed.
|
||||
return changes.length !== 0;
|
||||
}
|
||||
|
||||
handleFallback(props = this.props) {
|
||||
if (this.getTabNames(props).indexOf(props.activeTab) === -1) {
|
||||
props.setActiveTab(props.fallbackTab);
|
||||
@@ -48,38 +25,23 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
return this.getTabElements(props).map(el => el.props.tabId);
|
||||
}
|
||||
|
||||
getSlotElements(slot, props = this.props) {
|
||||
const { plugins } = this.context;
|
||||
return plugins.getSlotElements(
|
||||
slot,
|
||||
props.reduxState,
|
||||
props.slotProps,
|
||||
props.queryData
|
||||
);
|
||||
}
|
||||
|
||||
getPluginTabElements(props = this.props) {
|
||||
return this.getSlotTabElements(props.tabSlot);
|
||||
return props.slotElements[0].map(this.createPluginTabFactory(props));
|
||||
}
|
||||
|
||||
getPluginTabElementsPrepend(props = this.props) {
|
||||
return this.getSlotTabElements(props.tabSlotPrepend);
|
||||
return props.slotElements[1].map(this.createPluginTabFactory(props));
|
||||
}
|
||||
|
||||
getSlotTabElements(slot) {
|
||||
return this.getSlotElements(slot).map(el => {
|
||||
return (
|
||||
<ExtendableTab
|
||||
tabId={el.type.talkPluginName}
|
||||
key={el.type.talkPluginName}
|
||||
>
|
||||
{React.cloneElement(el, {
|
||||
active: this.props.activeTab === el.type.talkPluginName,
|
||||
})}
|
||||
</ExtendableTab>
|
||||
);
|
||||
});
|
||||
}
|
||||
createPluginTabFactory = (props = this.props) => el => {
|
||||
return (
|
||||
<ExtendableTab tabId={el.key} key={el.key}>
|
||||
{React.cloneElement(el, {
|
||||
active: props.activeTab === el.key,
|
||||
})}
|
||||
</ExtendableTab>
|
||||
);
|
||||
};
|
||||
|
||||
getTabElements(props = this.props) {
|
||||
const elements = [...this.getPluginTabElementsPrepend(props)];
|
||||
@@ -92,14 +54,16 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
return elements;
|
||||
}
|
||||
|
||||
createPluginTabPane(el) {
|
||||
return (
|
||||
<TabPane tabId={el.key} key={el.key}>
|
||||
{el}
|
||||
</TabPane>
|
||||
);
|
||||
}
|
||||
|
||||
getPluginTabPaneElements(props = this.props) {
|
||||
return this.getSlotElements(props.tabPaneSlot).map(el => {
|
||||
return (
|
||||
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
|
||||
{el}
|
||||
</TabPane>
|
||||
);
|
||||
});
|
||||
return props.slotElements[2].map(this.createPluginTabPane);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -132,15 +96,15 @@ ExtendableTabPanelContainer.propTypes = {
|
||||
tabSlot: PropTypes.string.isRequired,
|
||||
tabSlotPrepend: PropTypes.string.isRequired,
|
||||
tabPaneSlot: PropTypes.string.isRequired,
|
||||
slotProps: PropTypes.object.isRequired,
|
||||
queryData: PropTypes.object,
|
||||
slotPassthrough: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
sub: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
reduxState: state,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(ExtendableTabPanelContainer);
|
||||
export default compose(
|
||||
withSlotElements({
|
||||
slot: props => [props.tabSlot, props.tabSlotPrepend, props.tabPaneSlot],
|
||||
passthroughPropName: 'slotPassthrough',
|
||||
})
|
||||
)(ExtendableTabPanelContainer);
|
||||
|
||||
@@ -8,7 +8,7 @@ const initialState = {
|
||||
errors: {},
|
||||
};
|
||||
|
||||
export default function config(state = initialState, action) {
|
||||
export default function configure(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.UPDATE_PENDING: {
|
||||
let next = state;
|
||||
|
||||
@@ -7,11 +7,7 @@ class Configure extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="talk-embed-stream-configuration-container">
|
||||
<Settings
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
asset={this.props.asset}
|
||||
/>
|
||||
<Settings root={this.props.root} asset={this.props.asset} />
|
||||
<hr />
|
||||
<AssetStatusInfo asset={this.props.asset} />
|
||||
</div>
|
||||
@@ -20,7 +16,6 @@ class Configure extends React.Component {
|
||||
}
|
||||
|
||||
Configure.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,38 +1,15 @@
|
||||
import React from 'react';
|
||||
import QuestionBox from '../../../components/QuestionBox';
|
||||
import { Icon, Spinner } from 'coral-ui';
|
||||
import DefaultQuestionBoxIcon from '../../../components/DefaultQuestionBoxIcon';
|
||||
import cn from 'classnames';
|
||||
import styles from './QuestionBoxBuilder.css';
|
||||
import { Icon } from 'coral-ui';
|
||||
import MarkdownEditor from 'coral-framework/components/MarkdownEditor';
|
||||
|
||||
const DefaultIcon = <DefaultQuestionBoxIcon className={styles.defaultIcon} />;
|
||||
|
||||
const icons = [{ default: DefaultIcon }, 'forum', 'build', 'format_quote'];
|
||||
|
||||
class QuestionBoxBuilder extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.loadEditor();
|
||||
}
|
||||
|
||||
async loadEditor() {
|
||||
const {
|
||||
default: MarkdownEditor,
|
||||
} = await import('coral-framework/components/MarkdownEditor');
|
||||
|
||||
return this.setState({
|
||||
loading: false,
|
||||
MarkdownEditor,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
questionBoxIcon,
|
||||
@@ -40,11 +17,6 @@ class QuestionBoxBuilder extends React.Component {
|
||||
onContentChange,
|
||||
onIconChange,
|
||||
} = this.props;
|
||||
const { loading, MarkdownEditor } = this.state;
|
||||
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
|
||||
@@ -25,9 +25,9 @@ class Settings extends React.Component {
|
||||
onQuestionBoxContentChange,
|
||||
canSave,
|
||||
onApply,
|
||||
slotProps,
|
||||
queryData,
|
||||
slotPassthrough,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.container}>
|
||||
@@ -77,7 +77,7 @@ class Settings extends React.Component {
|
||||
</div>
|
||||
)}
|
||||
</Configuration>
|
||||
<Slot fill="streamSettings" queryData={queryData} {...slotProps} />
|
||||
<Slot fill="streamSettings" passthrough={slotPassthrough} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -85,8 +85,7 @@ class Settings extends React.Component {
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
queryData: PropTypes.object.isRequired,
|
||||
slotProps: PropTypes.object.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
onToggleModeration: PropTypes.func.isRequired,
|
||||
|
||||
@@ -13,13 +13,7 @@ class ConfigureContainer extends React.Component {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Configure
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
asset={this.props.asset}
|
||||
/>
|
||||
);
|
||||
return <Configure root={this.props.root} asset={this.props.asset} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,17 +57,24 @@ class SettingsContainer extends React.Component {
|
||||
const {
|
||||
mergedSettings,
|
||||
canSave,
|
||||
data,
|
||||
root,
|
||||
asset,
|
||||
errors,
|
||||
updatePending,
|
||||
} = this.props;
|
||||
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
asset,
|
||||
settings: mergedSettings,
|
||||
updatePending,
|
||||
errors,
|
||||
};
|
||||
|
||||
return (
|
||||
<Settings
|
||||
settings={mergedSettings}
|
||||
queryData={{ root, asset, settings: mergedSettings }}
|
||||
slotProps={{ data, updatePending, errors }}
|
||||
slotPassthrough={slotPassthrough}
|
||||
savePending={this.savePending}
|
||||
onToggleModeration={this.toggleModeration}
|
||||
onTogglePremodLinks={this.togglePremodLinks}
|
||||
@@ -82,7 +89,6 @@ class SettingsContainer extends React.Component {
|
||||
}
|
||||
|
||||
SettingsContainer.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
pending: PropTypes.object.isRequired,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.root {
|
||||
text-align: center;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 64px;
|
||||
color: #3a4a52;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 0px;
|
||||
font-size: 14px;
|
||||
color: #7d8285;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import styles from './BlankCommentHistory.css';
|
||||
import { Icon } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export default () => (
|
||||
<section className={cn(styles.root, 'talk-my-profile-comment-history-blank')}>
|
||||
<Icon name="chat" className={styles.icon} />
|
||||
<h1 className={styles.title}>{t('comment_history_blank.title')}</h1>
|
||||
<p className={styles.info}>{t('comment_history_blank.info')}</p>
|
||||
</section>
|
||||
);
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
.main {
|
||||
min-width: 70%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
|
||||
@@ -22,9 +22,9 @@ class Comment extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { comment, data, root } = this.props;
|
||||
const { comment, root } = this.props;
|
||||
const reactionCount = getTotalReactionsCount(comment.action_summaries);
|
||||
const queryData = { root, comment, asset: comment.asset };
|
||||
const slotPassthrough = { root, comment, asset: comment.asset };
|
||||
|
||||
return (
|
||||
<div className={styles.myComment}>
|
||||
@@ -33,8 +33,8 @@ class Comment extends React.Component {
|
||||
fill="commentContent"
|
||||
defaultComponent={CommentContent}
|
||||
className={cn(styles.commentBody, 'my-comment-body')}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
size={1}
|
||||
/>
|
||||
<div className={cn(styles.commentSummary, 'comment-summary')}>
|
||||
<span
|
||||
@@ -98,9 +98,10 @@ class Comment extends React.Component {
|
||||
fill="historyCommentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-history-comment-published-date'}
|
||||
created_at={comment.created_at}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
passthrough={{
|
||||
created_at: comment.created_at,
|
||||
...slotPassthrough,
|
||||
}}
|
||||
inline
|
||||
/>
|
||||
</li>
|
||||
@@ -114,7 +115,6 @@ class Comment extends React.Component {
|
||||
Comment.propTypes = {
|
||||
comment: PropTypes.object.isRequired,
|
||||
navigate: PropTypes.func.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Comment from '../containers/Comment';
|
||||
import LoadMore from './LoadMore';
|
||||
import BlankCommentHistory from './BlankCommentHistory';
|
||||
|
||||
class CommentHistory extends React.Component {
|
||||
state = {
|
||||
@@ -21,7 +22,10 @@ class CommentHistory extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigate, comments, data, root } = this.props;
|
||||
const { navigate, comments, root } = this.props;
|
||||
if (!comments.nodes.length) {
|
||||
return <BlankCommentHistory />;
|
||||
}
|
||||
return (
|
||||
<div className="talk-my-profile-comment-history">
|
||||
<div className="commentHistory__list">
|
||||
@@ -29,7 +33,6 @@ class CommentHistory extends React.Component {
|
||||
return (
|
||||
<Comment
|
||||
key={i}
|
||||
data={data}
|
||||
root={root}
|
||||
comment={comment}
|
||||
navigate={navigate}
|
||||
@@ -52,7 +55,6 @@ CommentHistory.propTypes = {
|
||||
comments: PropTypes.object.isRequired,
|
||||
loadMore: PropTypes.func,
|
||||
navigate: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,57 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentHistory from '../containers/CommentHistory';
|
||||
import ExtendableTabPanel from '../../../containers/ExtendableTabPanel';
|
||||
import { Tab, TabPane } from 'coral-ui';
|
||||
import styles from './Profile.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import TabPanel from '../containers/TabPanel';
|
||||
|
||||
const Profile = ({
|
||||
username,
|
||||
emailAddress,
|
||||
data,
|
||||
root,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
}) => (
|
||||
<div className="talk-my-profile talk-profile-container">
|
||||
<div className={styles.userInfo}>
|
||||
<h2 className={styles.username}>{username}</h2>
|
||||
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
|
||||
const Profile = ({ username, emailAddress, root, slotPassthrough }) => {
|
||||
return (
|
||||
<div className="talk-my-profile talk-profile-container">
|
||||
<div className={styles.userInfo}>
|
||||
<h2 className={styles.username}>{username}</h2>
|
||||
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
|
||||
</div>
|
||||
<Slot fill="profileSections" passthrough={slotPassthrough} />
|
||||
<TabPanel root={root} slotPassthrough={slotPassthrough} />
|
||||
</div>
|
||||
<Slot fill="profileSections" data={data} queryData={{ root }} />
|
||||
<ExtendableTabPanel
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
fallbackTab="comments"
|
||||
tabSlot="profileTabs"
|
||||
tabSlotPrepend="profileTabsPrepend"
|
||||
tabPaneSlot="profileTabPanes"
|
||||
slotProps={{ data }}
|
||||
queryData={{ root }}
|
||||
tabs={[
|
||||
<Tab key="comments" tabId="comments">
|
||||
{t('framework.my_comments')}
|
||||
</Tab>,
|
||||
]}
|
||||
tabPanes={[
|
||||
<TabPane key="comments" tabId="comments">
|
||||
<CommentHistory data={data} root={root} />
|
||||
</TabPane>,
|
||||
]}
|
||||
sub
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
Profile.propTypes = {
|
||||
username: PropTypes.string,
|
||||
emailAddress: PropTypes.string,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
setActiveTab: PropTypes.func.isRequired,
|
||||
slotPassthrough: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
|
||||
class Settings extends React.Component {
|
||||
render() {
|
||||
const { root } = this.props;
|
||||
const slotPassthrough = { root };
|
||||
return (
|
||||
<div>
|
||||
<Slot fill="profileSettings" passthrough={slotPassthrough} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CommentHistory from '../containers/CommentHistory';
|
||||
import ExtendableTabPanel from '../../../containers/ExtendableTabPanel';
|
||||
import { Tab, TabPane } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import Settings from '../containers/Settings';
|
||||
|
||||
const TabPanel = ({
|
||||
root,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
showSettingsTab,
|
||||
slotPassthrough,
|
||||
}) => {
|
||||
const tabs = [
|
||||
<Tab key="comments" tabId="comments">
|
||||
{t('framework.my_comments')}
|
||||
</Tab>,
|
||||
];
|
||||
|
||||
if (showSettingsTab) {
|
||||
tabs.push(
|
||||
<Tab key="settings" tabId="settings">
|
||||
{t('profile_settings')}
|
||||
</Tab>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExtendableTabPanel
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
fallbackTab="comments"
|
||||
tabSlot="profileTabs"
|
||||
tabSlotPrepend="profileTabsPrepend"
|
||||
tabPaneSlot="profileTabPanes"
|
||||
slotPassthrough={slotPassthrough}
|
||||
tabs={tabs}
|
||||
tabPanes={[
|
||||
<TabPane key="comments" tabId="comments">
|
||||
<CommentHistory root={root} />
|
||||
</TabPane>,
|
||||
<TabPane key="settings" tabId="settings">
|
||||
<Settings root={root} />
|
||||
</TabPane>,
|
||||
]}
|
||||
sub
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TabPanel.propTypes = {
|
||||
root: PropTypes.object,
|
||||
slotPassthrough: PropTypes.object,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
setActiveTab: PropTypes.func.isRequired,
|
||||
showSettingsTab: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default TabPanel;
|
||||
@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import CommentHistory from '../components/CommentHistory';
|
||||
import Comment from './Comment';
|
||||
import { withFragments } from 'coral-framework/hocs';
|
||||
import { withFragments, withFetchMore } from 'coral-framework/hocs';
|
||||
|
||||
import { appendNewNodes } from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
@@ -16,7 +16,7 @@ class CommentHistoryContainer extends Component {
|
||||
};
|
||||
|
||||
loadMore = () => {
|
||||
return this.props.data.fetchMore({
|
||||
return this.props.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
limit: 5,
|
||||
@@ -43,7 +43,6 @@ class CommentHistoryContainer extends Component {
|
||||
return (
|
||||
<CommentHistory
|
||||
comments={this.props.root.me.comments}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
loadMore={this.loadMore}
|
||||
navigate={this.navigate}
|
||||
@@ -57,8 +56,8 @@ CommentHistoryContainer.contextTypes = {
|
||||
};
|
||||
|
||||
CommentHistoryContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
fetchMore: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
@@ -99,5 +98,6 @@ const mapStateToProps = state => ({
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, null),
|
||||
withCommentHistoryFragments
|
||||
withCommentHistoryFragments,
|
||||
withFetchMore
|
||||
)(CommentHistoryContainer);
|
||||
|
||||
@@ -7,11 +7,10 @@ import { withQuery } from 'coral-framework/hocs';
|
||||
import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import Profile from '../components/Profile';
|
||||
import CommentHistory from './CommentHistory';
|
||||
import TabPanel from './TabPanel';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
|
||||
import { setActiveTab } from '../../../actions/profile';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
class ProfileContainer extends Component {
|
||||
@@ -23,7 +22,7 @@ class ProfileContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentUser, showSignInDialog, root, data } = this.props;
|
||||
const { currentUser, showSignInDialog, root } = this.props;
|
||||
const { me } = this.props.root;
|
||||
const loading = this.props.data.loading;
|
||||
|
||||
@@ -41,15 +40,14 @@ class ProfileContainer extends Component {
|
||||
|
||||
const localProfile = currentUser.profiles.find(p => p.provider === 'local');
|
||||
const emailAddress = localProfile && localProfile.id;
|
||||
const slotPassthrough = { root };
|
||||
|
||||
return (
|
||||
<Profile
|
||||
username={me.username}
|
||||
emailAddress={emailAddress}
|
||||
data={data}
|
||||
root={root}
|
||||
activeTab={this.props.activeTab}
|
||||
setActiveTab={this.props.setActiveTab}
|
||||
slotPassthrough={slotPassthrough}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -60,16 +58,9 @@ ProfileContainer.propTypes = {
|
||||
root: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showSignInDialog: PropTypes.func,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
setActiveTab: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const slots = [
|
||||
'profileSections',
|
||||
'profileTabs',
|
||||
'profileTabsPrepend',
|
||||
'profileTabPanes',
|
||||
];
|
||||
const slots = ['profileSections'];
|
||||
|
||||
const withProfileQuery = withQuery(
|
||||
gql`
|
||||
@@ -78,10 +69,10 @@ const withProfileQuery = withQuery(
|
||||
id
|
||||
username
|
||||
}
|
||||
...${getDefinitionName(CommentHistory.fragments.root)}
|
||||
...${getDefinitionName(TabPanel.fragments.root)}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
${CommentHistory.fragments.root}
|
||||
${TabPanel.fragments.root}
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
@@ -92,11 +83,10 @@ const withProfileQuery = withQuery(
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
currentUser: state.auth.user,
|
||||
activeTab: state.profile.activeTab,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ showSignInDialog, setActiveTab }, dispatch);
|
||||
bindActionCreators({ showSignInDialog }, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import Settings from '../components/Settings';
|
||||
import { withFragments } from 'coral-framework/hocs';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
const slots = ['profileSettings'];
|
||||
|
||||
class SettingsContainer extends React.Component {
|
||||
render() {
|
||||
return <Settings {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkEmbedStream_ProfileSettings_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
export default enhance(SettingsContainer);
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { withSlotElements, withFragments } from 'coral-framework/hocs';
|
||||
import Settings from './Settings';
|
||||
import CommentHistory from './CommentHistory';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
import TabPanel from '../components/TabPanel';
|
||||
import { setActiveTab } from '../../../actions/profile';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
class TabPanelContainer extends Component {
|
||||
render() {
|
||||
return (
|
||||
<TabPanel
|
||||
root={this.props.root}
|
||||
slotPassthrough={this.props.slotPassthrough}
|
||||
activeTab={this.props.activeTab}
|
||||
setActiveTab={this.props.setActiveTab}
|
||||
showSettingsTab={this.props.profileSettingsSlotElements.length > 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabPanelContainer.propTypes = {
|
||||
root: PropTypes.object,
|
||||
slotPassthrough: PropTypes.object,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
setActiveTab: PropTypes.func.isRequired,
|
||||
profileSettingsSlotElements: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
const slots = ['profileTabs', 'profileTabsPrepend', 'profileTabPanes'];
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
activeTab: state.profile.activeTab,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ setActiveTab }, dispatch);
|
||||
|
||||
export default compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkEmbedStream_ProfileTabPanel_root on RootQuery {
|
||||
__typename
|
||||
...${getDefinitionName(CommentHistory.fragments.root)}
|
||||
...${getDefinitionName(Settings.fragments.root)}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
${CommentHistory.fragments.root}
|
||||
${Settings.fragments.root}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSlotElements({
|
||||
slot: 'profileSettings',
|
||||
propName: 'profileSettingsSlotElements',
|
||||
passthroughPropName: 'slotPassthrough',
|
||||
})
|
||||
)(TabPanelContainer);
|
||||
@@ -126,7 +126,6 @@ class AllCommentsPane extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
comments,
|
||||
commentClassNames,
|
||||
@@ -164,7 +163,6 @@ class AllCommentsPane extends React.Component {
|
||||
return (
|
||||
<Comment
|
||||
commentClassNames={commentClassNames}
|
||||
data={data}
|
||||
root={root}
|
||||
disableReply={disableReply}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
@@ -205,7 +203,6 @@ class AllCommentsPane extends React.Component {
|
||||
}
|
||||
|
||||
AllCommentsPane.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
comments: PropTypes.object,
|
||||
commentClassNames: PropTypes.array,
|
||||
|
||||
@@ -16,7 +16,7 @@ import mapValues from 'lodash/mapValues';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import LoadMore from './LoadMore';
|
||||
import { getEditableUntilDate } from './util';
|
||||
import { getEditableUntilDate } from '../util';
|
||||
import { findCommentWithId } from '../../../graphql/utils';
|
||||
import CommentContent from 'coral-framework/components/CommentContent';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
@@ -181,7 +181,6 @@ export default class Comment extends React.Component {
|
||||
}),
|
||||
charCountEnable: PropTypes.bool.isRequired,
|
||||
maxCharCount: PropTypes.number,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
loadMore: PropTypes.func,
|
||||
postDontAgree: PropTypes.func,
|
||||
@@ -417,7 +416,6 @@ export default class Comment extends React.Component {
|
||||
{view.map(reply => {
|
||||
return (
|
||||
<CommentContainer
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
disableReply={disableReply}
|
||||
@@ -487,7 +485,6 @@ export default class Comment extends React.Component {
|
||||
renderComment() {
|
||||
const {
|
||||
asset,
|
||||
data,
|
||||
root,
|
||||
depth,
|
||||
comment,
|
||||
@@ -535,12 +532,8 @@ export default class Comment extends React.Component {
|
||||
);
|
||||
|
||||
// props that are passed down the slots.
|
||||
const slotProps = {
|
||||
data,
|
||||
const slotPassthrough = {
|
||||
depth,
|
||||
};
|
||||
|
||||
const queryData = {
|
||||
root,
|
||||
asset,
|
||||
comment,
|
||||
@@ -551,8 +544,7 @@ export default class Comment extends React.Component {
|
||||
<Slot
|
||||
className={cn(styles.commentAvatar, 'talk-stream-comment-avatar')}
|
||||
fill="commentAvatar"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
inline
|
||||
/>
|
||||
|
||||
@@ -573,8 +565,7 @@ export default class Comment extends React.Component {
|
||||
className={cn(styles.username, 'talk-stream-comment-user-name')}
|
||||
fill="commentAuthorName"
|
||||
defaultComponent={CommentAuthorName}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -591,8 +582,7 @@ export default class Comment extends React.Component {
|
||||
'talk-stream-comment-author-tags'
|
||||
)}
|
||||
fill="commentAuthorTags"
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
passthrough={slotPassthrough}
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
@@ -607,9 +597,10 @@ export default class Comment extends React.Component {
|
||||
fill="commentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-stream-comment-published-date'}
|
||||
created_at={comment.created_at}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
passthrough={{
|
||||
created_at: comment.created_at,
|
||||
...slotPassthrough,
|
||||
}}
|
||||
/>
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
@@ -624,8 +615,7 @@ export default class Comment extends React.Component {
|
||||
<Slot
|
||||
className={styles.commentInfoBar}
|
||||
fill="commentInfoBar"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
|
||||
{isActive &&
|
||||
@@ -665,9 +655,8 @@ export default class Comment extends React.Component {
|
||||
fill="commentContent"
|
||||
className="talk-stream-comment-content"
|
||||
defaultComponent={CommentContent}
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
slotSize={1}
|
||||
size={1}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -678,8 +667,7 @@ export default class Comment extends React.Component {
|
||||
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
inline
|
||||
/>
|
||||
|
||||
@@ -696,9 +684,7 @@ export default class Comment extends React.Component {
|
||||
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
|
||||
<Slot
|
||||
fill="commentActions"
|
||||
wrapperComponent={ActionButton}
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
inline
|
||||
/>
|
||||
<ActionButton>
|
||||
|
||||
@@ -18,14 +18,8 @@ class CommentForm extends React.Component {
|
||||
charCountEnable: PropTypes.bool.isRequired,
|
||||
maxCharCount: PropTypes.number,
|
||||
|
||||
// DOM ID for form input that edits comment body
|
||||
bodyInputId: PropTypes.string,
|
||||
|
||||
// screen reader label for input that edits comment body
|
||||
bodyLabel: PropTypes.string,
|
||||
|
||||
// Placeholder for input that edits comment body
|
||||
bodyPlaceholder: PropTypes.string,
|
||||
// Unique identifier for this form
|
||||
id: PropTypes.string,
|
||||
|
||||
// render at start of button container (useful for extra buttons)
|
||||
buttonContainerStart: PropTypes.node,
|
||||
@@ -37,15 +31,15 @@ class CommentForm extends React.Component {
|
||||
submitButtonCStyle: PropTypes.string,
|
||||
|
||||
// return whether the submit button should be enabled for the provided
|
||||
// comment ({ body }) (for reasons other than charCount)
|
||||
// input (for reasons other than charCount)
|
||||
submitEnabled: PropTypes.func,
|
||||
|
||||
// className to add to buttons
|
||||
submitButtonClassName: PropTypes.string,
|
||||
cancelButtonClassName: PropTypes.string,
|
||||
|
||||
body: PropTypes.string.isRequired,
|
||||
onBodyChange: PropTypes.func.isRequired,
|
||||
input: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
state: PropTypes.string,
|
||||
@@ -53,13 +47,12 @@ class CommentForm extends React.Component {
|
||||
registerHook: PropTypes.func,
|
||||
unregisterHook: PropTypes.func,
|
||||
isReply: PropTypes.bool,
|
||||
isEdit: PropTypes.bool,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object,
|
||||
};
|
||||
static get defaultProps() {
|
||||
return {
|
||||
bodyLabel: t('comment_box.comment'),
|
||||
bodyPlaceholder: t('comment_box.comment'),
|
||||
submitText: t('comment_box.post'),
|
||||
submitButtonCStyle: 'darkGrey',
|
||||
submitEnabled: () => true,
|
||||
@@ -90,20 +83,20 @@ class CommentForm extends React.Component {
|
||||
cancelButtonClassName,
|
||||
submitButtonClassName,
|
||||
charCountEnable,
|
||||
body,
|
||||
input,
|
||||
loadingState,
|
||||
comment,
|
||||
root,
|
||||
} = this.props;
|
||||
|
||||
const length = body.length;
|
||||
const length = input.body.length;
|
||||
const isRespectingMaxCount = length =>
|
||||
charCountEnable && maxCharCount && length > maxCharCount;
|
||||
const disableSubmitButton =
|
||||
!length ||
|
||||
body.trim().length === 0 ||
|
||||
input.body.trim().length === 0 ||
|
||||
isRespectingMaxCount(length) ||
|
||||
!submitEnabled({ body }) ||
|
||||
!submitEnabled(input) ||
|
||||
loadingState === 'loading';
|
||||
const disableCancelButton = loadingState === 'loading';
|
||||
const disableTextArea = loadingState === 'loading';
|
||||
@@ -113,17 +106,16 @@ class CommentForm extends React.Component {
|
||||
<DraftArea
|
||||
root={root}
|
||||
comment={comment}
|
||||
id={this.props.bodyInputId}
|
||||
label={this.props.bodyLabel}
|
||||
value={body}
|
||||
placeholder={this.props.bodyPlaceholder}
|
||||
onChange={this.props.onBodyChange}
|
||||
id={this.props.id}
|
||||
input={input}
|
||||
onInputChange={this.props.onInputChange}
|
||||
disabled={disableTextArea}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
registerHook={this.props.registerHook}
|
||||
unregisterHook={this.props.unregisterHook}
|
||||
isReply={this.props.isReply}
|
||||
isEdit={this.props.isEdit}
|
||||
/>
|
||||
<div className={cn(styles.buttonContainer, `${name}-button-container`)}>
|
||||
{this.props.buttonContainerStart}
|
||||
|
||||
@@ -13,17 +13,17 @@ import styles from './DraftArea.css';
|
||||
*/
|
||||
export default class DraftArea extends React.Component {
|
||||
renderCharCount() {
|
||||
const { value, maxCharCount } = this.props;
|
||||
const { input, maxCharCount } = this.props;
|
||||
|
||||
const className = cn(
|
||||
styles.charCount,
|
||||
'talk-plugin-commentbox-char-count',
|
||||
{
|
||||
[`${styles.charMax} talk-plugin-commentbox-char-max`]:
|
||||
value.length > maxCharCount,
|
||||
input.body.length > maxCharCount,
|
||||
}
|
||||
);
|
||||
const remaining = maxCharCount - value.length;
|
||||
const remaining = maxCharCount - input.body.length;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@@ -32,48 +32,61 @@ export default class DraftArea extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
if (this.props.isEdit) {
|
||||
return t('edit_comment.body_input_label');
|
||||
}
|
||||
return this.props.isReply ? t('comment_box.reply') : t('comment.comment');
|
||||
}
|
||||
|
||||
getPlaceholder() {
|
||||
if (this.props.isEdit) {
|
||||
return '';
|
||||
}
|
||||
return this.getLabel();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
placeholder,
|
||||
input,
|
||||
id,
|
||||
disabled,
|
||||
rows,
|
||||
label,
|
||||
charCountEnable,
|
||||
maxCharCount,
|
||||
onChange,
|
||||
queryData,
|
||||
onInputChange,
|
||||
isReply,
|
||||
isEdit,
|
||||
registerHook,
|
||||
unregisterHook,
|
||||
root,
|
||||
comment,
|
||||
} = this.props;
|
||||
|
||||
const tASettings = {
|
||||
value,
|
||||
placeholder,
|
||||
id,
|
||||
onChange,
|
||||
rows,
|
||||
disabled,
|
||||
isReply,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id={id}>
|
||||
<div
|
||||
className={cn(styles.container, 'talk-plugin-commentbox-container')}
|
||||
>
|
||||
<label htmlFor={id} className="screen-reader-text" aria-hidden={true}>
|
||||
{label}
|
||||
</label>
|
||||
<Slot
|
||||
fill="draftArea"
|
||||
defaultComponent={DraftAreaContent}
|
||||
className={styles.content}
|
||||
queryData={queryData}
|
||||
registerHook={this.props.registerHook}
|
||||
unregisterHook={this.props.unregisterHook}
|
||||
{...tASettings}
|
||||
passthrough={{
|
||||
id,
|
||||
root,
|
||||
comment,
|
||||
registerHook,
|
||||
unregisterHook,
|
||||
input,
|
||||
onInputChange,
|
||||
disabled,
|
||||
isReply,
|
||||
isEdit,
|
||||
placeholder: this.getPlaceholder(),
|
||||
label: this.getLabel(),
|
||||
}}
|
||||
/>
|
||||
{/* Is this slot here legitimate? (kiwi) */}
|
||||
<Slot fill="commentInputArea" />
|
||||
</div>
|
||||
{charCountEnable && maxCharCount > 0 && this.renderCharCount()}
|
||||
@@ -82,22 +95,17 @@ export default class DraftArea extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
DraftArea.defaultProps = {
|
||||
rows: 3,
|
||||
};
|
||||
|
||||
DraftArea.propTypes = {
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
input: PropTypes.object,
|
||||
onInputChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
rows: PropTypes.number,
|
||||
queryData: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object,
|
||||
registerHook: PropTypes.func,
|
||||
unregisterHook: PropTypes.func,
|
||||
isReply: PropTypes.bool,
|
||||
isEdit: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -3,36 +3,49 @@ import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './DraftAreaContent.css';
|
||||
|
||||
const DraftAreaContent = ({
|
||||
value,
|
||||
placeholder,
|
||||
id,
|
||||
onChange,
|
||||
rows,
|
||||
disabled,
|
||||
}) => (
|
||||
<textarea
|
||||
className={cn(styles.content, 'talk-plugin-commentbox-textarea')}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
id={id}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
rows={rows}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
DraftAreaContent.defaultProps = {
|
||||
rows: 3,
|
||||
};
|
||||
class DraftAreaContent extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
input,
|
||||
id,
|
||||
onInputChange,
|
||||
disabled,
|
||||
label,
|
||||
placeholder,
|
||||
} = this.props;
|
||||
const inputId = `${id}-textarea`;
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="screen-reader-text"
|
||||
aria-hidden={true}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<textarea
|
||||
id={inputId}
|
||||
className={cn(styles.content, 'talk-plugin-commentbox-textarea')}
|
||||
value={input.body}
|
||||
placeholder={placeholder}
|
||||
onChange={e => onInputChange({ body: e.target.value })}
|
||||
rows={3}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DraftAreaContent.propTypes = {
|
||||
id: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
input: PropTypes.object,
|
||||
placeholder: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
onInputChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
rows: PropTypes.number,
|
||||
isEdit: PropTypes.bool,
|
||||
isReply: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default DraftAreaContent;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { notifyForNewCommentStatus } from '../helpers';
|
||||
import CommentForm from '../containers/CommentForm';
|
||||
import styles from './Comment.css';
|
||||
import { CountdownSeconds } from './CountdownSeconds';
|
||||
import { getEditableUntilDate } from './util';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
|
||||
import { Icon } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -15,187 +12,83 @@ import t from 'coral-framework/services/i18n';
|
||||
* Renders a Comment's body in such a way that the end-user can edit it and save changes
|
||||
*/
|
||||
class EditableCommentContent extends React.Component {
|
||||
static propTypes = {
|
||||
// show notification to the user (e.g. for errors)
|
||||
notify: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
// comment that is being edited
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
editing: PropTypes.shape({
|
||||
edited: PropTypes.bool,
|
||||
|
||||
// ISO8601
|
||||
editableUntil: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
|
||||
// logged in user
|
||||
currentUser: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}),
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
|
||||
// edit a comment, passed {{ body }}
|
||||
editComment: PropTypes.func,
|
||||
|
||||
// called when editing should be stopped
|
||||
stopEditing: PropTypes.func,
|
||||
};
|
||||
|
||||
unmounted = false;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.editWindowExpiryTimeout = null;
|
||||
this.state = {
|
||||
body: props.comment.body,
|
||||
loadingState: '',
|
||||
// data: {@object} contains data that might be useful for plugins, metadata, etc
|
||||
data: {},
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
const editableUntil = getEditableUntilDate(this.props.comment);
|
||||
const now = new Date();
|
||||
const editWindowRemainingMs = editableUntil && editableUntil - now;
|
||||
if (editWindowRemainingMs > 0) {
|
||||
this.editWindowExpiryTimeout = setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
}, editWindowRemainingMs);
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
if (this.editWindowExpiryTimeout) {
|
||||
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
handleBodyChange = (body, data) => {
|
||||
this.setState(state => ({
|
||||
body,
|
||||
data: {
|
||||
...state.data,
|
||||
...data,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
handleSubmit = async () => {
|
||||
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
|
||||
this.props.notify('error', t('error.NOT_AUTHORIZED'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loadingState: 'loading' });
|
||||
|
||||
const { editComment, stopEditing } = this.props;
|
||||
if (typeof editComment !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
let input = {
|
||||
body: this.state.body,
|
||||
...this.state.data,
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await editComment(input);
|
||||
if (!this.unmounted) {
|
||||
this.setState({ loadingState: 'success' });
|
||||
}
|
||||
const status = response.data.editComment.comment.status;
|
||||
notifyForNewCommentStatus(this.props.notify, status);
|
||||
if (typeof stopEditing === 'function') {
|
||||
stopEditing();
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({ loadingState: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
getEditableUntil = (props = this.props) => {
|
||||
return getEditableUntilDate(props.comment);
|
||||
};
|
||||
|
||||
isEditWindowExpired = (props = this.props) => {
|
||||
return this.getEditableUntil(props) - new Date() < 0;
|
||||
};
|
||||
|
||||
isSubmitEnabled = comment => {
|
||||
// should be disabled if user hasn't actually changed their
|
||||
// original comment
|
||||
renderButtonContainerStart() {
|
||||
return (
|
||||
comment.body !== this.props.comment.body && !this.isEditWindowExpired()
|
||||
<div className={styles.buttonContainerLeft}>
|
||||
<span className={styles.editWindowRemaining}>
|
||||
{this.props.editWindowExpired ? (
|
||||
<span>
|
||||
{t('edit_comment.edit_window_expired')}
|
||||
<span>
|
||||
<a className={styles.link} onClick={this.props.onCancel}>
|
||||
{t('edit_comment.edit_window_expired_close')}
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Icon name="timer" className={styles.timerIcon} />{' '}
|
||||
{t('edit_comment.edit_window_timer_prefix')}
|
||||
<CountdownSeconds
|
||||
until={this.props.editableUntil}
|
||||
classNameForMsRemaining={remainingMs =>
|
||||
remainingMs <= 10 * 1000 ? styles.editWindowAlmostOver : ''
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const id = `edit-draft_${this.props.comment.id}`;
|
||||
return (
|
||||
<div className={styles.editCommentForm}>
|
||||
<CommentForm
|
||||
isEdit
|
||||
root={this.props.root}
|
||||
comment={this.props.comment}
|
||||
defaultValue={this.props.comment.body}
|
||||
bodyInputId={id}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
submitEnabled={this.isSubmitEnabled}
|
||||
body={this.state.body}
|
||||
onBodyChange={this.handleBodyChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
submitEnabled={this.props.submitEnabled}
|
||||
input={this.props.input}
|
||||
onInputChange={this.props.onInputChange}
|
||||
onSubmit={this.props.onSubmit}
|
||||
onCancel={this.props.onCancel}
|
||||
loadingState={this.props.loadingState}
|
||||
registerHook={this.props.registerHook}
|
||||
unregisterHook={this.props.unregisterHook}
|
||||
buttonContainerStart={this.renderButtonContainerStart()}
|
||||
submitButtonClassName={styles.button}
|
||||
cancelButtonClassName={styles.button}
|
||||
bodyLabel={t('edit_comment.body_input_label')}
|
||||
bodyPlaceholder=""
|
||||
submitText={<span>{t('edit_comment.save_button')}</span>}
|
||||
submitButtonCStyle="green"
|
||||
onCancel={this.props.stopEditing}
|
||||
submitButtonClassName={styles.button}
|
||||
cancelButtonClassName={styles.button}
|
||||
loadingState={this.state.loadingState}
|
||||
buttonContainerStart={
|
||||
<div className={styles.buttonContainerLeft}>
|
||||
<span className={styles.editWindowRemaining}>
|
||||
{this.isEditWindowExpired() ? (
|
||||
<span>
|
||||
{t('edit_comment.edit_window_expired')}
|
||||
{typeof this.props.stopEditing === 'function' ? (
|
||||
<span>
|
||||
<a
|
||||
className={styles.link}
|
||||
onClick={this.props.stopEditing}
|
||||
>
|
||||
{t('edit_comment.edit_window_expired_close')}
|
||||
</a>
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Icon name="timer" className={styles.timerIcon} />{' '}
|
||||
{t('edit_comment.edit_window_timer_prefix')}
|
||||
<CountdownSeconds
|
||||
until={this.getEditableUntil()}
|
||||
classNameForMsRemaining={remainingMs =>
|
||||
remainingMs <= 10 * 1000
|
||||
? styles.editWindowAlmostOver
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
id={id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditableCommentContent.propTypes = {
|
||||
charCountEnable: PropTypes.bool,
|
||||
submitEnabled: PropTypes.func,
|
||||
maxCharCount: PropTypes.number,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
input: PropTypes.object.isRequired,
|
||||
registerHook: PropTypes.func.isRequired,
|
||||
unregisterHook: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
loadingState: PropTypes.string,
|
||||
editWindowExpired: PropTypes.bool,
|
||||
editableUntil: PropTypes.object,
|
||||
};
|
||||
|
||||
export default EditableCommentContent;
|
||||
|
||||
@@ -15,14 +15,13 @@ import QuestionBox from '../../../components/QuestionBox';
|
||||
import { Tab, TabCount, TabPane } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import { reverseCommentParentTree } from '../../../graphql/utils';
|
||||
import AllCommentsPane from './AllCommentsPane';
|
||||
import ExtendableTabPanel from '../../../containers/ExtendableTabPanel';
|
||||
import ChangedUsername from './ChangedUsername';
|
||||
import CommentNotFound from '../containers/CommentNotFound';
|
||||
|
||||
import styles from './Stream.css';
|
||||
import ChangedUsername from './ChangedUsername';
|
||||
|
||||
class Stream extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -40,7 +39,6 @@ class Stream extends React.Component {
|
||||
|
||||
renderHighlightedComment() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
activeReplyBox,
|
||||
setActiveReplyBox,
|
||||
@@ -91,7 +89,6 @@ class Stream extends React.Component {
|
||||
</div>
|
||||
|
||||
<Comment
|
||||
data={data}
|
||||
root={root}
|
||||
commentClassNames={commentClassNames}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
@@ -122,7 +119,6 @@ class Stream extends React.Component {
|
||||
|
||||
renderExtendableTabPanel() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
activeReplyBox,
|
||||
setActiveReplyBox,
|
||||
@@ -147,15 +143,14 @@ class Stream extends React.Component {
|
||||
loading,
|
||||
} = this.props;
|
||||
|
||||
const slotProps = { data };
|
||||
const slotQueryData = { root, asset };
|
||||
const slotPassthrough = { root, asset };
|
||||
|
||||
// `key` of `ExtendableTabPanel` depends on sorting so that we always reset
|
||||
// the state when changing sorting.
|
||||
return (
|
||||
<div className={cn('talk-stream-tab-container', styles.tabContainer)}>
|
||||
<div className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}>
|
||||
<Slot fill="streamFilter" queryData={slotQueryData} {...slotProps} />
|
||||
<Slot fill="streamFilter" passthrough={slotPassthrough} />
|
||||
</div>
|
||||
|
||||
<ExtendableTabPanel
|
||||
@@ -166,8 +161,7 @@ class Stream extends React.Component {
|
||||
tabSlot="streamTabs"
|
||||
tabSlotPrepend="streamTabsPrepend"
|
||||
tabPaneSlot="streamTabPanes"
|
||||
slotProps={slotProps}
|
||||
queryData={slotQueryData}
|
||||
slotPassthrough={slotPassthrough}
|
||||
loading={loading}
|
||||
tabs={
|
||||
<Tab tabId={'all'} key="all">
|
||||
@@ -180,7 +174,6 @@ class Stream extends React.Component {
|
||||
tabPanes={
|
||||
<TabPane tabId="all" key="all">
|
||||
<AllCommentsPane
|
||||
data={data}
|
||||
root={root}
|
||||
asset={asset}
|
||||
comments={comments}
|
||||
@@ -212,7 +205,6 @@ class Stream extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
appendItemArray,
|
||||
asset,
|
||||
@@ -243,13 +235,17 @@ class Stream extends React.Component {
|
||||
!changedUsername &&
|
||||
!highlightedComment) ||
|
||||
keepCommentBox);
|
||||
const slotProps = { data };
|
||||
const slotQueryData = { root, asset };
|
||||
|
||||
if (highlightedComment === null) {
|
||||
return <StreamError>{t('stream.comment_not_found')}</StreamError>;
|
||||
return (
|
||||
<StreamError>
|
||||
<CommentNotFound />
|
||||
</StreamError>
|
||||
);
|
||||
}
|
||||
|
||||
const slotPassthrough = { root, asset };
|
||||
|
||||
return (
|
||||
<div id="stream" className={styles.root}>
|
||||
{open ? (
|
||||
@@ -263,11 +259,7 @@ class Stream extends React.Component {
|
||||
content={asset.settings.questionBoxContent}
|
||||
icon={asset.settings.questionBoxIcon}
|
||||
>
|
||||
<Slot
|
||||
fill="streamQuestionArea"
|
||||
queryData={slotQueryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
<Slot fill="streamQuestionArea" passthrough={slotPassthrough} />
|
||||
</QuestionBox>
|
||||
)}
|
||||
{!banned &&
|
||||
@@ -304,7 +296,7 @@ class Stream extends React.Component {
|
||||
<p>{asset.settings.closedMessage}</p>
|
||||
)}
|
||||
|
||||
<Slot fill="stream" queryData={slotQueryData} {...slotProps} />
|
||||
<Slot fill="stream" passthrough={slotPassthrough} />
|
||||
|
||||
{currentUser && (
|
||||
<ModerationLink
|
||||
@@ -324,7 +316,6 @@ class Stream extends React.Component {
|
||||
Stream.propTypes = {
|
||||
asset: PropTypes.object,
|
||||
activeStreamTab: PropTypes.string,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
activeReplyBox: PropTypes.string,
|
||||
setActiveReplyBox: PropTypes.func,
|
||||
|
||||
@@ -16,7 +16,6 @@ import { nest } from '../../../graphql/utils';
|
||||
|
||||
const slots = [
|
||||
'streamQuestionArea',
|
||||
'commentInputArea',
|
||||
'commentInputDetailArea',
|
||||
'commentInfoBar',
|
||||
'commentActions',
|
||||
|
||||
@@ -7,10 +7,24 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import { connect } from 'react-redux';
|
||||
import CommentForm from '../containers/CommentForm';
|
||||
import { notifyForNewCommentStatus } from '../helpers';
|
||||
import withHooks from '../hocs/withHooks';
|
||||
import { compose } from 'recompose';
|
||||
import once from 'lodash/once';
|
||||
|
||||
// TODO: (kiwi) Need to adapt CSS classes post refactor to match the rest.
|
||||
export const name = 'talk-plugin-commentbox';
|
||||
|
||||
// @Deprecated
|
||||
const showOldTagsWarningOnce = once(() => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.warn(
|
||||
'Using `addTags` and `removeTags` is deprecated. Please switch to `onInputChange` and `input` instead'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const initialInput = { body: '', tags: [] };
|
||||
|
||||
/**
|
||||
* Container for posting a new Comment
|
||||
*/
|
||||
@@ -19,14 +33,8 @@ class CommentBox extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
body: '',
|
||||
loadingState: '',
|
||||
// data: {@object} contains data that might be useful for plugins
|
||||
data: {},
|
||||
hooks: {
|
||||
preSubmit: [],
|
||||
postSubmit: [],
|
||||
},
|
||||
input: initialInput,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,29 +64,38 @@ class CommentBox extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
// @Deprecated
|
||||
const deprecatedTags = this.props.tags || [];
|
||||
if (deprecatedTags.length) {
|
||||
showOldTagsWarningOnce();
|
||||
}
|
||||
const tags = this.state.input.tags || [];
|
||||
|
||||
let input = {
|
||||
asset_id: assetId,
|
||||
parent_id: parentId,
|
||||
body: this.state.body,
|
||||
tags: this.props.tags,
|
||||
...this.state.data,
|
||||
...this.state.input,
|
||||
tags: [...deprecatedTags, ...tags],
|
||||
};
|
||||
|
||||
// Execute preSubmit Hooks
|
||||
this.state.hooks.preSubmit.forEach(hook =>
|
||||
hook(input, this.handleBodyChange)
|
||||
);
|
||||
this.props.forEachHook('preSubmit', hook => {
|
||||
const result = hook(input);
|
||||
if (result) {
|
||||
input = result;
|
||||
}
|
||||
});
|
||||
this.setState({ loadingState: 'loading' });
|
||||
|
||||
postComment(input, 'comments')
|
||||
.then(({ data }) => {
|
||||
this.setState({ loadingState: 'success', body: '' });
|
||||
this.setState({ loadingState: 'success', input: initialInput });
|
||||
const postedComment = data.createComment.comment;
|
||||
const actions = data.createComment.actions;
|
||||
|
||||
// Execute postSubmit Hooks
|
||||
this.state.hooks.postSubmit.forEach(hook =>
|
||||
hook(data, this.handleBodyChange)
|
||||
this.props.forEachHook('postSubmit', hook =>
|
||||
hook(data, this.handleInputChange)
|
||||
);
|
||||
|
||||
notifyForNewCommentStatus(notify, postedComment.status, actions);
|
||||
@@ -92,62 +109,32 @@ class CommentBox extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
handleBodyChange = (body, data) => {
|
||||
handleInputChange = input => {
|
||||
this.setState(state => ({
|
||||
body,
|
||||
data: {
|
||||
...state.data,
|
||||
...data,
|
||||
input: {
|
||||
...state.input,
|
||||
...input,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
registerHook = (hookType = '', hook = () => {}) => {
|
||||
if (typeof hook !== 'function') {
|
||||
return console.warn(
|
||||
`Hooks must be functions. Please check your ${hookType} hooks`
|
||||
);
|
||||
} else if (typeof hookType === 'string') {
|
||||
this.setState(state => ({
|
||||
hooks: {
|
||||
...state.hooks,
|
||||
[hookType]: [...state.hooks[hookType], hook],
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
hookType,
|
||||
hook,
|
||||
};
|
||||
} else {
|
||||
return console.warn(
|
||||
'hookTypes must be a string. Please check your hooks'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
unregisterHook = hookData => {
|
||||
const { hookType, hook } = hookData;
|
||||
|
||||
this.setState(state => {
|
||||
let newHooks = state.hooks[newHooks];
|
||||
const idx = state.hooks[hookType].indexOf(hook);
|
||||
|
||||
if (idx !== -1) {
|
||||
newHooks = [
|
||||
...state.hooks[hookType].slice(0, idx),
|
||||
...state.hooks[hookType].slice(idx + 1),
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
...state.hooks,
|
||||
[hookType]: newHooks,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
renderButtonContainerStart() {
|
||||
const { root, isReply, registerHook, unregisterHook } = this.props;
|
||||
return (
|
||||
<Slot
|
||||
fill="commentInputDetailArea"
|
||||
passthrough={{
|
||||
root,
|
||||
registerHook: registerHook,
|
||||
unregisterHook: unregisterHook,
|
||||
isReply,
|
||||
input: this.state.input,
|
||||
onInputChange: this.handleInputChange,
|
||||
}}
|
||||
inline
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -157,6 +144,8 @@ class CommentBox extends React.Component {
|
||||
parentId,
|
||||
comment,
|
||||
root,
|
||||
registerHook,
|
||||
unregisterHook,
|
||||
} = this.props;
|
||||
let { onCancel } = this.props;
|
||||
|
||||
@@ -178,25 +167,15 @@ class CommentBox extends React.Component {
|
||||
root={root}
|
||||
comment={comment}
|
||||
defaultValue={this.props.defaultValue}
|
||||
bodyLabel={isReply ? t('comment_box.reply') : t('comment.comment')}
|
||||
maxCharCount={maxCharCount}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
bodyPlaceholder={t('comment.comment')}
|
||||
bodyInputId={id}
|
||||
body={this.state.body}
|
||||
registerHook={this.registerHook}
|
||||
unregisterHook={this.unregisterHook}
|
||||
id={id}
|
||||
input={this.state.input}
|
||||
registerHook={registerHook}
|
||||
unregisterHook={unregisterHook}
|
||||
isReply={isReply}
|
||||
buttonContainerStart={
|
||||
<Slot
|
||||
fill="commentInputDetailArea"
|
||||
registerHook={this.registerHook}
|
||||
unregisterHook={this.unregisterHook}
|
||||
isReply={isReply}
|
||||
inline
|
||||
/>
|
||||
}
|
||||
onBodyChange={this.handleBodyChange}
|
||||
buttonContainerStart={this.renderButtonContainerStart()}
|
||||
onInputChange={this.handleInputChange}
|
||||
loadingState={this.state.loadingState}
|
||||
onCancel={onCancel}
|
||||
onSubmit={this.handleSubmit}
|
||||
@@ -223,6 +202,9 @@ CommentBox.propTypes = {
|
||||
tags: PropTypes.array,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object,
|
||||
registerHook: PropTypes.func.isRequired,
|
||||
unregisterHook: PropTypes.func.isRequired,
|
||||
forEachHook: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
CommentBox.fragments = CommentForm.fragments;
|
||||
@@ -231,4 +213,9 @@ const mapStateToProps = state => ({
|
||||
tags: state.stream.commentBoxTags,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(CommentBox);
|
||||
const enhance = compose(
|
||||
withHooks(['preSubmit', 'postSubmit']),
|
||||
connect(mapStateToProps, null)
|
||||
);
|
||||
|
||||
export default enhance(CommentBox);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'coral-ui';
|
||||
import { setActiveTab } from '../../../actions/embed';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class CommentNotFound extends React.Component {
|
||||
showAllTab = () => {
|
||||
this.props.setActiveTab('all');
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>{t('stream.comment_not_found')}</p>
|
||||
<Button onClick={this.showAllTab}>Show all comments</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentNotFound.propTypes = {
|
||||
setActiveTab: PropTypes.func,
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
setActiveTab,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default connect(null, mapDispatchToProps)(CommentNotFound);
|
||||
@@ -17,9 +17,17 @@ class DraftAreaContainer extends React.Component {
|
||||
}
|
||||
|
||||
async initValue() {
|
||||
const value = await this.context.pymSessionStorage.getItem(this.getPath());
|
||||
if (value && this.props.onChange) {
|
||||
this.props.onChange(value);
|
||||
const input = await this.context.pymSessionStorage.getItem(this.getPath());
|
||||
if (input && this.props.onInputChange) {
|
||||
let parsed = '';
|
||||
|
||||
// Older version saved a normal string, catch those and ignore them.
|
||||
try {
|
||||
parsed = JSON.parse(input);
|
||||
} catch (_e) {}
|
||||
if (typeof parsed === 'object') {
|
||||
this.props.onInputChange(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +35,13 @@ class DraftAreaContainer extends React.Component {
|
||||
return `${STORAGE_PATH}_${this.props.id}`;
|
||||
};
|
||||
|
||||
onChange = (body, data) => {
|
||||
this.props.onChange && this.props.onChange(body, data);
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.value !== nextProps.value) {
|
||||
if (nextProps.value) {
|
||||
this.context.pymSessionStorage.setItem(this.getPath(), nextProps.value);
|
||||
if (this.props.input !== nextProps.input) {
|
||||
if (nextProps.input) {
|
||||
this.context.pymSessionStorage.setItem(
|
||||
this.getPath(),
|
||||
JSON.stringify(nextProps.input)
|
||||
);
|
||||
} else {
|
||||
this.context.pymSessionStorage.removeItem(this.getPath());
|
||||
}
|
||||
@@ -42,23 +49,20 @@ class DraftAreaContainer extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const queryData = { comment: this.props.comment, root: this.props.root };
|
||||
|
||||
return (
|
||||
<DraftArea
|
||||
queryData={queryData}
|
||||
value={this.props.value}
|
||||
placeholder={this.props.placeholder}
|
||||
root={this.props.root}
|
||||
comment={this.props.comment}
|
||||
input={this.props.input}
|
||||
id={this.props.id}
|
||||
onChange={this.onChange}
|
||||
rows={this.props.rows}
|
||||
onInputChange={this.props.onInputChange}
|
||||
disabled={this.props.disabled}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
label={this.props.label}
|
||||
registerHook={this.props.registerHook}
|
||||
unregisterHook={this.props.unregisterHook}
|
||||
isReply={this.props.isReply}
|
||||
isEdit={this.props.isEdit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -74,20 +78,18 @@ DraftAreaContainer.propTypes = {
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
id: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
input: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
rows: PropTypes.number,
|
||||
label: PropTypes.string.isRequired,
|
||||
registerHook: PropTypes.func,
|
||||
unregisterHook: PropTypes.func,
|
||||
isReply: PropTypes.bool,
|
||||
isEdit: PropTypes.bool,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object,
|
||||
};
|
||||
|
||||
const slots = ['draftArea'];
|
||||
const slots = ['draftArea', 'commentInputArea'];
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
|
||||
@@ -1,11 +1,150 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { notifyForNewCommentStatus } from '../helpers';
|
||||
import { getEditableUntilDate } from '../util';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import EditableCommentContent from '../components/EditableCommentContent';
|
||||
import CommentForm from './CommentForm';
|
||||
import withHooks from '../hocs/withHooks';
|
||||
import { compose } from 'recompose';
|
||||
|
||||
const EditableCommentContentContainer = props => (
|
||||
<EditableCommentContent {...props} />
|
||||
);
|
||||
/**
|
||||
* Renders a Comment's body in such a way that the end-user can edit it and save changes
|
||||
*/
|
||||
class EditableCommentContentContainer extends React.Component {
|
||||
unmounted = false;
|
||||
editWindowExpiryTimeout = null;
|
||||
state = {
|
||||
loadingState: '',
|
||||
submitEnabled: false,
|
||||
input: {
|
||||
body: this.props.comment.body,
|
||||
},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const editableUntil = getEditableUntilDate(this.props.comment);
|
||||
const now = new Date();
|
||||
const editWindowRemainingMs = editableUntil && editableUntil - now;
|
||||
if (editWindowRemainingMs > 0) {
|
||||
this.editWindowExpiryTimeout = setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
}, editWindowRemainingMs);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
if (this.editWindowExpiryTimeout) {
|
||||
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange = input => {
|
||||
this.setState(state => ({
|
||||
submitEnabled: true,
|
||||
input: {
|
||||
...state.input,
|
||||
...input,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
handleSubmit = async () => {
|
||||
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
|
||||
this.props.notify('error', t('error.NOT_AUTHORIZED'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loadingState: 'loading' });
|
||||
|
||||
const { editComment, stopEditing } = this.props;
|
||||
if (typeof editComment !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
let input = this.state.input;
|
||||
|
||||
// Execute preSubmit Hooks
|
||||
this.props.forEachHook('preSubmit', hook => {
|
||||
const result = hook(input);
|
||||
if (result) {
|
||||
input = result;
|
||||
}
|
||||
});
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await editComment(input);
|
||||
// Execute postSubmit Hooks
|
||||
this.props.forEachHook('postSubmit', hook =>
|
||||
hook(response, this.handleInputChange)
|
||||
);
|
||||
|
||||
if (!this.unmounted) {
|
||||
this.setState({ loadingState: 'success' });
|
||||
}
|
||||
const status = response.data.editComment.comment.status;
|
||||
notifyForNewCommentStatus(this.props.notify, status);
|
||||
if (typeof stopEditing === 'function') {
|
||||
stopEditing();
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({ loadingState: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
getEditableUntil = (props = this.props) => {
|
||||
return getEditableUntilDate(props.comment);
|
||||
};
|
||||
|
||||
isEditWindowExpired = (props = this.props) => {
|
||||
return this.getEditableUntil(props) - new Date() < 0;
|
||||
};
|
||||
|
||||
isSubmitEnabled = () => this.state.submitEnabled;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditableCommentContent
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
submitEnabled={this.isSubmitEnabled}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
root={this.props.root}
|
||||
comment={this.props.comment}
|
||||
input={this.state.input}
|
||||
onInputChange={this.handleInputChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
onCancel={this.props.stopEditing}
|
||||
loadingState={this.state.loadingState}
|
||||
editWindowExpired={this.isEditWindowExpired()}
|
||||
editableUntil={this.getEditableUntil()}
|
||||
registerHook={this.props.registerHook}
|
||||
unregisterHook={this.props.unregisterHook}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditableCommentContentContainer.propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object,
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
editComment: PropTypes.func,
|
||||
stopEditing: PropTypes.func,
|
||||
registerHook: PropTypes.func.isRequired,
|
||||
unregisterHook: PropTypes.func.isRequired,
|
||||
forEachHook: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
EditableCommentContentContainer.fragments = CommentForm.fragments;
|
||||
|
||||
export default EditableCommentContentContainer;
|
||||
const enhance = compose(withHooks(['preSubmit', 'postSubmit']));
|
||||
|
||||
export default enhance(EditableCommentContentContainer);
|
||||
|
||||
@@ -367,6 +367,7 @@ const LOAD_MORE_QUERY = gql`
|
||||
`;
|
||||
|
||||
const slots = [
|
||||
'commentInputDetailArea',
|
||||
'streamTabs',
|
||||
'streamTabsPrepend',
|
||||
'streamTabPanes',
|
||||
@@ -465,7 +466,6 @@ const mapStateToProps = state => ({
|
||||
activeStreamTab: state.stream.activeTab,
|
||||
previousStreamTab: state.stream.previousTab,
|
||||
commentClassNames: state.stream.commentClassNames,
|
||||
pluginConfig: state.config.plugin_config,
|
||||
sortOrder: state.stream.sortOrder,
|
||||
sortBy: state.stream.sortBy,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
|
||||
/**
|
||||
* WithHooks provides a property `hooks` to the wrapped component.
|
||||
*/
|
||||
export default hooks =>
|
||||
hoistStatics(WrappedComponent => {
|
||||
class WithHooks extends React.Component {
|
||||
hooks = hooks.reduce((map, key) => {
|
||||
map[key] = [];
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
registerHook = (hookType = '', hook) => {
|
||||
if (typeof hook !== 'function') {
|
||||
return console.warn(
|
||||
`Hooks must be functions. Please check your ${hookType} hooks`
|
||||
);
|
||||
}
|
||||
if (!hooks.includes(hookType)) {
|
||||
throw new Error(`Unknown hookType ${hookType}`);
|
||||
}
|
||||
|
||||
this.hooks[hookType].push(hook);
|
||||
return {
|
||||
hookType,
|
||||
hook,
|
||||
};
|
||||
};
|
||||
|
||||
unregisterHook = hookData => {
|
||||
const { hookType, hook } = hookData;
|
||||
const idx = this.hooks[hookType].indexOf(hook);
|
||||
if (idx !== -1) {
|
||||
this.hooks[hookType].splice(idx, 1);
|
||||
}
|
||||
};
|
||||
|
||||
forEachHook = (hookType, callback) => {
|
||||
this.hooks[hookType].forEach(callback);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
registerHook={this.registerHook}
|
||||
unregisterHook={this.unregisterHook}
|
||||
forEachHook={this.forEachHook}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithHooks;
|
||||
});
|
||||
@@ -161,6 +161,14 @@ export default class Stream {
|
||||
);
|
||||
}
|
||||
|
||||
enablePluginsDebug() {
|
||||
this.pym.sendMessage('enablePluginsDebug');
|
||||
}
|
||||
|
||||
disablePluginsDebug() {
|
||||
this.pym.sendMessage('disablePluginsDebug');
|
||||
}
|
||||
|
||||
login(token) {
|
||||
this.pym.sendMessage('login', token);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ export default class StreamInterface {
|
||||
return this._stream.emitter.on(eventName, callback);
|
||||
}
|
||||
|
||||
off(eventName, callback) {
|
||||
return this._stream.emitter.off(eventName, callback);
|
||||
}
|
||||
|
||||
login(token) {
|
||||
return this._stream.login(token);
|
||||
}
|
||||
@@ -18,4 +22,12 @@ export default class StreamInterface {
|
||||
remove() {
|
||||
return this._stream.remove();
|
||||
}
|
||||
|
||||
enablePluginsDebug() {
|
||||
return this._stream.enablePluginsDebug();
|
||||
}
|
||||
|
||||
disablePluginsDebug() {
|
||||
return this._stream.disablePluginsDebug();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ export const setAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
// Dispatch the set auth token action. For some browsers and situations, we
|
||||
// may not be able to persist the auth token any other way. Keep it in redux!
|
||||
dispatch({ type: actions.SET_AUTH_TOKEN, token });
|
||||
|
||||
dispatch(checkLogin());
|
||||
};
|
||||
|
||||
@@ -87,6 +91,7 @@ export const handleSuccessfulLogin = (user, token) => (
|
||||
dispatch({
|
||||
type: actions.HANDLE_SUCCESSFUL_LOGIN,
|
||||
user,
|
||||
token,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { MERGE_CONFIG } from '../constants/config';
|
||||
import {
|
||||
MERGE_CONFIG,
|
||||
ENABLE_PLUGINS_DEBUG,
|
||||
DISABLE_PLUGINS_DEBUG,
|
||||
} from '../constants/config';
|
||||
|
||||
export const mergeConfig = config => ({
|
||||
type: MERGE_CONFIG,
|
||||
config,
|
||||
});
|
||||
|
||||
export const enablePluginsDebug = () => ({
|
||||
type: ENABLE_PLUGINS_DEBUG,
|
||||
});
|
||||
|
||||
export const disablePluginsDebug = () => ({
|
||||
type: DISABLE_PLUGINS_DEBUG,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.content {
|
||||
a {
|
||||
color: #063b9a;
|
||||
text-decoration: underline;
|
||||
font-weight: 300;
|
||||
background-color: #f4ff81;
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: #f4ff81;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import matchLinks from '../utils/matchLinks';
|
||||
import memoize from 'lodash/memoize';
|
||||
import cn from 'classnames';
|
||||
import styles from './AdminCommentContent.css';
|
||||
|
||||
function escapeHTML(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
// generate a regulare expression that catches the `phrases`.
|
||||
function generateRegExp(phrases) {
|
||||
const inner = phrases
|
||||
.map(phrase =>
|
||||
phrase
|
||||
.split(/\s+/)
|
||||
.map(word => escapeRegExp(word))
|
||||
.join('[\\s"?!.]+')
|
||||
)
|
||||
.join('|');
|
||||
|
||||
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
|
||||
try {
|
||||
return new RegExp(pattern, 'iu');
|
||||
} catch (_err) {
|
||||
// IE does not support unicode support, so we'll create one without.
|
||||
return new RegExp(pattern, 'i');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
|
||||
function getPhrasesRegexp(suspectWords, bannedWords) {
|
||||
return generateRegExp([...suspectWords, ...bannedWords]);
|
||||
}
|
||||
|
||||
// Memoized version as arguments rarely change.
|
||||
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
|
||||
|
||||
function nl2br(body, keyPrefix) {
|
||||
const tokens = body.split('\n').reduce((tokens, t, i) => {
|
||||
if (i !== 0) {
|
||||
tokens.push(<br key={`${keyPrefix}_${i}`} />);
|
||||
}
|
||||
tokens.push(t);
|
||||
return tokens;
|
||||
}, []);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
|
||||
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
|
||||
const tokens = body.split(regexp);
|
||||
return tokens.map(
|
||||
(token, i) =>
|
||||
i % 3 === 2 ? <mark key={`${keyPrefix}_${i}`}>{token}</mark> : token
|
||||
);
|
||||
}
|
||||
|
||||
// markLinks looks for links inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markLinks(body, keyPrefix) {
|
||||
const matches = matchLinks(body);
|
||||
const content = [];
|
||||
let index = 0;
|
||||
if (matches) {
|
||||
matches.forEach((match, i) => {
|
||||
content.push(body.substring(index, match.index));
|
||||
content.push(
|
||||
<a key={`${keyPrefix}_${i}`} href={match.url} target="_blank">
|
||||
{match.text}
|
||||
</a>
|
||||
);
|
||||
index = match.lastIndex;
|
||||
});
|
||||
}
|
||||
content.push(body.substring(index));
|
||||
return content;
|
||||
}
|
||||
|
||||
// markPhrasesHTML looks for `supsectWords` and `bannedWords` inside `text` and highlights them by returning
|
||||
// a HTML string.
|
||||
function markPhrasesHTML(text, suspectWords, bannedWords) {
|
||||
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
|
||||
const tokens = text.split(regexp);
|
||||
if (tokens.length === 1) {
|
||||
return text;
|
||||
}
|
||||
return tokens
|
||||
.map(
|
||||
(token, i) =>
|
||||
i % 3 === 2 ? `<mark>${escapeHTML(token)}</mark>` : escapeHTML(token)
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// markHTMLNode manipulates the node by looking for #text nodes and adding markers
|
||||
// for `supsectWords` and `bannedWords`.
|
||||
function markHTMLNode(parentNode, suspectWords, bannedWords) {
|
||||
parentNode.childNodes.forEach(node => {
|
||||
if (node.nodeName === '#text') {
|
||||
const newContent = markPhrasesHTML(
|
||||
node.textContent,
|
||||
suspectWords,
|
||||
bannedWords
|
||||
);
|
||||
if (newContent !== node.textContent) {
|
||||
const newNode = document.createElement('span');
|
||||
newNode.innerHTML = newContent;
|
||||
parentNode.replaceChild(newNode, node);
|
||||
}
|
||||
} else {
|
||||
markHTMLNode(node, suspectWords, bannedWords);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// renderText performs all the marking of a text body and returns an array of React Elements.
|
||||
function renderText(body, suspectWords, bannedWords) {
|
||||
return nl2br(body).map((element, index) => {
|
||||
// Skip br tags.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
return markLinks(element, index).map((element, index) => {
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const commonPropTypes = {
|
||||
className: PropTypes.string,
|
||||
bannedWords: PropTypes.array.isRequired,
|
||||
suspectWords: PropTypes.array.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const AdminCommentContentText = ({
|
||||
body,
|
||||
className,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, styles.content)}>
|
||||
{renderText(body, suspectWords, bannedWords)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
AdminCommentContentText.propTypes = commonPropTypes;
|
||||
|
||||
const AdminCommentContentHTML = ({
|
||||
body,
|
||||
className,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
}) => {
|
||||
// We create a Shadow DOM Tree with the HTML body content and
|
||||
// use it as a parser.
|
||||
const node = document.createElement('div');
|
||||
node.innerHTML = body;
|
||||
|
||||
// Then we traverse it recursively and manipulate it to highlight suspect words
|
||||
// and banned words.
|
||||
markHTMLNode(node, suspectWords, bannedWords);
|
||||
|
||||
// Finally we render the content of the Shadow DOM Tree
|
||||
return (
|
||||
<div
|
||||
className={cn(className, styles.content)}
|
||||
dangerouslySetInnerHTML={{ __html: node.innerHTML }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
AdminCommentContentHTML.propTypes = commonPropTypes;
|
||||
|
||||
const AdminCommentContent = ({
|
||||
className,
|
||||
body,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
html,
|
||||
}) => {
|
||||
const Component = html ? AdminCommentContentHTML : AdminCommentContentText;
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
body={body}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AdminCommentContent.propTypes = {
|
||||
...commonPropTypes,
|
||||
html: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AdminCommentContent;
|
||||
@@ -1,39 +1,14 @@
|
||||
import React, { Children } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getShallowChanges } from 'coral-framework/utils';
|
||||
import { withSlotElements, withCompatPassthrough } from '../hocs';
|
||||
import { compose } from 'recompose';
|
||||
|
||||
class IfSlotIsEmpty extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(next) {
|
||||
// Prevent Slot from rerendering when only reduxState has changed and
|
||||
// it does not result in a change.
|
||||
const changes = getShallowChanges(this.props, next);
|
||||
if (changes.length === 1 && changes[0] === 'reduxState') {
|
||||
return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next);
|
||||
}
|
||||
|
||||
// Prevent Slot from rerendering when no props has shallowly changed.
|
||||
return changes.length !== 0;
|
||||
}
|
||||
|
||||
isSlotEmpty(props = this.props) {
|
||||
const {
|
||||
slot,
|
||||
className: _a,
|
||||
reduxState,
|
||||
component: _b = 'div',
|
||||
children: _c,
|
||||
queryData,
|
||||
...rest
|
||||
} = props;
|
||||
const slots = Array.isArray(slot) ? slot : [slot];
|
||||
return slots.every(slot =>
|
||||
this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData)
|
||||
);
|
||||
const { slotElements } = props;
|
||||
return slotElements.length === 0
|
||||
? false
|
||||
: slotElements.every(elements => elements.length === 0);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -44,10 +19,15 @@ class IfSlotIsEmpty extends React.Component {
|
||||
|
||||
IfSlotIsEmpty.propTypes = {
|
||||
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
children: PropTypes.node.isRequired,
|
||||
passthrough: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
reduxState: state,
|
||||
});
|
||||
const omitProps = ['slot', 'children'];
|
||||
|
||||
export default connect(mapStateToProps, null)(IfSlotIsEmpty);
|
||||
export default compose(
|
||||
withCompatPassthrough(omitProps),
|
||||
withSlotElements({
|
||||
slot: props => props.slot,
|
||||
})
|
||||
)(IfSlotIsEmpty);
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
import React, { Children } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getShallowChanges } from 'coral-framework/utils';
|
||||
import { withSlotElements, withCompatPassthrough } from '../hocs';
|
||||
import { compose } from 'recompose';
|
||||
|
||||
class IfSlotIsNotEmpty extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(next) {
|
||||
// Prevent Slot from rerendering when only reduxState has changed and
|
||||
// it does not result in a change.
|
||||
const changes = getShallowChanges(this.props, next);
|
||||
if (changes.length === 1 && changes[0] === 'reduxState') {
|
||||
return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next);
|
||||
}
|
||||
|
||||
// Prevent Slot from rerendering when no props has shallowly changed.
|
||||
return changes.length !== 0;
|
||||
}
|
||||
|
||||
isSlotEmpty(props = this.props) {
|
||||
const {
|
||||
slot,
|
||||
className: _a,
|
||||
reduxState,
|
||||
component: _b = 'div',
|
||||
children: _c,
|
||||
queryData,
|
||||
...rest
|
||||
} = props;
|
||||
const slots = Array.isArray(slot) ? slot : [slot];
|
||||
return slots.every(slot =>
|
||||
this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData)
|
||||
);
|
||||
const { slotElements } = props;
|
||||
return slotElements.length === 0
|
||||
? true
|
||||
: slotElements.every(elements => elements.length === 0);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -44,10 +19,16 @@ class IfSlotIsNotEmpty extends React.Component {
|
||||
|
||||
IfSlotIsNotEmpty.propTypes = {
|
||||
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
slotElements: PropTypes.array.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
passthrough: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
reduxState: state,
|
||||
});
|
||||
const omitProps = ['slot', 'children'];
|
||||
|
||||
export default connect(mapStateToProps, null)(IfSlotIsNotEmpty);
|
||||
export default compose(
|
||||
withCompatPassthrough(omitProps),
|
||||
withSlotElements({
|
||||
slot: props => props.slot,
|
||||
})
|
||||
)(IfSlotIsNotEmpty);
|
||||
|
||||
@@ -1,154 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SimpleMDE from 'simplemde';
|
||||
import cn from 'classnames';
|
||||
import noop from 'lodash/noop';
|
||||
import styles from './MarkdownEditor.css';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import Loadable from 'react-loadable';
|
||||
|
||||
const config = {
|
||||
status: false,
|
||||
const MarkdownEditor = Loadable({
|
||||
loader: () =>
|
||||
import(/* webpackChunkName: "markdownEditor" */
|
||||
'./loadable/MarkdownEditor'),
|
||||
loading: Spinner,
|
||||
});
|
||||
|
||||
// Do not download fontAwesome icons as we replace them with
|
||||
// material icons.
|
||||
autoDownloadFontAwesome: false,
|
||||
|
||||
// Disable built-in spell checker as it is very rudimentary.
|
||||
spellChecker: false,
|
||||
|
||||
toolbar: [
|
||||
{
|
||||
name: 'bold',
|
||||
action: SimpleMDE.toggleBold,
|
||||
className: styles.iconBold,
|
||||
title: 'Bold',
|
||||
},
|
||||
{
|
||||
name: 'italic',
|
||||
action: SimpleMDE.toggleItalic,
|
||||
className: styles.iconItalic,
|
||||
title: 'Italic',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
action: SimpleMDE.toggleHeadingSmaller,
|
||||
className: styles.iconTitle,
|
||||
title: 'Title, Subtitle, Heading',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'quote',
|
||||
action: SimpleMDE.toggleBlockquote,
|
||||
className: styles.iconQuote,
|
||||
title: 'Quote',
|
||||
},
|
||||
{
|
||||
name: 'unordered-list',
|
||||
action: SimpleMDE.toggleUnorderedList,
|
||||
className: styles.iconUnorderedList,
|
||||
title: 'Generic List',
|
||||
},
|
||||
{
|
||||
name: 'ordered-list',
|
||||
action: SimpleMDE.toggleOrderedList,
|
||||
className: styles.iconOrderedList,
|
||||
title: 'Numbered List',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'link',
|
||||
action: SimpleMDE.drawLink,
|
||||
className: styles.iconLink,
|
||||
title: 'Create Link',
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
action: SimpleMDE.drawImage,
|
||||
className: styles.iconImage,
|
||||
title: 'Insert Image',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'preview',
|
||||
action: SimpleMDE.togglePreview,
|
||||
className: cn(styles.iconPreview, 'no-disable'),
|
||||
title: 'Toggle Preview',
|
||||
},
|
||||
{
|
||||
name: 'side-by-side',
|
||||
action: SimpleMDE.toggleSideBySide,
|
||||
className: cn(styles.iconSideBySide, 'no-disable'),
|
||||
title: 'Toggle Side by Side',
|
||||
},
|
||||
{
|
||||
name: 'fullscreen',
|
||||
action: SimpleMDE.toggleFullScreen,
|
||||
className: cn(styles.iconFullscreen, 'no-disable'),
|
||||
title: 'Toggle Fullscreen',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'guide',
|
||||
action: 'https://simplemde.com/markdown-guide',
|
||||
className: styles.iconGuide,
|
||||
title: 'Markdown Guide',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default class MarkdownEditor extends Component {
|
||||
textarea = null;
|
||||
editor = null;
|
||||
|
||||
onRef = ref => (this.textarea = ref);
|
||||
|
||||
componentDidMount() {
|
||||
this.editor = new SimpleMDE({
|
||||
...config,
|
||||
element: this.textarea,
|
||||
});
|
||||
|
||||
// Don't trap the key, to stay accessible.
|
||||
this.editor.codemirror.options.extraKeys['Tab'] = false;
|
||||
this.editor.codemirror.options.extraKeys['Shift-Tab'] = false;
|
||||
|
||||
this.editor.codemirror.on('change', this.onChange);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
this.props.value !== nextProps.value &&
|
||||
nextProps.value !== this.editor.value()
|
||||
) {
|
||||
this.editor.value(nextProps.value);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Workaround empty render issue.
|
||||
// https://github.com/NextStepWebs/simplemde-markdown-editor/issues/313
|
||||
this.editor.codemirror.refresh();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.editor.toTextArea();
|
||||
}
|
||||
|
||||
onChange = () => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this.editor.value());
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<textarea ref={this.onRef} {...this.props} onChange={noop} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownEditor.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
export default MarkdownEditor;
|
||||
|
||||
@@ -3,5 +3,36 @@
|
||||
}
|
||||
|
||||
.debug {
|
||||
background-color: coral;
|
||||
background-color: #e2e2e2;
|
||||
border-style: dotted solid;
|
||||
border-width: 2px;
|
||||
border: dotted 2px coral;
|
||||
padding: 2px;
|
||||
margin: 1px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.debug::before {
|
||||
content: attr(data-slot-name);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
opacity: 0;
|
||||
transition: 0.3s;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 999!important;
|
||||
white-space: pre-wrap;
|
||||
min-height: 16px;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.debug:hover::before {
|
||||
opacity: 1;
|
||||
top: 100%;
|
||||
}
|
||||
@@ -4,86 +4,21 @@ import styles from './Slot.css';
|
||||
import { connect } from 'react-redux';
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import get from 'lodash/get';
|
||||
import { getShallowChanges } from 'coral-framework/utils';
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
const emptyConfig = {};
|
||||
import { withSlotElements, withCompatPassthrough } from '../hocs';
|
||||
import { compose } from 'recompose';
|
||||
|
||||
class Slot extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(next) {
|
||||
// Prevent Slot from rerendering when only reduxState has changed and
|
||||
// it does not result in a change of slot children.
|
||||
const changes = getShallowChanges(this.props, next);
|
||||
if (changes.length === 1 && changes[0] === 'reduxState') {
|
||||
const prevChildrenKeys = this.getChildren(this.props).map(
|
||||
child => child.key
|
||||
);
|
||||
const nextChildrenKeys = this.getChildren(next).map(child => child.key);
|
||||
return !isEqual(prevChildrenKeys, nextChildrenKeys);
|
||||
}
|
||||
|
||||
// Prevent Slot from rerendering when no props has shallowly changed.
|
||||
return changes.length !== 0;
|
||||
}
|
||||
|
||||
getSlotProps(props = this.props) {
|
||||
return omit(props, [
|
||||
'fill',
|
||||
'inline',
|
||||
'className',
|
||||
'reduxState',
|
||||
'slotSize',
|
||||
'defaultComponent',
|
||||
'queryData',
|
||||
'childFactory',
|
||||
'component',
|
||||
]);
|
||||
}
|
||||
|
||||
getChildren(props = this.props) {
|
||||
const { slotSize = 0 } = props;
|
||||
const { plugins } = this.context;
|
||||
|
||||
return plugins.getSlotElements(
|
||||
props.fill,
|
||||
props.reduxState,
|
||||
this.getSlotProps(props),
|
||||
props.queryData,
|
||||
{ slotSize }
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
inline = false,
|
||||
className,
|
||||
reduxState,
|
||||
debug,
|
||||
component: Component,
|
||||
childFactory,
|
||||
defaultComponent: DefaultComponent,
|
||||
queryData,
|
||||
fill,
|
||||
} = this.props;
|
||||
const { plugins } = this.context;
|
||||
let children = this.getChildren();
|
||||
const pluginConfig =
|
||||
get(reduxState, 'config.plugins_config') || emptyConfig;
|
||||
if (children.length === 0 && DefaultComponent) {
|
||||
const props = plugins.getSlotComponentProps(
|
||||
DefaultComponent,
|
||||
reduxState,
|
||||
this.getSlotProps(this.props),
|
||||
queryData
|
||||
);
|
||||
children = <DefaultComponent {...props} />;
|
||||
}
|
||||
|
||||
let children = this.props.slotElements;
|
||||
if (childFactory) {
|
||||
children = children.map(childFactory);
|
||||
}
|
||||
@@ -91,10 +26,11 @@ class Slot extends React.Component {
|
||||
return (
|
||||
<Component
|
||||
className={cn(
|
||||
{ [styles.inline]: inline, [styles.debug]: pluginConfig.debug },
|
||||
{ [styles.inline]: inline, [styles.debug]: debug },
|
||||
className,
|
||||
`talk-slot-${kebabCase(fill)}`
|
||||
)}
|
||||
data-slot-name={fill}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
@@ -110,13 +46,14 @@ Slot.propTypes = {
|
||||
fill: PropTypes.string.isRequired,
|
||||
inline: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
reduxState: PropTypes.object,
|
||||
debug: PropTypes.bool,
|
||||
slotElements: PropTypes.arrayOf(PropTypes.element).isRequired,
|
||||
defaultComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
|
||||
|
||||
/**
|
||||
* Specifies the number of children that can fill the slot.
|
||||
*/
|
||||
slotSize: PropTypes.number,
|
||||
size: PropTypes.number,
|
||||
|
||||
/**
|
||||
* You may specify the component to use as the root wrapper.
|
||||
@@ -125,8 +62,12 @@ Slot.propTypes = {
|
||||
component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
|
||||
|
||||
// props coming from graphql must be passed through this property.
|
||||
// @Deprecated
|
||||
queryData: PropTypes.object,
|
||||
|
||||
// props that are passed to all Slot Components
|
||||
passthrough: PropTypes.object,
|
||||
|
||||
/**
|
||||
* You may need to apply reactive updates to a child as it is exiting.
|
||||
* This is generally done by using `cloneElement` however in the case of an exiting
|
||||
@@ -140,8 +81,27 @@ Slot.propTypes = {
|
||||
childFactory: PropTypes.func,
|
||||
};
|
||||
|
||||
const omitProps = [
|
||||
'fill',
|
||||
'inline',
|
||||
'className',
|
||||
'size',
|
||||
'defaultComponent',
|
||||
'queryData',
|
||||
'childFactory',
|
||||
'component',
|
||||
];
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
reduxState: state,
|
||||
debug: get(state, 'config.plugins_config.debug'),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(Slot);
|
||||
export default compose(
|
||||
withCompatPassthrough(omitProps),
|
||||
withSlotElements({
|
||||
slot: props => props.fill,
|
||||
size: props => props.size,
|
||||
defaultComponent: props => props.defaultComponent,
|
||||
}),
|
||||
connect(mapStateToProps, null)
|
||||
)(Slot);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user