Merge pull request #665 from sharetribe/policypanel

EditListingPolicyPanel aka Sauna rules
This commit is contained in:
Vesa Luusua 2018-01-23 18:14:41 +02:00 committed by GitHub
commit 5bd7ae1249
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 401 additions and 53 deletions

View file

@ -0,0 +1,22 @@
@import '../../marketplace.css';
.root {
flex-grow: 1;
width: 100%;
height: auto;
display: flex;
flex-direction: column;
padding: 11px 24px 0 24px;
}
.form {
flex-grow: 1;
}
.title {
margin-bottom: 19px;
@media (--viewportLarge) {
margin-bottom: 44px;
}
}

View file

@ -0,0 +1,86 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { ensureListing } from '../../util/data';
import { createSlug } from '../../util/urlHelpers';
import { NamedLink } from '../../components';
import { EditListingPoliciesForm } from '../../containers';
import css from './EditListingPoliciesPanel.css';
const EditListingPoliciesPanel = props => {
const {
className,
rootClassName,
listing,
onSubmit,
onChange,
submitButtonText,
panelUpdated,
updateInProgress,
errors,
} = props;
const classes = classNames(rootClassName || css.root, className);
const currentListing = ensureListing(listing);
const { title, publicData } = currentListing.attributes;
const listingTitle = title || '';
const listingLink = currentListing.id ? (
<NamedLink name="ListingPage" params={{ id: currentListing.id.uuid, slug: createSlug(title) }}>
{listingTitle}
</NamedLink>
) : (
''
);
const panelTitle = currentListing.id ? (
<FormattedMessage id="EditListingPoliciesPanel.title" values={{ listingTitle: listingLink }} />
) : (
<FormattedMessage id="EditListingPoliciesPanel.createListingTitle" />
);
const saunaRules = publicData && publicData.saunaRules ? publicData.saunaRules : '';
const initialSearchFormValues = { saunaRules };
return (
<div className={classes}>
<h1 className={css.title}>{panelTitle}</h1>
<EditListingPoliciesForm
className={css.form}
initialValues={initialSearchFormValues}
onSubmit={onSubmit}
onChange={onChange}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
updateInProgress={updateInProgress}
/>
</div>
);
};
const { func, object, string, bool } = PropTypes;
EditListingPoliciesPanel.defaultProps = {
className: null,
rootClassName: null,
listing: null,
};
EditListingPoliciesPanel.propTypes = {
className: string,
rootClassName: string,
// We cannot use propTypes.listing since the listing might be a draft.
listing: object,
onSubmit: func.isRequired,
onChange: func.isRequired,
submitButtonText: string.isRequired,
panelUpdated: bool.isRequired,
updateInProgress: bool.isRequired,
errors: object.isRequired,
};
export default EditListingPoliciesPanel;

View file

@ -42,6 +42,7 @@
.tab {
margin-left: 16px;
white-space: nowrap;
&:first-child {
margin-left: 0;

View file

@ -12,6 +12,7 @@ import {
EditListingDescriptionPanel,
EditListingLocationPanel,
EditListingPhotosPanel,
EditListingPoliciesPanel,
EditListingPricingPanel,
NamedRedirect,
Tabs,
@ -20,10 +21,11 @@ import {
import css from './EditListingWizard.css';
const DESCRIPTION = 'description';
const POLICY = 'policy';
const LOCATION = 'location';
const PRICING = 'pricing';
const PHOTOS = 'photos';
const STEPS = [DESCRIPTION, LOCATION, PRICING, PHOTOS];
const STEPS = [DESCRIPTION, POLICY, LOCATION, PRICING, PHOTOS];
// Tabs are horizontal in small screens
const MAX_HORIZONTAL_NAV_SCREEN_WIDTH = 1023;
@ -32,6 +34,8 @@ const submitText = (intl, isNew, step) => {
let key = null;
if (step === DESCRIPTION) {
key = isNew ? 'EditListingWizard.saveNewDescription' : 'EditListingWizard.saveEditDescription';
} else if (step === POLICY) {
key = isNew ? 'EditListingWizard.saveNewPolicies' : 'EditListingWizard.saveEditPolicies';
} else if (step === LOCATION) {
key = isNew ? 'EditListingWizard.saveNewLocation' : 'EditListingWizard.saveEditLocation';
} else if (step === PRICING) {
@ -42,6 +46,22 @@ const submitText = (intl, isNew, step) => {
return intl.formatMessage({ id: key });
};
const tabLabel = (intl, step) => {
let key = null;
if (step === DESCRIPTION) {
key = 'EditListingWizard.tabLabelDescription';
} else if (step === POLICY) {
key = 'EditListingWizard.tabLabelPolicy';
} else if (step === LOCATION) {
key = 'EditListingWizard.tabLabelLocation';
} else if (step === PRICING) {
key = 'EditListingWizard.tabLabelPricing';
} else if (step === PHOTOS) {
key = 'EditListingWizard.tabLabelPhotos';
}
return intl.formatMessage({ id: key });
};
/**
* Check which wizard steps are active and which are not yet available. Step is active is previous
* step is completed.
@ -53,6 +73,7 @@ const submitText = (intl, isNew, step) => {
const stepsActive = listing => {
const { description, geolocation, price, title, publicData } = listing.attributes;
const descriptionStep = !!(title && description);
const policyStep = !!(publicData && typeof publicData.saunaRules !== 'undefined');
const locationStep = !!(
geolocation &&
publicData &&
@ -65,7 +86,8 @@ const stepsActive = listing => {
return {
[DESCRIPTION]: true,
[LOCATION]: descriptionStep,
[POLICY]: descriptionStep,
[LOCATION]: policyStep,
[PRICING]: locationStep,
[PHOTOS]: pricingStep,
};
@ -160,21 +182,27 @@ class EditListingWizard extends Component {
this.hasScrolledToTab = true;
}
const panelProps = tab => {
return {
className: css.panel,
disabled: !stepsStatus[tab],
errors,
listing,
onChange,
panelUpdated: updatedTab === tab,
selected: selectedTab === tab,
submitButtonText: submitText(intl, isNew, tab),
tabId: `${id}_${tab}`,
tabLabel: tabLabel(intl, tab),
tabLinkProps: tabLink(tab),
updateInProgress,
};
};
return (
<Tabs rootClassName={classes} navRootClassName={css.nav} tabRootClassName={css.tab}>
<EditListingDescriptionPanel
className={css.panel}
tabId={`${id}_a${DESCRIPTION}`}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelDescription' })}
tabLinkProps={tabLink(DESCRIPTION)}
selected={selectedTab === DESCRIPTION}
panelUpdated={updatedTab === DESCRIPTION}
updateInProgress={updateInProgress}
disabled={!stepsStatus[DESCRIPTION]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, DESCRIPTION)}
onChange={onChange}
{...panelProps(DESCRIPTION)}
onSubmit={values => {
const { title, description, category } = values;
const updateValues = {
@ -186,7 +214,7 @@ class EditListingWizard extends Component {
if (isNew) {
onUpsertListingDraft(updateValues);
const pathParams = tabParams(LOCATION);
const pathParams = tabParams(POLICY);
// Redirect to location tab
history.push(
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
@ -196,19 +224,30 @@ class EditListingWizard extends Component {
}
}}
/>
<EditListingPoliciesPanel
{...panelProps(POLICY)}
onSubmit={values => {
const { saunaRules = '' } = values;
const updateValues = {
publicData: {
saunaRules,
},
};
if (isNew) {
onUpdateListingDraft(updateValues);
const pathParams = tabParams(LOCATION);
// Redirect to pricing tab
history.push(
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
update(POLICY, updateValues);
}
}}
/>
<EditListingLocationPanel
className={css.panel}
tabId={`${id}_${LOCATION}`}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelLocation' })}
tabLinkProps={tabLink(LOCATION)}
selected={selectedTab === LOCATION}
panelUpdated={updatedTab === LOCATION}
updateInProgress={updateInProgress}
disabled={!stepsStatus[LOCATION]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, LOCATION)}
onChange={onChange}
{...panelProps(LOCATION)}
onSubmit={values => {
const { building = '', location } = values;
const { selectedPlace: { address, origin } } = location;
@ -220,7 +259,6 @@ class EditListingWizard extends Component {
};
if (isNew) {
// TODO When API supports building number, etc. change this to use those fields instead.
onUpdateListingDraft(updateValues);
const pathParams = tabParams(PRICING);
// Redirect to pricing tab
@ -233,18 +271,7 @@ class EditListingWizard extends Component {
}}
/>
<EditListingPricingPanel
className={css.panel}
tabId={`${id}_${PRICING}`}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelPricing' })}
tabLinkProps={tabLink(PRICING)}
selected={selectedTab === PRICING}
panelUpdated={updatedTab === PRICING}
updateInProgress={updateInProgress}
disabled={!stepsStatus[PRICING]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, PRICING)}
onChange={onChange}
{...panelProps(PRICING)}
onSubmit={values => {
if (isNew) {
onUpdateListingDraft(values);
@ -259,25 +286,14 @@ class EditListingWizard extends Component {
}}
/>
<EditListingPhotosPanel
className={css.panel}
tabId={`${id}_${PHOTOS}`}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelPhotos' })}
tabLinkProps={tabLink(PHOTOS)}
selected={selectedTab === PHOTOS}
disabled={!stepsStatus[PHOTOS]}
panelUpdated={updatedTab === PHOTOS}
{...panelProps(PHOTOS)}
newListingCreated={newListingCreated}
updateInProgress={updateInProgress}
errors={errors}
fetchInProgress={fetchInProgress}
listing={listing}
images={images}
onImageUpload={onImageUpload}
onRemoveImage={onRemoveImage}
onPayoutDetailsFormChange={onPayoutDetailsFormChange}
onPayoutDetailsSubmit={onPayoutDetailsSubmit}
submitButtonText={submitText(intl, isNew, PHOTOS)}
onChange={onChange}
onSubmit={values => {
const { country, images: updatedImages } = values;
const updateValues = { ...listing.attributes, country, images: updatedImages };

View file

@ -20,6 +20,9 @@ export {
default as EditListingLocationPanel,
} from './EditListingLocationPanel/EditListingLocationPanel';
export { default as EditListingPhotosPanel } from './EditListingPhotosPanel/EditListingPhotosPanel';
export {
default as EditListingPoliciesPanel,
} from './EditListingPoliciesPanel/EditListingPoliciesPanel';
export {
default as EditListingPricingPanel,
} from './EditListingPricingPanel/EditListingPricingPanel';

View file

@ -0,0 +1,53 @@
@import '../../marketplace.css';
:root {
--formMargins: {
margin-bottom: 24px;
@media (--viewportMedium) {
margin-bottom: 32px;
}
}
}
.root {
/* Dimensions */
width: 100%;
height: auto;
/* Layout */
display: flex;
flex: 1;
flex-direction: column;
padding-top: 1px;
@media (--viewportMedium) {
padding-top: 2px;
}
}
.title {
@apply --formMargins;
}
.policy {
@apply --formMargins;
flex-shrink: 0;
}
.error {
color: var(--failColor);
}
.submitButton {
margin-top: auto;
margin-bottom: 24px;
flex-shrink: 0;
@media (--viewportLarge) {
display: inline-block;
width: 241px;
margin-top: 100px;
}
}

View file

@ -0,0 +1,15 @@
/* eslint-disable no-console */
import EditListingPoliciesForm from './EditListingPoliciesForm';
export const Empty = {
component: EditListingPoliciesForm,
props: {
onSubmit: values => {
console.log('Submit EditListingPoliciesForm with (unformatted) values:', values);
},
saveActionMsg: 'Save rules',
updated: false,
updateInProgress: false,
},
group: 'forms',
};

View file

@ -0,0 +1,89 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
import { Form, Button, TextInputField } from '../../components';
import css from './EditListingPoliciesForm.css';
export const EditListingPoliciesFormComponent = props => {
const {
className,
disabled,
handleSubmit,
intl,
form,
invalid,
saveActionMsg,
submitting,
updated,
updateError,
updateInProgress,
} = props;
const rulesLabelMessage = intl.formatMessage({ id: 'EditListingPoliciesForm.rulesLabel' });
const saunaRulesPlaceholderMessage = intl.formatMessage({
id: 'EditListingPoliciesForm.rulesPlaceholder',
});
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPoliciesForm.updateFailed" />
</p>
) : null;
const classes = classNames(css.root, className);
const submitReady = updated;
const submitInProgress = submitting || updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<TextInputField
className={css.policy}
type="textarea"
name="saunaRules"
id={`${form}.saunaRules`}
label={rulesLabelMessage}
placeholder={saunaRulesPlaceholderMessage}
/>
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
};
EditListingPoliciesFormComponent.defaultProps = {
selectedPlace: null,
updateError: null,
};
const { func, string, bool } = PropTypes;
EditListingPoliciesFormComponent.propTypes = {
...formPropTypes,
intl: intlShape.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
selectedPlace: propTypes.place,
updated: bool.isRequired,
updateError: propTypes.error,
updateInProgress: bool.isRequired,
};
const formName = 'EditListingPoliciesForm';
export default compose(reduxForm({ form: formName }), injectIntl)(EditListingPoliciesFormComponent);

View file

@ -0,0 +1,24 @@
// NOTE: renderdeep doesn't work due to Google Maps API integration
import React from 'react';
import { renderShallow } from '../../util/test-helpers';
import { fakeIntl, fakeFormProps } from '../../util/test-data';
import { EditListingPoliciesFormComponent } from './EditListingPoliciesForm';
const noop = () => null;
describe('EditListingPoliciesForm', () => {
it('matches snapshot', () => {
const tree = renderShallow(
<EditListingPoliciesFormComponent
{...fakeFormProps}
intl={fakeIntl}
dispatch={noop}
onSubmit={v => v}
saveActionMsg="Save rules"
updated={false}
updateInProgress={false}
/>
);
expect(tree).toMatchSnapshot();
});
});

View file

@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditListingPoliciesForm matches snapshot 1`] = `
<Form
className=""
onSubmit={[Function]}
>
<TextInputField
id="fakeTestForm.saunaRules"
label="EditListingPoliciesForm.rulesLabel"
name="saunaRules"
placeholder="EditListingPoliciesForm.rulesPlaceholder"
type="textarea"
/>
<Button
className={null}
disabled={false}
inProgress={false}
ready={false}
rootClassName={null}
type="submit"
>
Save rules
</Button>
</Form>
`;

View file

@ -12,6 +12,9 @@ export {
} from './EditListingLocationForm/EditListingLocationForm';
export { default as EditListingPage } from './EditListingPage/EditListingPage';
export { default as EditListingPhotosForm } from './EditListingPhotosForm/EditListingPhotosForm';
export {
default as EditListingPoliciesForm,
} from './EditListingPoliciesForm/EditListingPoliciesForm';
export { default as EditListingPricingForm } from './EditListingPricingForm/EditListingPricingForm';
export { default as EmailVerificationForm } from './EmailVerificationForm/EmailVerificationForm';
export { default as EmailVerificationPage } from './EmailVerificationPage/EmailVerificationPage';

View file

@ -58,6 +58,7 @@ import * as Colors from './containers/StyleguidePage/Colors.example';
import * as EditListingDescriptionForm from './containers/EditListingDescriptionForm/EditListingDescriptionForm.example';
import * as EditListingLocationForm from './containers/EditListingLocationForm/EditListingLocationForm.example';
import * as EditListingPhotosForm from './containers/EditListingPhotosForm/EditListingPhotosForm.example';
import * as EditListingPoliciesForm from './containers/EditListingPoliciesForm/EditListingPoliciesForm.example';
import * as EditListingPricingForm from './containers/EditListingPricingForm/EditListingPricingForm.example';
import * as EmailVerificationForm from './containers/EmailVerificationForm/EmailVerificationForm.example';
import * as EnquiryForm from './containers/EnquiryForm/EnquiryForm.example';
@ -85,6 +86,7 @@ export {
EditListingDescriptionForm,
EditListingLocationForm,
EditListingPhotosForm,
EditListingPoliciesForm,
EditListingPricingForm,
EmailVerificationForm,
EnquiryForm,

View file

@ -147,6 +147,11 @@
"EditListingPhotosPanel.payoutModalTitleOneMoreThing": "One more thing:",
"EditListingPhotosPanel.payoutModalTitlePayoutPreferences": "Payout preferences",
"EditListingPhotosPanel.title": "Edit the photos of {listingTitle}",
"EditListingPoliciesForm.rulesLabel": "Sauna rules",
"EditListingPoliciesForm.rulesPlaceholder": "Describe the no-nos",
"EditListingPoliciesForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingPoliciesPanel.createListingTitle": "Time to set some ground rules for the bathers.",
"EditListingPoliciesPanel.title": "Edit the rules of {listingTitle}",
"EditListingPricingForm.priceInputPlaceholder": "Choose your price…",
"EditListingPricingForm.pricePerUnit": "Price per night in euros",
"EditListingPricingForm.priceRequired": "You need to add a valid price.",
@ -157,14 +162,17 @@
"EditListingWizard.saveEditDescription": "Save description",
"EditListingWizard.saveEditLocation": "Save location",
"EditListingWizard.saveEditPhotos": "Save photos",
"EditListingWizard.saveEditPolicies": "Save rules",
"EditListingWizard.saveEditPricing": "Save pricing",
"EditListingWizard.saveNewDescription": "Next: Location",
"EditListingWizard.saveNewDescription": "Next: Policies",
"EditListingWizard.saveNewLocation": "Next: Pricing",
"EditListingWizard.saveNewPhotos": "Publish listing",
"EditListingWizard.saveNewPolicies": "Next: location",
"EditListingWizard.saveNewPricing": "Next: Photos",
"EditListingWizard.tabLabelDescription": "Description",
"EditListingWizard.tabLabelLocation": "Location",
"EditListingWizard.tabLabelPhotos": "Photos",
"EditListingWizard.tabLabelPolicy": "Sauna rules",
"EditListingWizard.tabLabelPricing": "Pricing",
"EmailVerificationForm.finishAccountSetup": "We will need to send you email notifications. To allow this, please verify your email address {email}.",
"EmailVerificationForm.successButtonText": "Continue exploring",