Create app namespace for all absolute requires of app modules. Move all packages under packages yarn workspace

This commit is contained in:
SleepWalker
2019-12-07 21:02:00 +02:00
parent d8d2df0702
commit f9d3bb4e20
404 changed files with 758 additions and 742 deletions

View File

@@ -0,0 +1,202 @@
import React from 'react';
import expect from 'app/test/unexpected';
import sinon from 'sinon';
import { shallow, mount } from 'enzyme';
import { IntlProvider } from 'react-intl';
import feedback from 'app/services/api/feedback';
import { User } from 'app/components/user';
import { ContactForm } from './ContactForm';
describe('ContactForm', () => {
describe('when rendered', () => {
const user = {} as User;
let component;
beforeEach(() => {
component = shallow(<ContactForm user={user} />);
});
[
{
type: 'Input',
name: 'subject',
},
{
type: 'Input',
name: 'email',
},
{
type: 'Dropdown',
name: 'category',
},
{
type: 'TextArea',
name: 'message',
},
].forEach(el => {
it(`should have ${el.name} field`, () => {
expect(component.find(`${el.type}[name="${el.name}"]`), 'to satisfy', {
length: 1,
});
});
});
it('should contain Form', () => {
expect(component.find('Form'), 'to satisfy', { length: 1 });
});
it('should contain submit Button', () => {
expect(component.find('Button[type="submit"]'), 'to satisfy', {
length: 1,
});
});
});
describe('when rendered with user', () => {
const user = {
email: 'foo@bar.com',
} as User;
let component;
beforeEach(() => {
component = shallow(<ContactForm user={user} />);
});
it('should render email field with user email', () => {
expect(
component.find('Input[name="email"]').prop('defaultValue'),
'to equal',
user.email,
);
});
});
describe('when email was successfully sent', () => {
const user = {
email: 'foo@bar.com',
} as User;
let component;
beforeEach(() => {
component = shallow(<ContactForm user={user} />);
component.setState({ isSuccessfullySent: true });
});
it('should not contain Form', () => {
expect(component.find('Form'), 'to satisfy', { length: 0 });
});
});
xdescribe('validation', () => {
const user = {
email: 'foo@bar.com',
} as User;
let component;
let wrapper;
beforeEach(() => {
// TODO: add polyfill for from validation for jsdom
wrapper = mount(
<IntlProvider locale="en" defaultLocale="en">
<ContactForm user={user} ref={el => (component = el)} />
</IntlProvider>,
);
});
it('should require email, subject and message', () => {
// wrapper.find('[type="submit"]').simulate('click');
wrapper.find('form').simulate('submit');
expect(component.form.hasErrors(), 'to be true');
});
});
describe('when user submits form', () => {
const user = {
email: 'foo@bar.com',
} as User;
let component;
let wrapper;
const requestData = {
email: user.email,
subject: 'Test subject',
message: 'Test message',
};
beforeEach(() => {
sinon.stub(feedback, 'send');
// TODO: add polyfill for from validation for jsdom
if (!(Element.prototype as any).checkValidity) {
(Element.prototype as any).checkValidity = () => true;
}
// TODO: try to rewrite with unexpected-react
wrapper = mount(
<IntlProvider locale="en" defaultLocale="en">
<ContactForm user={user} ref={el => (component = el)} />
</IntlProvider>,
);
wrapper.find('input[name="email"]').getDOMNode().value =
requestData.email;
wrapper.find('input[name="subject"]').getDOMNode().value =
requestData.subject;
wrapper.find('textarea[name="message"]').getDOMNode().value =
requestData.message;
});
afterEach(() => {
(feedback.send as any).restore();
});
xit('should call onSubmit', () => {
sinon.stub(component, 'onSubmit');
wrapper.find('form').simulate('submit');
expect(component.onSubmit, 'was called');
});
it('should call send with required data', () => {
(feedback.send as any).returns(Promise.resolve());
component.onSubmit();
expect(feedback.send, 'to have a call satisfying', [requestData]);
});
it('should set isSuccessfullySent', () => {
(feedback.send as any).returns(Promise.resolve());
return component
.onSubmit()
.then(() =>
expect(component.state, 'to satisfy', { isSuccessfullySent: true }),
);
});
it('should handle isLoading during request', () => {
(feedback.send as any).returns(Promise.resolve());
const promise = component.onSubmit();
expect(component.state, 'to satisfy', { isLoading: true });
return promise.then(() =>
expect(component.state, 'to satisfy', { isLoading: false }),
);
});
it('should render success message with user email', () => {
(feedback.send as any).returns(Promise.resolve());
return component
.onSubmit()
.then(() => expect(wrapper.text(), 'to contain', user.email));
});
});
});

