#22: refactor application forms

This commit is contained in:
SleepWalker 2018-05-05 12:01:25 +03:00
parent f6b925122f
commit c454c58d5c
15 changed files with 212 additions and 212 deletions

View File

@ -1,26 +1,25 @@
// @flow // @flow
import type { ComponentType } from 'react';
import type { MessageDescriptor } from 'react-intl';
import type { OauthAppResponse } from 'services/api/oauth';
import type { ApplicationType } from 'components/dev/apps';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { FormattedMessage as Message } from 'react-intl'; import { FormattedMessage as Message } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Form, FormModel, Button } from 'components/ui/form'; import { Form, FormModel, Button } from 'components/ui/form';
import { BackButton } from 'components/profile/ProfileForm'; import { BackButton } from 'components/profile/ProfileForm';
import { COLOR_GREEN } from 'components/ui'; import { COLOR_GREEN } from 'components/ui';
import { TYPE_APPLICATION, TYPE_MINECRAFT_SERVER } from 'components/dev/apps'; import { TYPE_APPLICATION, TYPE_MINECRAFT_SERVER } from 'components/dev/apps';
import styles from 'components/profile/profileForm.scss'; import styles from 'components/profile/profileForm.scss';
import logger from 'services/logger';
import messages from './ApplicationForm.intl.json'; import messages from './ApplicationForm.intl.json';
import ApplicationTypeSwitcher from './ApplicationTypeSwitcher'; import ApplicationTypeSwitcher from './ApplicationTypeSwitcher';
import WebsiteType from './WebsiteType'; import WebsiteType from './WebsiteType';
import MinecraftServerType from './MinecraftServerType'; import MinecraftServerType from './MinecraftServerType';
import type { ComponentType } from 'react';
import type { MessageDescriptor } from 'react-intl';
import type { OauthAppResponse } from 'services/api/oauth';
const typeToForm: { const typeToForm: {
[key: string]: { [key: ApplicationType]: {
label: MessageDescriptor, label: MessageDescriptor,
component: ComponentType<any>, component: ComponentType<any>,
}, },
@ -36,9 +35,10 @@ const typeToForm: {
}; };
const typeToLabel: { const typeToLabel: {
[key: string]: MessageDescriptor, [key: ApplicationType]: MessageDescriptor,
} = Object.keys(typeToForm).reduce((result, key: string) => { } = Object.keys(typeToForm).reduce((result, key: ApplicationType) => {
result[key] = typeToForm[key].label; result[key] = typeToForm[key].label;
return result; return result;
}, {}); }, {});
@ -46,12 +46,10 @@ export default class ApplicationForm extends Component<{
app: OauthAppResponse, app: OauthAppResponse,
form: FormModel, form: FormModel,
displayTypeSwitcher?: bool, displayTypeSwitcher?: bool,
type: ?string, type: ?ApplicationType,
setType: (string) => void, setType: (ApplicationType) => void,
onSubmit: (FormModel) => Promise<*>, onSubmit: (FormModel) => Promise<void>,
}> { }> {
static displayName = 'ApplicationForm';
static defaultProps = { static defaultProps = {
setType: () => {}, setType: () => {},
}; };
@ -87,15 +85,15 @@ export default class ApplicationForm extends Component<{
</div> </div>
)} )}
{!FormComponent && ( {FormComponent ? (
<FormComponent form={form} app={app} />
) : (
<div className={styles.formRow}> <div className={styles.formRow}>
<p className={styles.description}> <p className={styles.description}>
<Message {...messages.toDisplayRegistrationFormChooseType} /> <Message {...messages.toDisplayRegistrationFormChooseType} />
</p> </p>
</div> </div>
)} )}
{FormComponent && <FormComponent form={form} app={app} />}
</div> </div>
</div> </div>
@ -123,7 +121,7 @@ export default class ApplicationForm extends Component<{
return; return;
} }
throw resp; logger.unexpected(new Error('Error submitting application form'), resp);
} }
}; };
} }

View File

