mirror of
https://github.com/wassname/talk.git
synced 2026-09-09 11:38:08 +08:00
Merge branch 'master' into fix_translation_staff
This commit is contained in:
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -5,14 +5,7 @@ export const singleView = () => ({ type: actions.SINGLE_VIEW });
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => (dispatch, _, { localStorage }) => {
|
||||
try {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
// above will fail in Safari private mode
|
||||
}
|
||||
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import styles from './UserDetail.css';
|
||||
import AccountHistory from './AccountHistory';
|
||||
import UserHistory from './UserHistory';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
import UserDetailCommentList from '../components/UserDetailCommentList';
|
||||
import {
|
||||
@@ -28,26 +28,6 @@ import UserInfoTooltip from './UserInfoTooltip';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
rejectThenReload = async info => {
|
||||
await this.props.rejectComment(info);
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
acceptThenReload = async info => {
|
||||
await this.props.acceptComment(info);
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
bulkAcceptThenReload = async () => {
|
||||
await this.props.bulkAccept();
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
bulkRejectThenReload = async () => {
|
||||
await this.props.bulkReject();
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
changeTab = tab => {
|
||||
this.props.changeTab(tab);
|
||||
};
|
||||
@@ -110,8 +90,14 @@ class UserDetail extends React.Component {
|
||||
unbanUser,
|
||||
unsuspendUser,
|
||||
modal,
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
bulkAccept,
|
||||
bulkReject,
|
||||
} = this.props;
|
||||
|
||||
console.log(rejectedComments, totalComments);
|
||||
|
||||
// if totalComments is 0, you're dividing by zero
|
||||
let rejectedPercent = rejectedComments / totalComments * 100;
|
||||
|
||||
@@ -286,7 +272,7 @@ class UserDetail extends React.Component {
|
||||
'talk-admin-user-detail-history-tab'
|
||||
)}
|
||||
>
|
||||
{t('user_detail.account_history')}
|
||||
{t('user_detail.user_history')}
|
||||
</Tab>
|
||||
</TabBar>
|
||||
|
||||
@@ -304,12 +290,12 @@ class UserDetail extends React.Component {
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
acceptComment={acceptComment}
|
||||
rejectComment={rejectComment}
|
||||
selectedCommentIds={selectedCommentIds}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
bulkAcceptThenReload={this.bulkAcceptThenReload}
|
||||
bulkRejectThenReload={this.bulkRejectThenReload}
|
||||
bulkAcceptThenReload={bulkAccept}
|
||||
bulkRejectThenReload={bulkReject}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane
|
||||
@@ -322,19 +308,19 @@ class UserDetail extends React.Component {
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
acceptComment={acceptComment}
|
||||
rejectComment={rejectComment}
|
||||
selectedCommentIds={selectedCommentIds}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
bulkAcceptThenReload={this.bulkAcceptThenReload}
|
||||
bulkRejectThenReload={this.bulkRejectThenReload}
|
||||
bulkAcceptThenReload={bulkAccept}
|
||||
bulkRejectThenReload={bulkReject}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tabId={'history'}
|
||||
className={'talk-admin-user-detail-history-tab-pane'}
|
||||
>
|
||||
<AccountHistory user={user} />
|
||||
<UserHistory user={user} />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</Drawer>
|
||||
|
||||
+19
-21
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { murmur3 } from 'murmurhash-js';
|
||||
import styles from './AccountHistory.css';
|
||||
import styles from './UserHistory.css';
|
||||
import cn from 'classnames';
|
||||
import flatten from 'lodash/flatten';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
@@ -43,15 +43,15 @@ const readableDuration = (startDate, endDate) => {
|
||||
const buildActionResponse = (typename, created_at, until, status) => {
|
||||
switch (typename) {
|
||||
case 'UsernameStatusHistory':
|
||||
return t('account_history.username_status', status);
|
||||
return t('user_history.username_status', status);
|
||||
case 'BannedStatusHistory':
|
||||
return status
|
||||
? t('account_history.user_banned')
|
||||
: t('account_history.ban_removed');
|
||||
? t('user_history.user_banned')
|
||||
: t('user_history.ban_removed');
|
||||
case 'SuspensionStatusHistory':
|
||||
return until
|
||||
? t('account_history.suspended', readableDuration(created_at, until))
|
||||
: t('account_history.suspension_removed');
|
||||
? t('user_history.suspended', readableDuration(created_at, until))
|
||||
: t('user_history.suspension_removed');
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
@@ -62,43 +62,41 @@ const getModerationValue = assignedBy =>
|
||||
assignedBy.username
|
||||
) : (
|
||||
<span>
|
||||
<Icon name="computer" /> {t('account_history.system')}
|
||||
<Icon name="computer" /> {t('user_history.system')}
|
||||
</span>
|
||||
);
|
||||
|
||||
class AccountHistory extends React.Component {
|
||||
class UserHistory extends React.Component {
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
const userHistory = buildUserHistory(user.state);
|
||||
return (
|
||||
<div>
|
||||
<div className={cn(styles.table, 'talk-admin-account-history')}>
|
||||
<div className={cn(styles.table, 'talk-admin-user-history')}>
|
||||
<div
|
||||
className={cn(
|
||||
styles.headerRow,
|
||||
'talk-admin-account-history-header-row'
|
||||
'talk-admin-user-history-header-row'
|
||||
)}
|
||||
>
|
||||
<div className={styles.headerRowItem}>{t('user_history.date')}</div>
|
||||
<div className={styles.headerRowItem}>
|
||||
{t('account_history.date')}
|
||||
{t('user_history.action')}
|
||||
</div>
|
||||
<div className={styles.headerRowItem}>
|
||||
{t('account_history.action')}
|
||||
</div>
|
||||
<div className={styles.headerRowItem}>
|
||||
{t('account_history.taken_by')}
|
||||
{t('user_history.taken_by')}
|
||||
</div>
|
||||
</div>
|
||||
{userHistory.map(
|
||||
({ __typename, created_at, assigned_by, until, status }) => (
|
||||
<div
|
||||
className={cn(styles.row, 'talk-admin-account-history-row')}
|
||||
className={cn(styles.row, 'talk-admin-user-history-row')}
|
||||
key={`${__typename}_${murmur3(created_at)}`}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
styles.item,
|
||||
'talk-admin-account-history-row-date'
|
||||
'talk-admin-user-history-row-date'
|
||||
)}
|
||||
>
|
||||
{moment(new Date(created_at)).format('MMM DD, YYYY')}
|
||||
@@ -107,7 +105,7 @@ class AccountHistory extends React.Component {
|
||||
className={cn(
|
||||
styles.item,
|
||||
styles.action,
|
||||
'talk-admin-account-history-row-status'
|
||||
'talk-admin-user-history-row-status'
|
||||
)}
|
||||
>
|
||||
{buildActionResponse(__typename, created_at, until, status)}
|
||||
@@ -116,7 +114,7 @@ class AccountHistory extends React.Component {
|
||||
className={cn(
|
||||
styles.item,
|
||||
styles.username,
|
||||
'talk-admin-account-history-row-assigned-by'
|
||||
'talk-admin-user-history-row-assigned-by'
|
||||
)}
|
||||
>
|
||||
{getModerationValue(assigned_by)}
|
||||
@@ -130,8 +128,8 @@ class AccountHistory extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
AccountHistory.propTypes = {
|
||||
UserHistory.propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default AccountHistory;
|
||||
export default UserHistory;
|
||||
@@ -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`;
|
||||
|
||||
@@ -148,6 +148,7 @@ UserDetailContainer.propTypes = {
|
||||
selectedCommentIds: PropTypes.array,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string,
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
@@ -245,7 +246,6 @@ export const withUserDetailQuery = withQuery(
|
||||
options: ({ userId, statuses }) => {
|
||||
return {
|
||||
variables: { author_id: userId, statuses },
|
||||
fetchPolicy: 'network-only',
|
||||
};
|
||||
},
|
||||
skip: ownProps => !ownProps.userId,
|
||||
|
||||
@@ -15,7 +15,8 @@ import { hideShortcutsNote } from './actions/moderation';
|
||||
smoothscroll.polyfill();
|
||||
|
||||
function init({ store, localStorage }) {
|
||||
if (localStorage && localStorage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
const shouldHide = localStorage.getItem('coral:shortcutsNote') === 'hide';
|
||||
if (shouldHide) {
|
||||
store.dispatch(hideShortcutsNote());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,10 +61,7 @@ export default class Configure extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
<SectionComponent
|
||||
root={this.props.root}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
{React.cloneElement(this.props.children, passProps)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -86,10 +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,
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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,14 +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}
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -74,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,
|
||||
@@ -93,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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { GraphQLDocs } from 'graphql-docs';
|
||||
|
||||
import fetcher from './services/fetcher';
|
||||
|
||||
// Render the application into the DOM
|
||||
render(<GraphQLDocs fetcher={fetcher} />, document.querySelector('#root'));
|
||||
@@ -1,10 +0,0 @@
|
||||
export default function fetcher(query) {
|
||||
return fetch(`${window.location.origin}/api/v1/graph/ql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -24,14 +24,20 @@ export default class Embed extends React.Component {
|
||||
>
|
||||
{t('embed_comments_tab')}
|
||||
</Tab>,
|
||||
<Tab
|
||||
key="profile"
|
||||
tabId="profile"
|
||||
className="talk-embed-stream-profile-tab"
|
||||
>
|
||||
{t('framework.my_profile')}
|
||||
</Tab>,
|
||||
];
|
||||
|
||||
if (this.props.currentUser) {
|
||||
tabs.push(
|
||||
<Tab
|
||||
key="profile"
|
||||
tabId="profile"
|
||||
className="talk-embed-stream-profile-tab"
|
||||
>
|
||||
{t('framework.my_profile')}
|
||||
</Tab>
|
||||
);
|
||||
}
|
||||
|
||||
if (can(this.props.currentUser, 'UPDATE_ASSET_CONFIG')) {
|
||||
tabs.push(
|
||||
<Tab
|
||||
@@ -43,6 +49,7 @@ export default class Embed extends React.Component {
|
||||
</Tab>
|
||||
);
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@ class ExtendableTabPanel extends React.Component {
|
||||
} = this.props;
|
||||
return (
|
||||
<div {...rest}>
|
||||
<TabBar activeTab={activeTab} onTabClick={setActiveTab} sub={sub}>
|
||||
{tabs}
|
||||
</TabBar>
|
||||
{tabs && (
|
||||
<TabBar activeTab={activeTab} onTabClick={setActiveTab} sub={sub}>
|
||||
{tabs}
|
||||
</TabBar>
|
||||
)}
|
||||
{loading ? (
|
||||
<div className={styles.spinnerContainer}>
|
||||
<Spinner />
|
||||
|
||||
@@ -35,12 +35,9 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
|
||||
createPluginTabFactory = (props = this.props) => el => {
|
||||
return (
|
||||
<ExtendableTab
|
||||
tabId={el.type.talkPluginName}
|
||||
key={el.type.talkPluginName}
|
||||
>
|
||||
<ExtendableTab tabId={el.key} key={el.key}>
|
||||
{React.cloneElement(el, {
|
||||
active: props.activeTab === el.type.talkPluginName,
|
||||
active: props.activeTab === el.key,
|
||||
})}
|
||||
</ExtendableTab>
|
||||
);
|
||||
@@ -59,7 +56,7 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
|
||||
createPluginTabPane(el) {
|
||||
return (
|
||||
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
|
||||
<TabPane tabId={el.key} key={el.key}>
|
||||
{el}
|
||||
</TabPane>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ class Comment extends React.Component {
|
||||
defaultComponent={CommentContent}
|
||||
className={cn(styles.commentBody, 'my-comment-body')}
|
||||
passthrough={slotPassthrough}
|
||||
size={1}
|
||||
/>
|
||||
<div className={cn(styles.commentSummary, 'comment-summary')}>
|
||||
<span
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
.message {
|
||||
padding: 10px 0 20px;
|
||||
letter-spacing: 0.1px;
|
||||
font-size: 13px;
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
.message a {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './NotLoggedIn.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export default ({ showSignInDialog }) => (
|
||||
<div className={cn(styles.message, 'talk-embed-stream-not-logged-in')}>
|
||||
<div>
|
||||
<a onClick={showSignInDialog}>{t('settings.sign_in')}</a>{' '}
|
||||
{t('settings.to_access')}
|
||||
</div>
|
||||
<div>{t('from_settings_page')}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -2,27 +2,17 @@ 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 { withQuery } from 'coral-framework/hocs';
|
||||
import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import Profile from '../components/Profile';
|
||||
import TabPanel from './TabPanel';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
class ProfileContainer extends Component {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.currentUser && nextProps.currentUser) {
|
||||
// Refetch after login.
|
||||
this.props.data.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentUser, showSignInDialog, root } = this.props;
|
||||
const { currentUser, root } = this.props;
|
||||
const { me } = this.props.root;
|
||||
const loading = this.props.data.loading;
|
||||
|
||||
@@ -30,10 +20,6 @@ class ProfileContainer extends Component {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
|
||||
}
|
||||
|
||||
if (loading || !me) {
|
||||
return <Spinner />;
|
||||
}
|
||||
@@ -57,7 +43,6 @@ ProfileContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showSignInDialog: PropTypes.func,
|
||||
};
|
||||
|
||||
const slots = ['profileSections'];
|
||||
@@ -85,10 +70,6 @@ const mapStateToProps = state => ({
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ showSignInDialog }, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withProfileQuery
|
||||
)(ProfileContainer);
|
||||
export default compose(connect(mapStateToProps), withProfileQuery)(
|
||||
ProfileContainer
|
||||
);
|
||||
|
||||
@@ -18,6 +18,10 @@ const InactiveCommentLabel = ({ status, className, ...rest }) => {
|
||||
label = t('modqueue.rejected');
|
||||
icon = 'close';
|
||||
break;
|
||||
case 'SYSTEM_WITHHELD':
|
||||
label = t('modqueue.system_withheld');
|
||||
icon = 'flag';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown inactive status ${status}`);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ export const checkLogin = () => (
|
||||
rest('/auth')
|
||||
.then(result => {
|
||||
if (!result.user) {
|
||||
if (localStorage) {
|
||||
cleanAuthData(localStorage);
|
||||
}
|
||||
cleanAuthData(localStorage);
|
||||
dispatch(checkLoginSuccess(null));
|
||||
return;
|
||||
}
|
||||
@@ -52,10 +50,12 @@ const checkLoginSuccess = user => ({
|
||||
});
|
||||
|
||||
export const setAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
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());
|
||||
};
|
||||
@@ -66,11 +66,8 @@ export const handleSuccessfulLogin = (user, token) => (
|
||||
{ client, localStorage, postMessage }
|
||||
) => {
|
||||
const { exp } = jwtDecode(token);
|
||||
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
localStorage.setItem('exp', exp);
|
||||
localStorage.setItem('token', token);
|
||||
|
||||
// Send the message via the messages service to the window.opener if it
|
||||
// exists.
|
||||
@@ -87,6 +84,7 @@ export const handleSuccessfulLogin = (user, token) => (
|
||||
dispatch({
|
||||
type: actions.HANDLE_SUCCESSFUL_LOGIN,
|
||||
user,
|
||||
token,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -100,9 +98,8 @@ export const logout = () => async (
|
||||
) => {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
|
||||
if (localStorage) {
|
||||
cleanAuthData(localStorage);
|
||||
}
|
||||
// Clear the auth data persisted to localStorage.
|
||||
cleanAuthData(localStorage);
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
@@ -7,7 +7,7 @@ class IfSlotIsNotEmpty extends React.Component {
|
||||
isSlotEmpty(props = this.props) {
|
||||
const { slotElements } = props;
|
||||
return slotElements.length === 0
|
||||
? false
|
||||
? true
|
||||
: slotElements.every(elements => elements.length === 0);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ 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,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const prefix = `TALK_FRAMEWORK`;
|
||||
|
||||
export const SET_AUTH_TOKEN = `${prefix}_SET_AUTH_TOKEN`;
|
||||
|
||||
export const CHECK_LOGIN_REQUEST = `${prefix}_CHECK_LOGIN_REQUEST`;
|
||||
export const CHECK_LOGIN_SUCCESS = `${prefix}_CHECK_LOGIN_SUCCESS`;
|
||||
export const CHECK_LOGIN_FAILURE = `${prefix}_CHECK_LOGIN_FAILURE`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { gql } from 'react-apollo';
|
||||
import withMutation from '../hocs/withMutation';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
function convertItemType(item_type) {
|
||||
switch (item_type) {
|
||||
@@ -167,9 +168,39 @@ export const withSetCommentStatus = withMutation(
|
||||
errors: null,
|
||||
},
|
||||
},
|
||||
updateQueries: {
|
||||
CoralAdmin_UserDetail: prev => {
|
||||
const increment = {
|
||||
rejectedComments: {
|
||||
$apply: count =>
|
||||
count < prev.totalComments ? count + 1 : count,
|
||||
},
|
||||
};
|
||||
|
||||
const decrement = {
|
||||
rejectedComments: {
|
||||
$apply: count => (count > 0 ? count - 1 : 0),
|
||||
},
|
||||
};
|
||||
|
||||
// If rejected then increment rejectedComments by one
|
||||
if (status === 'REJECTED') {
|
||||
const updated = update(prev, increment);
|
||||
return updated;
|
||||
}
|
||||
|
||||
// If approved then decrement rejectedComments by one
|
||||
if (status === 'ACCEPTED') {
|
||||
const updated = update(prev, decrement);
|
||||
return updated;
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
},
|
||||
update: proxy => {
|
||||
const fragment = gql`
|
||||
fragment Talk_SetCommentStatus on Comment {
|
||||
fragment Talk_SetCommentStatus_Comment on Comment {
|
||||
status
|
||||
status_history {
|
||||
type
|
||||
@@ -182,9 +213,11 @@ export const withSetCommentStatus = withMutation(
|
||||
const data = proxy.readFragment({ fragment, id: fragmentId });
|
||||
|
||||
data.status = status;
|
||||
|
||||
data.status_history = data.status_history
|
||||
? data.status_history
|
||||
: [];
|
||||
|
||||
data.status_history.push({
|
||||
__typename: 'CommentStatusHistory',
|
||||
type: status,
|
||||
|
||||
@@ -5,6 +5,7 @@ const initialState = {
|
||||
checkedInitialLogin: false,
|
||||
initialLoginError: null,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
|
||||
const purge = user => {
|
||||
@@ -14,12 +15,18 @@ const purge = user => {
|
||||
|
||||
export default function auth(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SET_AUTH_TOKEN:
|
||||
return {
|
||||
...state,
|
||||
token: action.token || null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
initialLoginError: action.error,
|
||||
checkedInitialLogin: true,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return {
|
||||
@@ -31,11 +38,13 @@ export default function auth(state = initialState, action) {
|
||||
return {
|
||||
...state,
|
||||
user: action.user ? purge(action.user) : null,
|
||||
token: action.token || null,
|
||||
};
|
||||
case actions.LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
case actions.UPDATE_STATUS: {
|
||||
return {
|
||||
|
||||
+7
-1
@@ -47,6 +47,12 @@ const getAuthToken = (store, storage) => {
|
||||
} else if (!bowser.safari && !bowser.ios && storage) {
|
||||
// Use local storage auth tokens where there's a stable api.
|
||||
return storage.getItem('token');
|
||||
} else if (state.auth && state.auth.token) {
|
||||
// Use the redux token state if the remaining methods fall out. If the embed
|
||||
// is called with `embed.login(token)`, and the browser is not capable of
|
||||
// storing the token in localStorage, then we would have persisted it to the
|
||||
// redux state.
|
||||
return state.auth.token;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -123,7 +129,7 @@ export async function createContext({
|
||||
// Try to get the token from localStorage. If it isn't here, it may
|
||||
// be passed as a cookie.
|
||||
|
||||
// NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT
|
||||
// NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERENT
|
||||
// TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT.
|
||||
return getAuthToken(store, localStorage);
|
||||
};
|
||||
|
||||
@@ -52,26 +52,41 @@ let lang;
|
||||
let timeagoInstance;
|
||||
|
||||
function setLocale(storage, locale) {
|
||||
try {
|
||||
if (storage) {
|
||||
storage.setItem('locale', locale);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
storage.setItem('locale', locale);
|
||||
}
|
||||
|
||||
function getLocale(storage) {
|
||||
// detectLanguage will try to get the locale from storage if available,
|
||||
// otherwise will try to get it from the navigator, otherwise, it will fallback
|
||||
// to the default language.
|
||||
function detectLanguage(storage) {
|
||||
try {
|
||||
return (
|
||||
(storage && storage.getItem('locale')) ||
|
||||
navigator.language ||
|
||||
defaultLanguage
|
||||
).split('-')[0];
|
||||
const lang = storage.getItem('locale') || navigator.language;
|
||||
if (lang) {
|
||||
return lang;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
console.warn(
|
||||
'Error while trying to detect language, will fallback to',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
console.warn('Could not detect language, will fallback to', defaultLanguage);
|
||||
return defaultLanguage;
|
||||
}
|
||||
|
||||
// getLocale will get the users locale from the local detector and parse it to a
|
||||
// format we can work with.
|
||||
function getLocale(storage) {
|
||||
// Get the language from the local detector.
|
||||
const lang = detectLanguage(storage);
|
||||
|
||||
// Some language strings come with additional subtags as defined in:
|
||||
//
|
||||
// https://www.ietf.org/rfc/bcp/bcp47.txt
|
||||
//
|
||||
// So we should strip that off if we find it.
|
||||
return lang.split('-')[0];
|
||||
}
|
||||
|
||||
export function setupTranslations() {
|
||||
@@ -136,6 +151,7 @@ export function t(key, ...replacements) {
|
||||
replacements.forEach((str, i) => {
|
||||
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
|
||||
});
|
||||
|
||||
return translation;
|
||||
} else {
|
||||
console.warn(`${lang}.${key} and en.${key} language key not set`);
|
||||
|
||||
@@ -173,11 +173,30 @@ class PluginsService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This adds a consistent keying for the slot elements.
|
||||
* It uses the plugin name as the key. If the same plugin inserts
|
||||
* multiple elements it will append `.${noOfOccurence}` to the
|
||||
* key starting with the second element.
|
||||
*/
|
||||
const getKey = (() => {
|
||||
const map = {};
|
||||
return component => {
|
||||
if (map[component.talkPluginName] === undefined) {
|
||||
map[component.talkPluginName] = 0;
|
||||
} else {
|
||||
map[component.talkPluginName]++;
|
||||
}
|
||||
const i = map[component.talkPluginName];
|
||||
return `${component.talkPluginName}${i > 0 ? `.${i}` : ''}`;
|
||||
};
|
||||
})();
|
||||
|
||||
return (size > 0 ? slots.slice(0, size) : slots)
|
||||
.map((component, i) => ({
|
||||
.map(component => ({
|
||||
component,
|
||||
disabled: isDisabled(component),
|
||||
key: i,
|
||||
key: getKey(component),
|
||||
}))
|
||||
.filter(o => !o.disabled)
|
||||
.map(({ component, key }) =>
|
||||
|
||||
@@ -1,40 +1,116 @@
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
function getStorage(type) {
|
||||
let storage;
|
||||
try {
|
||||
storage = window[type];
|
||||
const x = '__storage_test__';
|
||||
storage.setItem(x, x);
|
||||
storage.removeItem(x);
|
||||
} catch (e) {
|
||||
const ignore =
|
||||
e instanceof DOMException &&
|
||||
// everything except Firefox
|
||||
(e.code === 22 ||
|
||||
// SecurityError related to having 3rd party cookies disabled.
|
||||
e.code === 18 ||
|
||||
// Firefox
|
||||
function testStorageAccess(storage) {
|
||||
const key = '__storage_test__';
|
||||
|
||||
e.code === 1014 ||
|
||||
// test name field too, because code might not be present
|
||||
// Create a unique test value.
|
||||
const expectedValue = String(Date.now());
|
||||
|
||||
// everything except Firefox
|
||||
e.name === 'QuotaExceededError' ||
|
||||
// Firefox
|
||||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED');
|
||||
if (!ignore) {
|
||||
console.warn(e);
|
||||
// Try to set, get, and remove that item.
|
||||
storage.setItem(key, expectedValue);
|
||||
const canSetGet = expectedValue === storage.getItem(key);
|
||||
storage.removeItem(key);
|
||||
|
||||
if (!canSetGet) {
|
||||
// We can't access the desired storage!
|
||||
throw new Error('Storage access test failed');
|
||||
}
|
||||
}
|
||||
|
||||
// InMemoryStorage is a dumb implementation of the Storage interface that will
|
||||
// not persist the data at all. It implements the Storage interface found:
|
||||
//
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Storage
|
||||
//
|
||||
class InMemoryStorage {
|
||||
constructor() {
|
||||
this.storage = {};
|
||||
}
|
||||
|
||||
get length() {
|
||||
return Object.keys(this.storage).length;
|
||||
}
|
||||
|
||||
key(n) {
|
||||
if (this.length <= n) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// When third party cookies are disabled, session storage is readable/
|
||||
// writable, but localStorage is not. Try to get the sessionStorage to use.
|
||||
if (type !== 'sessionStorage') {
|
||||
return getStorage('sessionStorage');
|
||||
return this.storage[Object.keys(this.storage)[n]];
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.storage[key];
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.storage[key] = value;
|
||||
|
||||
try {
|
||||
// Test sessionStorage. We could have been given access recently.
|
||||
testStorageAccess(sessionStorage);
|
||||
|
||||
// Test passed! Set the item in sessionStorage.
|
||||
sessionStorage.setItem(key, value);
|
||||
console.log(
|
||||
'Attempt to persist InMemoryStorage value to sessionStorage succeeded'
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Attempt to persist InMemoryStorage value to sessionStorage failed',
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return storage;
|
||||
removeItem(key) {
|
||||
delete this.storage[key];
|
||||
|
||||
try {
|
||||
// Test sessionStorage. We could have been given access recently.
|
||||
testStorageAccess(sessionStorage);
|
||||
|
||||
// Test passed! Remove the item from sessionStorage.
|
||||
sessionStorage.removeItem(key);
|
||||
console.log(
|
||||
'Attempt to persist InMemoryStorage delete to sessionStorage succeeded'
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Attempt to persist InMemoryStorage delete to sessionStorage failed',
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getStorage will test to see if the requested storage type is available, if it
|
||||
// is not, it will try sessionStorage, and if that is also not available, it
|
||||
// will fallback to InMemoryStorage.
|
||||
function getStorage(type) {
|
||||
try {
|
||||
// Get the desired storage from the window and test it out.
|
||||
const storage = window[type];
|
||||
testStorageAccess(storage);
|
||||
|
||||
// Storage test was successful! Return it.
|
||||
return storage;
|
||||
} catch (err) {
|
||||
// When third party cookies are disabled, session storage is readable/
|
||||
// writable, but localStorage is not. Try to get the sessionStorage to use.
|
||||
if (type !== 'sessionStorage') {
|
||||
console.warn('Could not access', type, 'trying sessionStorage', err);
|
||||
return getStorage('sessionStorage');
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'Could not access sessionStorage falling back to InMemoryStorage',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
// No acceptable storage could be found, returning the InMemoryStorage.
|
||||
return new InMemoryStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,11 +27,14 @@ const TextField = ({
|
||||
);
|
||||
|
||||
TextField.propTypes = {
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
errorMsg: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
showErrors: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default TextField;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const { pluginsPath } = require('../plugins');
|
||||
|
||||
const buildTargets = ['coral-admin'];
|
||||
|
||||
const buildEmbeds = ['stream'];
|
||||
|
||||
const specPattern = 'client/**/__tests__/**/*.spec.js?(x)';
|
||||
|
||||
module.exports = {
|
||||
rootDir: '../',
|
||||
testMatch: [
|
||||
`<rootDir>/${specPattern}`,
|
||||
`<rootDir>/plugins/**/${specPattern}`,
|
||||
],
|
||||
setupTestFrameworkScriptFile: '<rootDir>/test/client/setupJest.js',
|
||||
modulePaths: [
|
||||
'<rootDir>/plugins',
|
||||
'<rootDir>/client',
|
||||
...buildTargets.map(target => `<rootDir>/client/${target}/src`),
|
||||
...buildEmbeds.map(embed => `<rootDir>/client/coral-embed-${embed}/src`),
|
||||
],
|
||||
moduleFileExtensions: ['js', 'jsx', 'json', 'yaml', 'yml'],
|
||||
moduleDirectories: ['node_modules'],
|
||||
transform: {
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
'\\.ya?ml$': '<rootDir>/test/client/yamlTransformer.js',
|
||||
},
|
||||
testResultsProcessor: process.env.JEST_REPORTER,
|
||||
moduleNameMapper: {
|
||||
'^plugin-api\\/(.*)$': '<rootDir>/plugin-api/$1',
|
||||
'^plugins\\/(.*)$': '<rootDir>/plugins/$1',
|
||||
'^pluginsConfig$': pluginsPath,
|
||||
'\\.(scss|css|less)$': 'identity-obj-proxy',
|
||||
'\\.(gif|ttf|eot|svg)$': '<rootDir>/test/client/fileMock.js',
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user