View File

@@ -0,0 +1,199 @@
import React from 'react';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { FormattedMessage as Message } from 'react-intl';
import {
Input,
TextArea,
Button,
Form,
FormModel,
Dropdown,
} from 'app/components/ui/form';
import feedback from 'app/services/api/feedback';
import icons from 'app/components/ui/icons.scss';
import popupStyles from 'app/components/ui/popup/popup.scss';
import { RootState } from 'app/reducers';
import logger from 'app/services/logger';
import { User } from 'app/components/user';
import styles from './contactForm.scss';
import messages from './contactForm.intl.json';
const CONTACT_CATEGORIES = [
// TODO: сюда позже проставить реальные id категорий с backend
<Message key="m1" {...messages.cannotAccessMyAccount} />,
<Message key="m2" {...messages.foundBugOnSite} />,
<Message key="m3" {...messages.improvementsSuggestion} />,
<Message key="m4" {...messages.integrationQuestion} />,
<Message key="m5" {...messages.other} />,
];
export class ContactForm extends React.Component<
{
onClose: () => void;
user: User;
},
{
isLoading: boolean;
isSuccessfullySent: boolean;
lastEmail: string | null;
}
> {
static defaultProps = {
onClose() {},
};
state = {
isLoading: false,
isSuccessfullySent: false,
lastEmail: null,
};
form = new FormModel();
render() {
const { isSuccessfullySent } = this.state || {};
const { onClose } = this.props;
return (
<div
data-e2e="feedbackPopup"
className={
isSuccessfullySent ? styles.successState : styles.contactForm
}
>
<div className={popupStyles.popup}>
<div className={popupStyles.header}>
<h2 className={popupStyles.headerTitle}>
<Message {...messages.title} />
</h2>
<span
className={classNames(icons.close, popupStyles.close)}
onClick={onClose}
/>
</div>
{isSuccessfullySent ? this.renderSuccess() : this.renderForm()}
</div>
</div>
);
}
renderForm() {
const { form } = this;
const { user } = this.props;
const { isLoading } = this.state;
return (
<Form form={form} onSubmit={this.onSubmit} isLoading={isLoading}>
<div className={popupStyles.body}>
<div className={styles.philosophicalThought}>
<Message {...messages.philosophicalThought} />
</div>
<div className={styles.formDisclaimer}>
<Message {...messages.disclaimer} />
<br />
</div>
<div className={styles.pairInputRow}>
<div className={styles.pairInput}>
<Input
{...form.bindField('subject')}
required
label={messages.subject}
skin="light"
/>
</div>
<div className={styles.pairInput}>
<Input
{...form.bindField('email')}
required
label={messages.email}
type="email"
skin="light"
defaultValue={user.email}
/>
</div>
</div>
<div className={styles.formMargin}>
<Dropdown
{...form.bindField('category')}
label={messages.whichQuestion}
items={CONTACT_CATEGORIES}
block
/>
</div>
<TextArea
{...form.bindField('message')}
required
label={messages.message}
skin="light"
minRows={6}
maxRows={6}
/>
</div>
<div className={styles.footer}>
<Button label={messages.send} block type="submit" />
</div>
</Form>
);
}
renderSuccess() {
const { lastEmail: email } = this.state;
const { onClose } = this.props;
return (
<div>
<div className={styles.successBody}>
<span className={styles.successIcon} />
<div className={styles.successDescription}>
<Message {...messages.youMessageReceived} />
</div>
<div className={styles.sentToEmail}>{email}</div>
</div>
<div className={styles.footer}>
<Button label={messages.close} block onClick={onClose} />
</div>
</div>
);
}
onSubmit = () => {
if (this.state.isLoading) {
return;
}
this.setState({ isLoading: true });
return feedback
.send(this.form.serialize())
.then(() =>
this.setState({
isSuccessfullySent: true,
lastEmail: this.form.value('email'),
}),
)
.catch(resp => {
if (resp.errors) {
this.form.setErrors(resp.errors);
return;
}
logger.warn('Error sending feedback', resp);
})
.finally(() => this.setState({ isLoading: false }));
};
}
export default connect((state: RootState) => ({
user: state.user,
}))(ContactForm);

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { connect } from 'react-redux';
import { create as createPopup } from 'app/components/ui/popup/actions';
import ContactForm from './ContactForm';
type Props = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
createContactPopup: () => void;
};
function ContactLink({ createContactPopup, ...props }: Props) {
return (
<a
href="#"
data-e2e-button="feedbackPopup"
onClick={event => {
event.preventDefault();
createContactPopup();
}}
{...props}
/>
);
}
export default connect(null, {
createContactPopup: () => createPopup({ Popup: ContactForm }),
})(ContactLink);