@ -1,40 +1,32 @@
// @flow // @flow
import React, { Component } from 'react'; import type { ApplicationType } from 'components/dev/apps';
import type { MessageDescriptor } from 'react-intl';
import React from 'react';
import { SKIN_LIGHT } from 'components/ui'; import { SKIN_LIGHT } from 'components/ui';
import { Radio } from 'components/ui/form'; import { Radio } from 'components/ui/form';
import styles from './applicationTypeSwitcher.scss'; import styles from './applicationTypeSwitcher.scss';
import type { MessageDescriptor } from 'react-intl'; export default function ApplicationTypeSwitcher({ setType, appTypes, selectedType }: {
export default class ApplicationTypeSwitcher extends Component<{
appTypes: { appTypes: {
[key: string]: MessageDescriptor, [key: ApplicationType]: MessageDescriptor,
}, },
selectedType: ?string, selectedType: ?ApplicationType,
setType: (type: string) => void, setType: (type: ApplicationType) => void,
}> { }) {
render() { return (
const { appTypes, selectedType } = this.props; <div>
{Object.keys(appTypes).map((type: ApplicationType) => (
return ( <div className={styles.radioContainer} key={type}>
<div onChange={this.onChange}> <Radio
{Object.keys(appTypes).map((type: string) => ( onChange={() => setType(type)}
<div className={styles.radioContainer} key={type}> skin={SKIN_LIGHT}
<Radio label={appTypes[type]}
skin={SKIN_LIGHT} value={type}
label={appTypes[type]} checked={selectedType === type}
value={type} />
checked={selectedType === type} </div>
/> ))}
</div> </div>
))} );
</div>
);
}
onChange = (event: {target: {value: string}}) => {
this.props.setType(event.target.value);
}
} }

View File

@ -1,59 +1,53 @@
// @flow // @flow
import React, { Component } from 'react'; import type {OauthAppResponse} from 'services/api/oauth';
import React from 'react';
import { FormattedMessage as Message } from 'react-intl'; import { FormattedMessage as Message } from 'react-intl';
import styles from 'components/profile/profileForm.scss'; import styles from 'components/profile/profileForm.scss';
import messages from './ApplicationForm.intl.json';
import { Input, FormModel } from 'components/ui/form'; import { Input, FormModel } from 'components/ui/form';
import { SKIN_LIGHT } from 'components/ui'; import { SKIN_LIGHT } from 'components/ui';
import type {OauthAppResponse} from 'services/api/oauth'; import messages from './ApplicationForm.intl.json';
export default class MinecraftServerType extends Component<{ export default function MinecraftServerType({ form, app }: {
form: FormModel, form: FormModel,
app: OauthAppResponse, app: OauthAppResponse,
}> { }) {
render() { return (
const { form, app } = this.props; <div>
<div className={styles.formRow}>
return ( <Input {...form.bindField('name')}
<div> label={messages.serverName}
<div className={styles.formRow}> defaultValue={app.name}
<Input {...form.bindField('name')} required
label={messages.serverName} skin={SKIN_LIGHT}
defaultValue={app.name} />
required
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.ipAddressIsOptionButPreferable} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('minecraftServerIp')}
label={messages.serverIp}
defaultValue={app.minecraftServerIp}
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.youCanAlsoSpecifyServerSite} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('websiteUrl')}
label={messages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>
</div>
</div> </div>
);
} <div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.ipAddressIsOptionButPreferable} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('minecraftServerIp')}
label={messages.serverIp}
defaultValue={app.minecraftServerIp}
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.youCanAlsoSpecifyServerSite} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('websiteUrl')}
label={messages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>
</div>
</div>
);
} }

View File

