Merge branch 'developers-edit-settings' into 'develop'
Developers: add ability to edit raw settings JSON See merge request soapbox-pub/soapbox-fe!968
This commit is contained in:
commit
6f702a4d0e
7 changed files with 144 additions and 4 deletions
|
@ -8,6 +8,7 @@ import { createSelector } from 'reselect';
|
||||||
|
|
||||||
export const SETTING_CHANGE = 'SETTING_CHANGE';
|
export const SETTING_CHANGE = 'SETTING_CHANGE';
|
||||||
export const SETTING_SAVE = 'SETTING_SAVE';
|
export const SETTING_SAVE = 'SETTING_SAVE';
|
||||||
|
export const SETTINGS_UPDATE = 'SETTINGS_UPDATE';
|
||||||
|
|
||||||
export const FE_NAME = 'soapbox_fe';
|
export const FE_NAME = 'soapbox_fe';
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,16 @@ class Developers extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='dashcounter'>
|
||||||
|
<Link to='/developers/settings_store'>
|
||||||
|
<div className='dashcounter__icon'>
|
||||||
|
<Icon src={require('@tabler/icons/icons/code-plus.svg')} />
|
||||||
|
</div>
|
||||||
|
<div className='dashcounter__label'>
|
||||||
|
<FormattedMessage id='developers.navigation.settings_store_label' defaultMessage='Settings store' />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<div className='dashcounter'>
|
<div className='dashcounter'>
|
||||||
<Link to='/error'>
|
<Link to='/error'>
|
||||||
<div className='dashcounter__icon'>
|
<div className='dashcounter__icon'>
|
||||||
|
|
111
app/soapbox/features/developers/settings_store.js
Normal file
111
app/soapbox/features/developers/settings_store.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||||
|
import Column from 'soapbox/features/ui/components/column';
|
||||||
|
import { SimpleForm, SimpleTextarea } from 'soapbox/features/forms';
|
||||||
|
import { showAlertForError } from 'soapbox/actions/alerts';
|
||||||
|
import { patchMe } from 'soapbox/actions/me';
|
||||||
|
import { FE_NAME, SETTINGS_UPDATE } from 'soapbox/actions/settings';
|
||||||
|
|
||||||
|
const isJSONValid = text => {
|
||||||
|
try {
|
||||||
|
JSON.parse(text);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.settings_store', defaultMessage: 'Settings store' },
|
||||||
|
hint: { id: 'developers.settings_store.hint', defaultMessage: 'It is possible to directly edit your user settings here. BE CAREFUL! Editing this section can break your account, and you will only be able to recover through the API.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
settingsStore: state.get('settings'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class SettingsStore extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
settingsStore: ImmutablePropTypes.map.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
rawJSON: JSON.stringify(this.props.settingsStore, null, 2),
|
||||||
|
jsonValid: true,
|
||||||
|
isLoading: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { settingsStore } = this.props;
|
||||||
|
|
||||||
|
if (settingsStore !== prevProps.settingsStore) {
|
||||||
|
this.setState({
|
||||||
|
rawJSON: JSON.stringify(settingsStore, null, 2),
|
||||||
|
jsonValid: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEditJSON = ({ target }) => {
|
||||||
|
const rawJSON = target.value;
|
||||||
|
this.setState({ rawJSON, jsonValid: isJSONValid(rawJSON) });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = e => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { rawJSON } = this.state;
|
||||||
|
|
||||||
|
const settings = JSON.parse(rawJSON);
|
||||||
|
|
||||||
|
this.setState({ isLoading: true });
|
||||||
|
dispatch(patchMe({
|
||||||
|
pleroma_settings_store: {
|
||||||
|
[FE_NAME]: settings,
|
||||||
|
},
|
||||||
|
})).then(response => {
|
||||||
|
dispatch({ type: SETTINGS_UPDATE, settings });
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlertForError(error));
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
const { rawJSON, jsonValid, isLoading } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column heading={intl.formatMessage(messages.heading)}>
|
||||||
|
<SimpleForm onSubmit={this.handleSubmit} disabled={!jsonValid || isLoading}>
|
||||||
|
<div className={jsonValid ? 'code-editor' : 'code-editor code-editor--invalid'}>
|
||||||
|
<SimpleTextarea
|
||||||
|
hint={intl.formatMessage(messages.hint)}
|
||||||
|
value={rawJSON}
|
||||||
|
onChange={this.handleEditJSON}
|
||||||
|
disabled={isLoading}
|
||||||
|
rows={12}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='actions'>
|
||||||
|
<button name='button' type='submit' className='btn button button-primary' disabled={!jsonValid || isLoading}>
|
||||||
|
<FormattedMessage id='soapbox_config.save' defaultMessage='Save' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SimpleForm>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -116,6 +116,7 @@ import {
|
||||||
IntentionalError,
|
IntentionalError,
|
||||||
Developers,
|
Developers,
|
||||||
CreateApp,
|
CreateApp,
|
||||||
|
SettingsStore,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
|
|
||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
|
@ -321,8 +322,9 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
<WrappedRoute path='/admin/users' staffOnly page={AdminPage} component={UserIndex} content={children} exact />
|
<WrappedRoute path='/admin/users' staffOnly page={AdminPage} component={UserIndex} content={children} exact />
|
||||||
<WrappedRoute path='/info' page={EmptyPage} component={ServerInfo} content={children} />
|
<WrappedRoute path='/info' page={EmptyPage} component={ServerInfo} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/developers/apps/create' page={DefaultPage} component={CreateApp} content={children} />
|
<WrappedRoute path='/developers/apps/create' developerOnly page={DefaultPage} component={CreateApp} content={children} />
|
||||||
<WrappedRoute path='/developers' page={DefaultPage} component={Developers} content={children} />
|
<WrappedRoute path='/developers/settings_store' developerOnly page={DefaultPage} component={SettingsStore} content={children} />
|
||||||
|
<WrappedRoute path='/developers' developerOnly page={DefaultPage} component={Developers} content={children} />
|
||||||
<WrappedRoute path='/error' page={EmptyPage} component={IntentionalError} content={children} />
|
<WrappedRoute path='/error' page={EmptyPage} component={IntentionalError} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/donate/crypto' publicRoute page={DefaultPage} component={CryptoDonate} content={children} />
|
<WrappedRoute path='/donate/crypto' publicRoute page={DefaultPage} component={CryptoDonate} content={children} />
|
||||||
|
|
|
@ -445,3 +445,7 @@ export function Developers() {
|
||||||
export function CreateApp() {
|
export function CreateApp() {
|
||||||
return import(/* webpackChunkName: "features/developers" */'../../developers/apps/create');
|
return import(/* webpackChunkName: "features/developers" */'../../developers/apps/create');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SettingsStore() {
|
||||||
|
return import(/* webpackChunkName: "features/developers" */'../../developers/settings_store');
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import ColumnLoading from '../components/column_loading';
|
||||||
import ColumnForbidden from '../components/column_forbidden';
|
import ColumnForbidden from '../components/column_forbidden';
|
||||||
import BundleColumnError from '../components/bundle_column_error';
|
import BundleColumnError from '../components/bundle_column_error';
|
||||||
import BundleContainer from '../containers/bundle_container';
|
import BundleContainer from '../containers/bundle_container';
|
||||||
|
import { getSettings } from 'soapbox/actions/settings';
|
||||||
import { isStaff, isAdmin } from 'soapbox/utils/accounts';
|
import { isStaff, isAdmin } from 'soapbox/utils/accounts';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
|
@ -15,6 +16,7 @@ const mapStateToProps = state => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account: state.getIn(['accounts', me]),
|
account: state.getIn(['accounts', me]),
|
||||||
|
settings: getSettings(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,9 +29,11 @@ class WrappedRoute extends React.Component {
|
||||||
componentParams: PropTypes.object,
|
componentParams: PropTypes.object,
|
||||||
layout: PropTypes.object,
|
layout: PropTypes.object,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
publicRoute: PropTypes.bool,
|
publicRoute: PropTypes.bool,
|
||||||
staffOnly: PropTypes.bool,
|
staffOnly: PropTypes.bool,
|
||||||
adminOnly: PropTypes.bool,
|
adminOnly: PropTypes.bool,
|
||||||
|
developerOnly: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -100,10 +104,11 @@ class WrappedRoute extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { component: Component, content, account, publicRoute, staffOnly, adminOnly, ...rest } = this.props;
|
const { component: Component, content, account, settings, publicRoute, developerOnly, staffOnly, adminOnly, ...rest } = this.props;
|
||||||
|
|
||||||
const authorized = [
|
const authorized = [
|
||||||
account || publicRoute,
|
account || publicRoute,
|
||||||
|
developerOnly ? settings.get('isDeveloper') : true,
|
||||||
staffOnly ? account && isStaff(account) : true,
|
staffOnly ? account && isStaff(account) : true,
|
||||||
adminOnly ? account && isAdmin(account) : true,
|
adminOnly ? account && isAdmin(account) : true,
|
||||||
].every(c => c);
|
].every(c => c);
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
import { SETTING_CHANGE, SETTING_SAVE, FE_NAME } from '../actions/settings';
|
import {
|
||||||
|
SETTING_CHANGE,
|
||||||
|
SETTING_SAVE,
|
||||||
|
SETTINGS_UPDATE,
|
||||||
|
FE_NAME,
|
||||||
|
} from '../actions/settings';
|
||||||
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
|
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
|
||||||
import { SEARCH_FILTER_SET } from '../actions/search';
|
import { SEARCH_FILTER_SET } from '../actions/search';
|
||||||
import { EMOJI_USE } from '../actions/emojis';
|
import { EMOJI_USE } from '../actions/emojis';
|
||||||
|
@ -35,6 +40,8 @@ export default function settings(state = initialState, action) {
|
||||||
return updateFrequentEmojis(state, action.emoji);
|
return updateFrequentEmojis(state, action.emoji);
|
||||||
case SETTING_SAVE:
|
case SETTING_SAVE:
|
||||||
return state.set('saved', true);
|
return state.set('saved', true);
|
||||||
|
case SETTINGS_UPDATE:
|
||||||
|
return fromJS(action.settings);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue