diff --git a/.babelrc b/.babelrc index 92a64c2a4..ed21974ac 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,6 @@ { "presets": [ - ["es2015", {modules: false}] + ["es2015", {"modules": false}] ], "plugins": [ "transform-class-properties", diff --git a/.circleci/config.yml b/.circleci/config.yml index 9e1ee91e0..2fe14ad6e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 \ No newline at end of file + # - test_integration_safari diff --git a/.circleci/e2e.sh b/.circleci/e2e.sh index 2d9fc25af..826736bfc 100755 --- a/.circleci/e2e.sh +++ b/.circleci/e2e.sh @@ -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 diff --git a/.eslintignore b/.eslintignore index a9543d933..bdce1b9f9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,5 @@ **/*.html dist -docs node_modules public - +**/*.min.js diff --git a/.gitignore b/.gitignore index a6630d22e..1235e0db0 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.nodemon.json b/.nodemon.json index 101104f4a..5e192d80c 100644 --- a/.nodemon.json +++ b/.nodemon.json @@ -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": [ ".", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33324ea5a..1ee0ce6b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/. diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild index 5739d6f95..3e837aad6 100644 --- a/Dockerfile.onbuild +++ b/Dockerfile.onbuild @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index db9f65c3d..6bdb7fda0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bin/cli-assets b/bin/cli-assets index 78229d91b..210b9f22e 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -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) { diff --git a/bin/cli-serve b/bin/cli-serve index b8dbd4594..d40edc9e4 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -10,6 +10,12 @@ const serve = require('../serve'); program .option('-j, --jobs', 'enable job processing on this thread') + .option( + '--disabled-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); }); diff --git a/bin/cli-setup b/bin/cli-setup index 282a8a2d7..572e8c7fc 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -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, diff --git a/bin/cli-users b/bin/cli-users index ea308ae44..cfcbcd13a 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -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 ') .description('delete a user') diff --git a/bin/templates/plugin/client/components/MyPluginComponent.js b/bin/templates/plugin/client/components/MyPluginComponent.js index 98488b133..fc7a50ece 100644 --- a/bin/templates/plugin/client/components/MyPluginComponent.js +++ b/bin/templates/plugin/client/components/MyPluginComponent.js @@ -12,7 +12,7 @@ class MyPluginComponent extends React.Component { To read more about plugins check{' '} - + our docs and guides! diff --git a/bin/templates/plugin/client/index.js b/bin/templates/plugin/client/index.js index 72a7fe6a7..6735bacb0 100644 --- a/bin/templates/plugin/client/index.js +++ b/bin/templates/plugin/client/index.js @@ -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'; diff --git a/bin/util.js b/bin/util.js index e80f123a5..648c79bcc 100644 --- a/bin/util.js +++ b/bin/util.js @@ -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 diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 9d68d592b..28255aeb5 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -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 = ( - + + + + + + + + {/* Community Routes */} diff --git a/client/coral-admin/src/actions/configure.js b/client/coral-admin/src/actions/configure.js index acc30be1b..47c7fd25f 100644 --- a/client/coral-admin/src/actions/configure.js +++ b/client/coral-admin/src/actions/configure.js @@ -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 }; }; diff --git a/client/coral-admin/src/components/AccountHistory.js b/client/coral-admin/src/components/AccountHistory.js index a16b334ca..0d85b62af 100644 --- a/client/coral-admin/src/components/AccountHistory.js +++ b/client/coral-admin/src/components/AccountHistory.js @@ -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', diff --git a/client/coral-admin/src/components/CommentDetails.js b/client/coral-admin/src/components/CommentDetails.js index dc1be2d85..a9c0884a4 100644 --- a/client/coral-admin/src/components/CommentDetails.js +++ b/client/coral-admin/src/components/CommentDetails.js @@ -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 ( ); @@ -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, diff --git a/client/coral-admin/src/components/CommentFormatter.js b/client/coral-admin/src/components/CommentFormatter.js deleted file mode 100644 index 8874d123f..000000000 --- a/client/coral-admin/src/components/CommentFormatter.js +++ /dev/null @@ -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 ? {token} : 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({match.text}); - 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 ( - - {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 ( - - {content} - {i !== textbreaks.length - 1 && ( -
- )} -
- ); - })} -
- ); -}; - -CommentFormatter.propTypes = { - className: PropTypes.string, - bannedWords: PropTypes.array, - suspectWords: PropTypes.array, - body: PropTypes.string, -}; - -export default CommentFormatter; diff --git a/client/coral-admin/src/components/CommentLabels.js b/client/coral-admin/src/components/CommentLabels.js index 8eac152bd..1f71d9771 100644 --- a/client/coral-admin/src/components/CommentLabels.js +++ b/client/coral-admin/src/components/CommentLabels.js @@ -43,6 +43,9 @@ const CommentLabels = ({ comment, comment: { className, status, actions, hasParent }, }) => { + const slotPassthrough = { + comment, + }; return (
@@ -69,7 +72,7 @@ const CommentLabels = ({
); diff --git a/client/coral-admin/src/components/Forbidden.css b/client/coral-admin/src/components/Forbidden.css new file mode 100644 index 000000000..8f93cf869 --- /dev/null +++ b/client/coral-admin/src/components/Forbidden.css @@ -0,0 +1,8 @@ +.container { + max-width: 1280px; + margin: 0 auto; +} + +.copy { + padding: 20px 0; +} \ No newline at end of file diff --git a/client/coral-admin/src/components/Forbidden.js b/client/coral-admin/src/components/Forbidden.js new file mode 100644 index 000000000..0c50e6d76 --- /dev/null +++ b/client/coral-admin/src/components/Forbidden.js @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './Forbidden.css'; + +const Forbidden = () => ( +
+

+ This page is for team use only. Please contact an administrator if you + want to join this team. +

+
+); + +export default Forbidden; diff --git a/client/coral-admin/src/components/IfHasLink.js b/client/coral-admin/src/components/IfHasLink.js index 8209a28e5..35be8c967 100644 --- a/client/coral-admin/src/components/IfHasLink.js +++ b/client/coral-admin/src/components/IfHasLink.js @@ -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); diff --git a/client/coral-admin/src/components/Layout.css b/client/coral-admin/src/components/Layout.css index ac13e96f7..9244c7c80 100644 --- a/client/coral-admin/src/components/Layout.css +++ b/client/coral-admin/src/components/Layout.css @@ -1,4 +1,6 @@ .layout { margin: 0 auto; background-color: #FAFAFA; -} + height: inherit; + min-height: calc(100vh - 58px); +} \ No newline at end of file diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index b1f68bac2..31ad999dc 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -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 (
- +
@@ -301,7 +301,6 @@ class UserDetail extends React.Component {
- Story: {comment.asset.title} + {t('common.story')}:{' '} + {comment.asset.title ? comment.asset.title : comment.asset.url} { {t('modqueue.moderate')} @@ -88,15 +88,13 @@ class UserDetailComment extends React.Component {
- + ); } @@ -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, diff --git a/client/coral-admin/src/components/UserDetailCommentList.js b/client/coral-admin/src/components/UserDetailCommentList.js index a0ff9982b..3a31ee3f9 100644 --- a/client/coral-admin/src/components/UserDetailCommentList.js +++ b/client/coral-admin/src/components/UserDetailCommentList.js @@ -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, diff --git a/client/coral-admin/src/constants/configure.js b/client/coral-admin/src/constants/configure.js index 05673b5aa..9ab22580d 100644 --- a/client/coral-admin/src/constants/configure.js +++ b/client/coral-admin/src/constants/configure.js @@ -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`; diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js index aad9971c4..580b20f9b 100644 --- a/client/coral-admin/src/containers/Layout.js +++ b/client/coral-admin/src/containers/Layout.js @@ -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 ( -

- This page is for team use only. Please contact an administrator if - you want to join this team. -

+
); } diff --git a/client/coral-admin/src/reducers/configure.js b/client/coral-admin/src/reducers/configure.js index 9809b0fa9..c87463423 100644 --- a/client/coral-admin/src/reducers/configure.js +++ b/client/coral-admin/src/reducers/configure.js @@ -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; } diff --git a/client/coral-admin/src/routes/Community/components/People.css b/client/coral-admin/src/routes/Community/components/People.css index c4561ace1..669da480d 100644 --- a/client/coral-admin/src/routes/Community/components/People.css +++ b/client/coral-admin/src/routes/Community/components/People.css @@ -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; +} + diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index 1f5e4bab8..5645f88f1 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -200,24 +200,31 @@ class People extends React.Component { setUserRole(user.id, role)} >
+ + Cancel + + + +
+ +); + +SaveChangesDialog.propTypes = { + saveDialog: PropTypes.bool.isRequired, + hideSaveDialog: PropTypes.func.isRequired, + saveChanges: PropTypes.func.isRequired, + discardChanges: PropTypes.func.isRequired, +}; + +export default SaveChangesDialog; diff --git a/client/coral-admin/src/routes/Configure/components/StreamSettings.js b/client/coral-admin/src/routes/Configure/components/StreamSettings.js index c447b8453..e6424848f 100644 --- a/client/coral-admin/src/routes/Configure/components/StreamSettings.js +++ b/client/coral-admin/src/routes/Configure/components/StreamSettings.js @@ -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 ( @@ -220,13 +220,7 @@ class StreamSettings extends React.Component { {/* the above card should be the last one if at all possible because of z-index issues with the selects */} - + ); } @@ -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; diff --git a/client/coral-admin/src/routes/Configure/components/TechSettings.js b/client/coral-admin/src/routes/Configure/components/TechSettings.js index 8bb10e9a3..e6d9ba8af 100644 --- a/client/coral-admin/src/routes/Configure/components/TechSettings.js +++ b/client/coral-admin/src/routes/Configure/components/TechSettings.js @@ -34,7 +34,7 @@ class TechSettings extends React.Component { }; render() { - const { settings, data, root, errors, updatePending } = this.props; + const { settings, slotPassthrough } = this.props; return ( - + ); } @@ -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; diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js index fe87766b6..9f980d31b 100644 --- a/client/coral-admin/src/routes/Configure/containers/Configure.js +++ b/client/coral-admin/src/routes/Configure/containers/Configure.js @@ -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
{this.props.data.error.message}
; @@ -30,15 +85,20 @@ class ConfigureContainer extends Component { return ( + > + {this.props.children} + ); } } @@ -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, }; diff --git a/client/coral-admin/src/routes/Configure/containers/ModerationSettings.js b/client/coral-admin/src/routes/Configure/containers/ModerationSettings.js index c5e1e401a..8882477d1 100644 --- a/client/coral-admin/src/routes/Configure/containers/ModerationSettings.js +++ b/client/coral-admin/src/routes/Configure/containers/ModerationSettings.js @@ -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); diff --git a/client/coral-admin/src/routes/Configure/containers/StreamSettings.js b/client/coral-admin/src/routes/Configure/containers/StreamSettings.js index 24e8ad459..5bc8e69a9 100644 --- a/client/coral-admin/src/routes/Configure/containers/StreamSettings.js +++ b/client/coral-admin/src/routes/Configure/containers/StreamSettings.js @@ -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); diff --git a/client/coral-admin/src/routes/Configure/containers/TechSettings.js b/client/coral-admin/src/routes/Configure/containers/TechSettings.js index c096d1af9..7fbe81cdb 100644 --- a/client/coral-admin/src/routes/Configure/containers/TechSettings.js +++ b/client/coral-admin/src/routes/Configure/containers/TechSettings.js @@ -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); diff --git a/client/coral-admin/src/routes/Install/components/Steps/FinalStep.js b/client/coral-admin/src/routes/Install/components/Steps/FinalStep.js index 72fd67a34..b0a0c43ef 100644 --- a/client/coral-admin/src/routes/Install/components/Steps/FinalStep.js +++ b/client/coral-admin/src/routes/Install/components/Steps/FinalStep.js @@ -10,12 +10,14 @@ const InitialStep = () => { return (

{t('install.final.description')}

- - + + + + + +
); }; diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index f865908b2..28dcf8ca8 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -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 (
  • - Story: {comment.asset.title} + {t('common.story')}:{' '} + {comment.asset.title ? comment.asset.title : comment.asset.url} {!currentAsset && ( {t('modqueue.moderate')} @@ -134,13 +138,10 @@ class Comment extends React.Component {
    - + ); } diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css index 465c7ccbd..cef4184db 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css @@ -6,10 +6,6 @@ margin-top: 16px; } -:global(html) { - height: inherit; -} - .list { outline: none; } diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js index 0e7cbe347..e47a162ef 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js @@ -344,7 +344,6 @@ class ModerationQueue extends React.Component { child = (
    - + ); } diff --git a/client/coral-admin/src/routes/Stories/components/Stories.css b/client/coral-admin/src/routes/Stories/components/Stories.css index 63a05ab6a..28f67151d 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.css +++ b/client/coral-admin/src/routes/Stories/components/Stories.css @@ -89,3 +89,12 @@ display: inline-block; } } + +.statusDropdown { + width: 150px; +} + +.statusDropdownOption { + min-width: 100px; +} + diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index f94685fe8..ad2c7c5fc 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -22,11 +22,20 @@ class Stories extends Component { const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now()); return ( this.props.onStatusChange(value, id)} > - ); }; diff --git a/client/coral-admin/src/utils/index.js b/client/coral-admin/src/utils/index.js index 41ff3db3c..180615a87 100644 --- a/client/coral-admin/src/utils/index.js +++ b/client/coral-admin/src/utils/index.js @@ -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) => diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index bc31c90e9..0f43ffe10 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -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, }); diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index 0e251e9a3..0c4fed129 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -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 (
    - + 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 ( - - {React.cloneElement(el, { - active: this.props.activeTab === el.type.talkPluginName, - })} - - ); - }); - } + createPluginTabFactory = (props = this.props) => el => { + return ( + + {React.cloneElement(el, { + active: props.activeTab === el.key, + })} + + ); + }; getTabElements(props = this.props) { const elements = [...this.getPluginTabElementsPrepend(props)]; @@ -92,14 +54,16 @@ class ExtendableTabPanelContainer extends React.Component { return elements; } + createPluginTabPane(el) { + return ( + + {el} + + ); + } + getPluginTabPaneElements(props = this.props) { - return this.getSlotElements(props.tabPaneSlot).map(el => { - return ( - - {el} - - ); - }); + 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); diff --git a/client/coral-embed-stream/src/reducers/configure.js b/client/coral-embed-stream/src/reducers/configure.js index 41d87f8d8..48b28ec72 100644 --- a/client/coral-embed-stream/src/reducers/configure.js +++ b/client/coral-embed-stream/src/reducers/configure.js @@ -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; diff --git a/client/coral-embed-stream/src/tabs/configure/components/Configure.js b/client/coral-embed-stream/src/tabs/configure/components/Configure.js index b4b925d92..566aefe00 100644 --- a/client/coral-embed-stream/src/tabs/configure/components/Configure.js +++ b/client/coral-embed-stream/src/tabs/configure/components/Configure.js @@ -7,11 +7,7 @@ class Configure extends React.Component { render() { return (
    - +
    @@ -20,7 +16,6 @@ class Configure extends React.Component { } Configure.propTypes = { - data: PropTypes.object.isRequired, root: PropTypes.object.isRequired, asset: PropTypes.object.isRequired, }; diff --git a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js index 30fd8842c..64593fc43 100644 --- a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js +++ b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js @@ -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 = ; - 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 ; - } return (
    diff --git a/client/coral-embed-stream/src/tabs/configure/components/Settings.js b/client/coral-embed-stream/src/tabs/configure/components/Settings.js index 7e11b1311..2b1e49c84 100644 --- a/client/coral-embed-stream/src/tabs/configure/components/Settings.js +++ b/client/coral-embed-stream/src/tabs/configure/components/Settings.js @@ -25,9 +25,9 @@ class Settings extends React.Component { onQuestionBoxContentChange, canSave, onApply, - slotProps, - queryData, + slotPassthrough, } = this.props; + return (
    @@ -77,7 +77,7 @@ class Settings extends React.Component {
    )} - +
    ); @@ -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, diff --git a/client/coral-embed-stream/src/tabs/configure/containers/Configure.js b/client/coral-embed-stream/src/tabs/configure/containers/Configure.js index b6104b067..1ba86509e 100644 --- a/client/coral-embed-stream/src/tabs/configure/containers/Configure.js +++ b/client/coral-embed-stream/src/tabs/configure/containers/Configure.js @@ -13,13 +13,7 @@ class ConfigureContainer extends React.Component { return
    {this.props.data.error.message}
    ; } - return ( - - ); + return ; } } diff --git a/client/coral-embed-stream/src/tabs/configure/containers/Settings.js b/client/coral-embed-stream/src/tabs/configure/containers/Settings.js index 2058ceaa2..cfb72c0ae 100644 --- a/client/coral-embed-stream/src/tabs/configure/containers/Settings.js +++ b/client/coral-embed-stream/src/tabs/configure/containers/Settings.js @@ -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 ( ( +
    + +

    {t('comment_history_blank.title')}

    +

    {t('comment_history_blank.info')}

    +
    +); diff --git a/client/coral-embed-stream/src/tabs/profile/components/Comment.css b/client/coral-embed-stream/src/tabs/profile/components/Comment.css index 40a003de1..9004a1e6c 100644 --- a/client/coral-embed-stream/src/tabs/profile/components/Comment.css +++ b/client/coral-embed-stream/src/tabs/profile/components/Comment.css @@ -15,6 +15,7 @@ .main { min-width: 70%; + max-width: 100%; } .sidebar { diff --git a/client/coral-embed-stream/src/tabs/profile/components/Comment.js b/client/coral-embed-stream/src/tabs/profile/components/Comment.js index 69725a362..171172dab 100644 --- a/client/coral-embed-stream/src/tabs/profile/components/Comment.js +++ b/client/coral-embed-stream/src/tabs/profile/components/Comment.js @@ -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 (
    @@ -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} />
  • @@ -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, }; diff --git a/client/coral-embed-stream/src/tabs/profile/components/CommentHistory.js b/client/coral-embed-stream/src/tabs/profile/components/CommentHistory.js index 39f3bfd04..c15314f29 100644 --- a/client/coral-embed-stream/src/tabs/profile/components/CommentHistory.js +++ b/client/coral-embed-stream/src/tabs/profile/components/CommentHistory.js @@ -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 ; + } return (
    @@ -29,7 +33,6 @@ class CommentHistory extends React.Component { return ( ( -
    -
    -

    {username}

    - {emailAddress ?

    {emailAddress}

    : null} +const Profile = ({ username, emailAddress, root, slotPassthrough }) => { + return ( +
    +
    +

    {username}

    + {emailAddress ?

    {emailAddress}

    : null} +
    + +
    - - - {t('framework.my_comments')} - , - ]} - tabPanes={[ - - - , - ]} - sub - /> -
    -); + ); +}; 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; diff --git a/client/coral-embed-stream/src/tabs/profile/components/Settings.js b/client/coral-embed-stream/src/tabs/profile/components/Settings.js new file mode 100644 index 000000000..c61a09a92 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/profile/components/Settings.js @@ -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 ( +
    + +
    + ); + } +} + +Settings.propTypes = { + root: PropTypes.object, +}; + +export default Settings; diff --git a/client/coral-embed-stream/src/tabs/profile/components/TabPanel.js b/client/coral-embed-stream/src/tabs/profile/components/TabPanel.js new file mode 100644 index 000000000..08270bcd8 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/profile/components/TabPanel.js @@ -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 = [ + + {t('framework.my_comments')} + , + ]; + + if (showSettingsTab) { + tabs.push( + + {t('profile_settings')} + + ); + } + + return ( + + + , + + + , + ]} + sub + /> + ); +}; + +TabPanel.propTypes = { + root: PropTypes.object, + slotPassthrough: PropTypes.object, + activeTab: PropTypes.string.isRequired, + setActiveTab: PropTypes.func.isRequired, + showSettingsTab: PropTypes.bool.isRequired, +}; + +export default TabPanel; diff --git a/client/coral-embed-stream/src/tabs/profile/containers/CommentHistory.js b/client/coral-embed-stream/src/tabs/profile/containers/CommentHistory.js index a02225939..27727216e 100644 --- a/client/coral-embed-stream/src/tabs/profile/containers/CommentHistory.js +++ b/client/coral-embed-stream/src/tabs/profile/containers/CommentHistory.js @@ -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 ( ({ export default compose( connect(mapStateToProps, null), - withCommentHistoryFragments + withCommentHistoryFragments, + withFetchMore )(CommentHistoryContainer); diff --git a/client/coral-embed-stream/src/tabs/profile/containers/Profile.js b/client/coral-embed-stream/src/tabs/profile/containers/Profile.js index 82a0a49eb..30d634d94 100644 --- a/client/coral-embed-stream/src/tabs/profile/containers/Profile.js +++ b/client/coral-embed-stream/src/tabs/profile/containers/Profile.js @@ -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 ( ); } @@ -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), diff --git a/client/coral-embed-stream/src/tabs/profile/containers/Settings.js b/client/coral-embed-stream/src/tabs/profile/containers/Settings.js new file mode 100644 index 000000000..795a3c10a --- /dev/null +++ b/client/coral-embed-stream/src/tabs/profile/containers/Settings.js @@ -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 ; + } +} + +const enhance = compose( + withFragments({ + root: gql` + fragment TalkEmbedStream_ProfileSettings_root on RootQuery { + __typename + ${getSlotFragmentSpreads(slots, 'root')} + } + `, + }) +); + +export default enhance(SettingsContainer); diff --git a/client/coral-embed-stream/src/tabs/profile/containers/TabPanel.js b/client/coral-embed-stream/src/tabs/profile/containers/TabPanel.js new file mode 100644 index 000000000..46c24227f --- /dev/null +++ b/client/coral-embed-stream/src/tabs/profile/containers/TabPanel.js @@ -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 ( + 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); diff --git a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js index 3190ab155..9aa2b8a4c 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js @@ -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 ( { return ( @@ -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} />
    @@ -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 ? ( @@ -624,8 +615,7 @@ export default class Comment extends React.Component { {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} />
    )} @@ -678,8 +667,7 @@ export default class Comment extends React.Component {
    @@ -696,9 +684,7 @@ export default class Comment extends React.Component {
    diff --git a/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js b/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js index 65937b3ce..a65950d7d 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js +++ b/client/coral-embed-stream/src/tabs/stream/components/CommentForm.js @@ -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 {
    {this.props.buttonContainerStart} diff --git a/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js b/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js index 202349370..60bf18b40 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js +++ b/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js @@ -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 (
    @@ -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 ( -
    +
    - + {/* Is this slot here legitimate? (kiwi) */}
    {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, }; diff --git a/client/coral-embed-stream/src/tabs/stream/components/DraftAreaContent.js b/client/coral-embed-stream/src/tabs/stream/components/DraftAreaContent.js index 50a481f6f..74e59f795 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/DraftAreaContent.js +++ b/client/coral-embed-stream/src/tabs/stream/components/DraftAreaContent.js @@ -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, -}) => ( -