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

View File

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

View File

@ -1,59 +1,53 @@
// @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 styles from 'components/profile/profileForm.scss';
import messages from './ApplicationForm.intl.json';
import { Input, FormModel } from 'components/ui/form';
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,
app: OauthAppResponse,
}> {
render() {
const { form, app } = this.props;
return (
<div>
<div className={styles.formRow}>
<Input {...form.bindField('name')}
label={messages.serverName}
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>
}) {
return (
<div>
<div className={styles.formRow}>
<Input {...form.bindField('name')}
label={messages.serverName}
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>
);
}

View File

@ -1,74 +1,68 @@
// @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 styles from 'components/profile/profileForm.scss';
import messages from './ApplicationForm.intl.json';
import { Input, TextArea, FormModel } from 'components/ui/form';
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,
app: OauthAppResponse,
}> {
render() {
const { form, app } = this.props;
return (
<div>
<div className={styles.formRow}>
<Input {...form.bindField('name')}
label={messages.applicationName}
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>
}) {
return (
<div>
<div className={styles.formRow}>
<Input {...form.bindField('name')}
label={messages.applicationName}
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>
);
}

View File

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

View File

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

View File

@ -44,10 +44,14 @@ export default class Checkbox extends FormInputComponent<{
}
getValue() {
return this.el.checked ? 1 : 0;
const { el } = this;
return el && el.checked ? 1 : 0;
}
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}
*/
formatMessage(message: string | MessageDescriptor) {
if (message && message.id && this.context && this.context.intl) {
message = this.context.intl.formatMessage(message);
formatMessage(message: string | MessageDescriptor): string {
if (message && message.id) {
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() {
if (!this.el) {
const el = this.el;
if (!el) {
return;
}
this.el.focus();
setTimeout(this.el.focus.bind(this.el), 10);
el.focus();
setTimeout(el.focus.bind(el), 10);
}
onCopy = async () => {
if (!this.getValue()) {
const value = this.getValue();
if (!value) {
return;
}
try {
clearTimeout(copiedStateTimeout);
await copy(this.getValue());
await copy(value);
this.setState({wasCopied: true});
copiedStateTimeout = setTimeout(() => this.setState({wasCopied: false}), 2000);
} catch (err) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
// @flow
/* eslint camelcase: off */
import type { Resp } from 'services/request';
import type { ApplicationType } from 'components/dev/apps';
import request from 'services/request';
export type OauthAppResponse = {
clientId: string,
clientSecret: string,
type: string,
type: ApplicationType,
name: string,
websiteUrl: string,
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) {
log('error', message, context);
}