View File

@@ -0,0 +1,19 @@
{
"title": "Feedback form",
"subject": "Subject",
"email": "Email",
"message": "Message",
"send": "Send",
"philosophicalThought": "Properly formulated question — half of the answer",
"disclaimer": "Please formulate your feedback providing as much useful information, as possible to help us understand your problem and solve it",
"whichQuestion": "What are you interested in?",
"cannotAccessMyAccount": "Can not access my account",
"foundBugOnSite": "I found a bug on the site",
"improvementsSuggestion": "I have a suggestion for improving the functional",
"integrationQuestion": "Service integration question",
"other": "Other",
"youMessageReceived": "Your message was received. We will respond to you shortly. The answer will come to your Email:",
"close": "Close"
}

View File

@@ -0,0 +1,85 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@import '~app/components/ui/popup/popup.scss';
/* Form state */
.contactForm {
composes: popupWrapper from '~app/components/ui/popup/popup.scss';
@include popupBounding(500px);
}
.philosophicalThought {
font-family: $font-family-title;
font-size: 19px;
color: $green;
text-align: center;
margin-bottom: 5px;
}
.formDisclaimer {
font-size: 12px;
line-height: 14px;
text-align: center;
max-width: 400px;
margin: 0 auto 10px;
}
.pairInputRow {
display: flex;
margin-bottom: 10px;
}
.pairInput {
width: 50%;
&:first-of-type {
margin-right: $popupPadding;
}
}
.formMargin {
margin-bottom: 20px;
}
/* Success State */
.successState {
composes: popupWrapper from '~app/components/ui/popup/popup.scss';
@include popupBounding(320px);
}
.successBody {
composes: body from '~app/components/ui/popup/popup.scss';
text-align: center;
}
.successDescription {
@extend .formDisclaimer;
margin-bottom: 15px;
}
.successIcon {
composes: checkmark from '~app/components/ui/icons.scss';
font-size: 90px;
color: #aaa;
margin-bottom: 20px;
line-height: 71px;
}
.sentToEmail {
font-family: $font-family-title;
color: #444;
font-size: 18px;
}
/* Common */
.footer {
margin-top: 0;
}

View File

@@ -0,0 +1 @@
export { default as ContactLink } from './ContactLink';