Created SimpleCard component to reduce duplicated code when rendering cards

This commit is contained in:
Alejandro Celaya 2020-12-08 19:10:29 +01:00
parent e60d241fcf
commit 2017ee7456
4 changed files with 126 additions and 93 deletions

View file

@ -1,6 +1,7 @@
import { Card, CardBody, CardHeader, FormGroup, Input } from 'reactstrap'; import { FormGroup, Input } from 'reactstrap';
import classNames from 'classnames'; import classNames from 'classnames';
import ToggleSwitch from '../utils/ToggleSwitch'; import ToggleSwitch from '../utils/ToggleSwitch';
import { SimpleCard } from '../utils/SimpleCard';
import { Settings } from './reducers/settings'; import { Settings } from './reducers/settings';
interface RealTimeUpdatesProps { interface RealTimeUpdatesProps {
@ -14,39 +15,36 @@ const intervalValue = (interval?: number) => !interval ? '' : `${interval}`;
const RealTimeUpdates = ( const RealTimeUpdates = (
{ settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps, { settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
) => ( ) => (
<Card> <SimpleCard title="Real-time updates">
<CardHeader>Real-time updates</CardHeader> <FormGroup>
<CardBody> <ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
<FormGroup> Enable or disable real-time updates, when using Shlink v2.2.0 or newer.
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}> </ToggleSwitch>
Enable or disable real-time updates, when using Shlink v2.2.0 or newer. </FormGroup>
</ToggleSwitch> <FormGroup className="mb-0">
</FormGroup> <label className={classNames({ 'text-muted': !realTimeUpdates.enabled })}>
<FormGroup className="mb-0"> Real-time updates frequency (in minutes):
<label className={classNames({ 'text-muted': !realTimeUpdates.enabled })}> </label>
Real-time updates frequency (in minutes): <Input
</label> type="number"
<Input min={0}
type="number" placeholder="Immediate"
min={0} disabled={!realTimeUpdates.enabled}
placeholder="Immediate" value={intervalValue(realTimeUpdates.interval)}
disabled={!realTimeUpdates.enabled} onChange={(e) => setRealTimeUpdatesInterval(Number(e.target.value))}
value={intervalValue(realTimeUpdates.interval)} />
onChange={(e) => setRealTimeUpdatesInterval(Number(e.target.value))} {realTimeUpdates.enabled && (
/> <small className="form-text text-muted">
{realTimeUpdates.enabled && ( {realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && (
<small className="form-text text-muted"> <span>
{realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && ( Updates will be reflected in the UI every <b>{realTimeUpdates.interval}</b> minute{realTimeUpdates.interval > 1 && 's'}.
<span> </span>
Updates will be reflected in the UI every <b>{realTimeUpdates.interval}</b> minute{realTimeUpdates.interval > 1 && 's'}. )}
</span> {!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'}
)} </small>
{!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'} )}
</small> </FormGroup>
)} </SimpleCard>
</FormGroup>
</CardBody>
</Card>
); );
export default RealTimeUpdates; export default RealTimeUpdates;

View file

@ -1,6 +1,6 @@
import { isEmpty, pipe, replace, trim } from 'ramda'; import { isEmpty, pipe, replace, trim } from 'ramda';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { Button, Card, CardBody, CardHeader, FormGroup, Input } from 'reactstrap'; import { Button, FormGroup, Input } from 'reactstrap';
import { InputType } from 'reactstrap/lib/Input'; import { InputType } from 'reactstrap/lib/Input';
import * as m from 'moment'; import * as m from 'moment';
import DateInput, { DateInputProps } from '../utils/DateInput'; import DateInput, { DateInputProps } from '../utils/DateInput';
@ -11,6 +11,7 @@ import { isReachableServer, SelectedServer } from '../servers/data';
import { formatIsoDate } from '../utils/helpers/date'; import { formatIsoDate } from '../utils/helpers/date';
import { TagsSelectorProps } from '../tags/helpers/TagsSelector'; import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { DomainSelectorProps } from '../domains/DomainSelector'; import { DomainSelectorProps } from '../domains/DomainSelector';
import { SimpleCard } from '../utils/SimpleCard';
import { ShortUrlData } from './data'; import { ShortUrlData } from './data';
import { ShortUrlCreation } from './reducers/shortUrlCreation'; import { ShortUrlCreation } from './reducers/shortUrlCreation';
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon'; import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
@ -121,55 +122,48 @@ const CreateShortUrl = (
<> <>
<div className="row"> <div className="row">
<div className="col-sm-6 mb-3"> <div className="col-sm-6 mb-3">
<Card> <SimpleCard title="Customize the short URL">
<CardHeader>Customize the short URL</CardHeader> <p>
<CardBody> Use a custom slug for your marketing campaigns, change the domain or set a specific length for
<p> the auto-generated short code.
Use a custom slug for your marketing campaigns, change the domain or set a specific length for </p>
the auto-generated short code. {renderOptionalInput('customSlug', 'Custom slug', 'text', {
</p> disabled: hasValue(shortUrlCreation.shortCodeLength),
{renderOptionalInput('customSlug', 'Custom slug', 'text', { })}
disabled: hasValue(shortUrlCreation.shortCodeLength), {renderOptionalInput('shortCodeLength', 'Short code length', 'number', {
})} min: 4,
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', { disabled: disableShortCodeLength || hasValue(shortUrlCreation.customSlug),
min: 4, ...disableShortCodeLength && {
disabled: disableShortCodeLength || hasValue(shortUrlCreation.customSlug), title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length',
...disableShortCodeLength && { },
title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length', })}
}, {!showDomainSelector && renderOptionalInput('domain', 'Domain', 'text', {
})} disabled: disableDomain,
{!showDomainSelector && renderOptionalInput('domain', 'Domain', 'text', { ...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' },
disabled: disableDomain, })}
...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' }, {showDomainSelector && (
})} <FormGroup>
{showDomainSelector && ( <DomainSelector
<FormGroup> value={shortUrlCreation.domain}
<DomainSelector onChange={(domain?: string) => setShortUrlCreation({ ...shortUrlCreation, domain })}
value={shortUrlCreation.domain} />
onChange={(domain?: string) => setShortUrlCreation({ ...shortUrlCreation, domain })} </FormGroup>
/> )}
</FormGroup> </SimpleCard>
)}
</CardBody>
</Card>
</div> </div>
<div className="col-sm-6 mb-3"> <div className="col-sm-6 mb-3">
<Card> <SimpleCard title="Limit access to the short URL">
<CardHeader>Limit access to the short URL</CardHeader> <p>Determine when and how many times your short URL can be accessed.</p>
<CardBody> {renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
<p>Determine when and how many times your short URL can be accessed.</p> {renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlCreation.validUntil as m.Moment | undefined })}
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })} {renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlCreation.validSince as m.Moment | undefined })}
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlCreation.validUntil as m.Moment | undefined })} </SimpleCard>
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlCreation.validSince as m.Moment | undefined })}
</CardBody>
</Card>
</div> </div>
</div> </div>
<Card className="mb-3"> <ForServerVersion minVersion="1.16.0">
<CardHeader>Extra validations</CardHeader> <SimpleCard title="Extra validations" className="mb-3">
<CardBody>
<p> <p>
Make sure the long URL is valid, or ensure an existing short URL is returned if it matches all Make sure the long URL is valid, or ensure an existing short URL is returned if it matches all
provided data. provided data.
@ -185,21 +179,19 @@ const CreateShortUrl = (
</Checkbox> </Checkbox>
</p> </p>
</ForServerVersion> </ForServerVersion>
<ForServerVersion minVersion="1.16.0"> <p>
<p> <Checkbox
<Checkbox inline
inline className="mr-2"
className="mr-2" checked={shortUrlCreation.findIfExists}
checked={shortUrlCreation.findIfExists} onChange={(findIfExists) => setShortUrlCreation({ ...shortUrlCreation, findIfExists })}
onChange={(findIfExists) => setShortUrlCreation({ ...shortUrlCreation, findIfExists })} >
> Use existing URL if found
Use existing URL if found </Checkbox>
</Checkbox> <UseExistingIfFoundInfoIcon />
<UseExistingIfFoundInfoIcon /> </p>
</p> </SimpleCard>
</ForServerVersion> </ForServerVersion>
</CardBody>
</Card>
</> </>
)} )}