@ -1,74 +1,68 @@
// @flow // @flow
import React, { Component } from 'react'; import type { OauthAppResponse } from 'services/api/oauth';
import React from 'react';
import { FormattedMessage as Message } from 'react-intl'; import { FormattedMessage as Message } from 'react-intl';
import styles from 'components/profile/profileForm.scss';
import messages from './ApplicationForm.intl.json';
import { Input, TextArea, FormModel } from 'components/ui/form'; import { Input, TextArea, FormModel } from 'components/ui/form';
import { SKIN_LIGHT } from 'components/ui'; import { SKIN_LIGHT } from 'components/ui';
import styles from 'components/profile/profileForm.scss';
import type { OauthAppResponse } from 'services/api/oauth'; import messages from './ApplicationForm.intl.json';
export default class WebsiteType extends Component<{ export default function WebsiteType({ form, app }: {
form: FormModel, form: FormModel,
app: OauthAppResponse, app: OauthAppResponse,
}> { }) {
render() { return (
const { form, app } = this.props; <div>
<div className={styles.formRow}>
return ( <Input {...form.bindField('name')}
<div> label={messages.applicationName}
<div className={styles.formRow}> defaultValue={app.name}
<Input {...form.bindField('name')} required
label={messages.applicationName} skin={SKIN_LIGHT}
defaultValue={app.name} />
required
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.appDescriptionWillBeAlsoVisibleOnOauthPage} />
</p>
</div>
<div className={styles.formRow}>
<TextArea {...form.bindField('description')}
label={messages.description}
defaultValue={app.description}
skin={SKIN_LIGHT}
minRows={3}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.websiteLinkWillBeUsedAsAdditionalId} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('websiteUrl')}
label={messages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.redirectUriLimitsAllowableBaseAddress} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('redirectUri')}
label={messages.redirectUri}
defaultValue={app.redirectUri}
required
skin={SKIN_LIGHT}
/>
</div>
</div> </div>
);
} <div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.appDescriptionWillBeAlsoVisibleOnOauthPage} />
</p>
</div>
<div className={styles.formRow}>
<TextArea {...form.bindField('description')}
label={messages.description}
defaultValue={app.description}
skin={SKIN_LIGHT}
minRows={3}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.websiteLinkWillBeUsedAsAdditionalId} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('websiteUrl')}
label={messages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.redirectUriLimitsAllowableBaseAddress} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('redirectUri')}
label={messages.redirectUri}
defaultValue={app.redirectUri}
required
skin={SKIN_LIGHT}
/>
</div>
</div>
);
} }

View File

@ -1,4 +1,4 @@
// @flow // @flow
export type ApplicationType = 'application' | 'minecraft-server';
export const TYPE_APPLICATION = 'application'; export const TYPE_APPLICATION: 'application' = 'application';
export const TYPE_MINECRAFT_SERVER = 'minecraft-server'; export const TYPE_MINECRAFT_SERVER: 'minecraft-server' = 'minecraft-server';

View File

@ -1,13 +1,11 @@
// @flow // @flow
import type { Node } from 'react';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Motion, spring } from 'react-motion'; import { Motion, spring } from 'react-motion';
import MeasureHeight from 'components/MeasureHeight'; import MeasureHeight from 'components/MeasureHeight';
import styles from './collapse.scss'; import styles from './collapse.scss';
import type { Node } from 'react';
type Props = { type Props = {
isOpened?: bool, isOpened?: bool,
children: Node, children: Node,

View File

@ -44,10 +44,14 @@ export default class Checkbox extends FormInputComponent<{
} }
getValue() { getValue() {
return this.el.checked ? 1 : 0; const { el } = this;
return el && el.checked ? 1 : 0;
} }
focus() { focus() {
this.el.focus(); const { el } = this;
el && el.focus();
} }
} }

View File