13
src/utils/SimpleCard.tsx Normal file
View file

@ -0,0 +1,13 @@
import { CardProps } from 'reactstrap/lib/Card';
import { Card, CardBody, CardHeader } from 'reactstrap';
interface SimpleCardProps extends CardProps {
title?: string;
}
export const SimpleCard = ({ title, children, ...rest }: SimpleCardProps) => (
<Card {...rest}>
{title && <CardHeader>{title}</CardHeader>}
<CardBody>{children}</CardBody>
</Card>
);

View file

@ -0,0 +1,30 @@
import { shallow } from 'enzyme';
import { Card, CardBody, CardHeader } from 'reactstrap';
import { SimpleCard } from '../../src/utils/SimpleCard';
describe('<SimpleCard />', () => {
it.each([
[{}, 0 ],
[{ title: 'Cool title' }, 1 ],
])('renders header only if title is provided', (props, expectedAmountOfHeaders) => {
const wrapper = shallow(<SimpleCard {...props} />);
expect(wrapper.find(CardHeader)).toHaveLength(expectedAmountOfHeaders);
});
it('renders children inside body', () => {
const wrapper = shallow(<SimpleCard>Hello world</SimpleCard>);
const body = wrapper.find(CardBody);
expect(body).toHaveLength(1);
expect(body.html()).toContain('Hello world');
});
it('passes extra props to nested card', () => {
const wrapper = shallow(<SimpleCard className="foo" color="primary">Hello world</SimpleCard>);
const card = wrapper.find(Card);
expect(card.prop('className')).toEqual('foo');
expect(card.prop('color')).toEqual('primary');
});
});