@ -15,12 +15,16 @@ export default class FormComponent<P, S = void> extends Component<P, S> {
* *
* @return {string} * @return {string}
*/ */
formatMessage(message: string | MessageDescriptor) { formatMessage(message: string | MessageDescriptor): string {
if (message && message.id && this.context && this.context.intl) { if (message && message.id) {
message = this.context.intl.formatMessage(message); if (this.context && this.context.intl) {
message = this.context.intl.formatMessage(message);
} else {
return '';
}
} }
return message; return ((message: any): string);
} }
/** /**

View File

@ -112,22 +112,26 @@ export default class Input extends FormInputComponent<{
} }
focus() { focus() {
if (!this.el) { const el = this.el;
if (!el) {
return; return;
} }
this.el.focus(); el.focus();
setTimeout(this.el.focus.bind(this.el), 10); setTimeout(el.focus.bind(el), 10);
} }
onCopy = async () => { onCopy = async () => {
if (!this.getValue()) { const value = this.getValue();
if (!value) {
return; return;
} }
try { try {
clearTimeout(copiedStateTimeout); clearTimeout(copiedStateTimeout);
await copy(this.getValue()); await copy(value);
this.setState({wasCopied: true}); this.setState({wasCopied: true});
copiedStateTimeout = setTimeout(() => this.setState({wasCopied: false}), 2000); copiedStateTimeout = setTimeout(() => this.setState({wasCopied: false}), 2000);
} catch (err) { } catch (err) {

View File

@ -44,10 +44,14 @@ export default class Radio extends FormInputComponent<{
} }
getValue() { getValue() {
return this.el.checked ? 1 : 0; const { el } = this;
return el && el.checked ? 1 : 0;
} }
focus() { focus() {
this.el.focus(); const { el } = this;
el && el.focus();
} }
} }

View File

@ -74,11 +74,17 @@ export default class TextArea extends FormInputComponent<{
} }
getValue() { getValue() {
return this.el.value; return this.el && this.el.value;
} }
focus() { focus() {
this.el.focus(); const { el } = this;
setTimeout(this.el.focus.bind(this.el), 10);
if (!el) {
return;
}
el.focus();
setTimeout(el.focus.bind(el), 10);
} }
} }

View File

@ -1,21 +1,18 @@
// @flow // @flow
import type {OauthAppResponse} from 'services/api/oauth';
import type { ApplicationType } from 'components/dev/apps';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { FormModel } from 'components/ui/form'; import { FormModel } from 'components/ui/form';
import ApplicationForm from 'components/dev/apps/applicationForm/ApplicationForm'; import ApplicationForm from 'components/dev/apps/applicationForm/ApplicationForm';
import oauth from 'services/api/oauth'; import oauth from 'services/api/oauth';
import { browserHistory } from 'services/history'; import { browserHistory } from 'services/history';
import type {OauthAppResponse} from 'services/api/oauth';
const app: OauthAppResponse = { const app: OauthAppResponse = {
clientId: '', clientId: '',
clientSecret: '', clientSecret: '',
countUsers: 0, countUsers: 0,
createdAt: 0, createdAt: 0,
type: '', type: 'application',
name: '', name: '',
description: '', description: '',
websiteUrl: '', websiteUrl: '',
@ -24,15 +21,13 @@ const app: OauthAppResponse = {
}; };
export default class CreateNewApplicationPage extends Component<{}, { export default class CreateNewApplicationPage extends Component<{}, {
type: ?string, type: ?ApplicationType,
}> { }> {
static displayName = 'CreateNewApplicationPage';
state = { state = {
type: null, type: null,
}; };
form = new FormModel(); form: FormModel = new FormModel();
render() { render() {
return ( return (
@ -50,6 +45,7 @@ export default class CreateNewApplicationPage extends Component<{}, {
onSubmit = async () => { onSubmit = async () => {
const { form } = this; const { form } = this;
const { type } = this.state; const { type } = this.state;
if (!type) { if (!type) {
throw new Error('Form was submitted without specified type'); throw new Error('Form was submitted without specified type');
} }
@ -61,7 +57,7 @@ export default class CreateNewApplicationPage extends Component<{}, {
this.goToMainPage(result.data.clientId); this.goToMainPage(result.data.clientId);
}; };
setType = (type: string) => { setType = (type: ApplicationType) => {
this.setState({ this.setState({
type, type,
}); });

View File

@ -1,17 +1,15 @@
// @flow // @flow
import type { OauthAppResponse } from 'services/api/oauth';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import logger from 'services/logger';
import { FormModel } from 'components/ui/form'; import { FormModel } from 'components/ui/form';
import ApplicationForm from 'components/dev/apps/applicationForm/ApplicationForm';
import { browserHistory } from 'services/history'; import { browserHistory } from 'services/history';
import oauth from 'services/api/oauth'; import oauth from 'services/api/oauth';
import loader from 'services/loader'; import loader from 'services/loader';
import PageNotFound from 'pages/404/PageNotFound'; import PageNotFound from 'pages/404/PageNotFound';
import { getApp, fetchApp } from 'components/dev/apps/actions';
import type { OauthAppResponse } from 'services/api/oauth'; import ApplicationForm from 'components/dev/apps/applicationForm/ApplicationForm';
type MatchType = { type MatchType = {
match: { match: {
@ -23,24 +21,23 @@ type MatchType = {
class UpdateApplicationPage extends Component<{ class UpdateApplicationPage extends Component<{
app: ?OauthAppResponse, app: ?OauthAppResponse,
fetchApp: (string) => Promise<*>, fetchApp: (string) => Promise<void>,
} & MatchType, { } & MatchType, {
isNotFound: bool, isNotFound: bool,
}> { }> {
static displayName = 'UpdateApplicationPage'; form: FormModel = new FormModel();
form = new FormModel();
state = { state = {
isNotFound: false, isNotFound: false,
}; };
componentWillMount() { componentDidMount() {
this.props.app === null && this.fetchApp(); this.props.app === null && this.fetchApp();
} }
render() { render() {
const { app } = this.props; const { app } = this.props;
if (this.state.isNotFound) { if (this.state.isNotFound) {
return ( return (
<PageNotFound /> <PageNotFound />
@ -48,7 +45,8 @@ class UpdateApplicationPage extends Component<{
} }
if (!app) { if (!app) {
return (<div/>); // we are loading
return null;
} }
return ( return (
@ -63,11 +61,13 @@ class UpdateApplicationPage extends Component<{
async fetchApp() { async fetchApp() {
const { fetchApp, match } = this.props; const { fetchApp, match } = this.props;
try { try {
loader.show(); loader.show();
await fetchApp(match.params.clientId); await fetchApp(match.params.clientId);
} catch (resp) { } catch (resp) {
const { status } = resp.originalResponse; const { status } = resp.originalResponse;
if (status === 403) { if (status === 403) {
this.goToMainPage(); this.goToMainPage();
return; return;
@ -80,7 +80,7 @@ class UpdateApplicationPage extends Component<{
return; return;
} }
throw resp; logger.unexpected('Error fetching app', resp);
} finally { } finally {
loader.hide(); loader.hide();
} }
@ -89,6 +89,7 @@ class UpdateApplicationPage extends Component<{
onSubmit = async () => { onSubmit = async () => {
const { form } = this; const { form } = this;
const { app } = this.props; const { app } = this.props;
if (!app || !app.clientId) { if (!app || !app.clientId) {
throw new Error('Form has an invalid state'); throw new Error('Form has an invalid state');
} }
@ -103,9 +104,6 @@ class UpdateApplicationPage extends Component<{
goToMainPage = (hash?: string) => browserHistory.push(`/dev/applications${hash ? `#${hash}` : ''}`); goToMainPage = (hash?: string) => browserHistory.push(`/dev/applications${hash ? `#${hash}` : ''}`);
} }
import { connect } from 'react-redux';
import { getApp, fetchApp } from 'components/dev/apps/actions';
export default connect((state, props: MatchType) => ({ export default connect((state, props: MatchType) => ({
app: getApp(state, props.match.params.clientId), app: getApp(state, props.match.params.clientId),
}), { }), {

View File

@ -1,12 +1,13 @@
// @flow // @flow
/* eslint camelcase: off */ /* eslint camelcase: off */
import type { Resp } from 'services/request'; import type { Resp } from 'services/request';
import type { ApplicationType } from 'components/dev/apps';
import request from 'services/request'; import request from 'services/request';
export type OauthAppResponse = { export type OauthAppResponse = {
clientId: string, clientId: string,
clientSecret: string, clientSecret: string,
type: string, type: ApplicationType,
name: string, name: string,
websiteUrl: string, websiteUrl: string,
createdAt: number, createdAt: number,

View File

@ -55,6 +55,13 @@ class Logger {
}); });
} }
unexpected(message: string | Error, previous: mixed) {
// TODO: check whether previous was already handled. Cover with tests
this.error(message, {
error: previous
});
}
error(message: string | Error, context: Object) { error(message: string | Error, context: Object) {
log('error', message, context); log('error', message, context);
} }