Created component to allow selecting from existing domains list

This commit is contained in:
Alejandro Celaya
2020-11-25 21:05:27 +01:00
parent 2a7c2474cd
commit 983e4db3b1
12 changed files with 210 additions and 4 deletions

View File

@@ -0,0 +1,20 @@
@import '../utils/mixins/vertical-align';
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn,
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:hover,
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:active {
text-align: left;
color: #6c757d;
border-color: #6c757d;
background-color: white;
}
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn::after {
right: .75rem;
@include vertical-align();
}
.domains-dropdown__menu {
width: 100%;
}

View File

@@ -0,0 +1,82 @@
import { useEffect } from 'react';
import {
Button,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
Input,
InputGroup,
InputGroupAddon,
InputProps,
UncontrolledTooltip,
} from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUndo } from '@fortawesome/free-solid-svg-icons';
import { isEmpty, pipe } from 'ramda';
import { useToggle } from '../utils/helpers/hooks';
import { DomainsList } from './reducers/domainsList';
import './DomainsDropdown.scss';
export interface DomainsDropdownProps extends Omit<InputProps, 'onChange'> {
value?: string;
onChange: (domain?: string) => void;
}
interface DomainsDropdownConnectProps extends DomainsDropdownProps {
listDomains: Function;
domainsList: DomainsList;
}
export const DomainsDropdown = ({ listDomains, value, domainsList, onChange }: DomainsDropdownConnectProps) => {
const [ inputDisplayed,, showInput, hideInput ] = useToggle();
const [ isDropdownOpen, toggleDropdown ] = useToggle();
const { domains } = domainsList;
const defaultDomain = domains.find(({ isDefault }) => isDefault);
const valueIsEmpty = isEmpty(value);
const unselectDomain = () => onChange('');
useEffect(() => {
listDomains();
}, []);
return inputDisplayed ? (
<InputGroup>
<Input
value={value}
placeholder="Domain"
onChange={(e) => onChange(e.target.value)}
/>
<InputGroupAddon addonType="append">
<Button id="backToDropdown" outline type="button" onClick={pipe(unselectDomain, hideInput)}>
<FontAwesomeIcon icon={faUndo} />
</Button>
<UncontrolledTooltip target="backToDropdown" placement="left" trigger="hover">
Existing domains
</UncontrolledTooltip>
</InputGroupAddon>
</InputGroup>
) : (
<Dropdown isOpen={isDropdownOpen} toggle={toggleDropdown}>
<DropdownToggle caret className="domains-dropdown__toggle-btn btn-block">
Domain: {valueIsEmpty ? defaultDomain?.domain : value}
</DropdownToggle>
<DropdownMenu className="domains-dropdown__menu">
{domains.map(({ domain, isDefault }) => (
<DropdownItem
key={domain}
active={value === domain || isDefault && valueIsEmpty}
onClick={() => onChange(domain)}
>
{domain}
{isDefault && <span className="float-right text-muted">default</span>}
</DropdownItem>
))}
<DropdownItem divider />
<DropdownItem onClick={pipe(unselectDomain, showInput)}>
<i>New domain</i>
</DropdownItem>
</DropdownMenu>
</Dropdown>
);
};

View File

@@ -0,0 +1,49 @@
import { Action, Dispatch } from 'redux';
import { ShlinkDomain } from '../../utils/services/types';
import { buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
import { GetState } from '../../container/types';
/* eslint-disable padding-line-between-statements */
export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START';
export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR';
export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
/* eslint-enable padding-line-between-statements */
export interface DomainsList {
domains: ShlinkDomain[];
loading: boolean;
error: boolean;
}
interface ListDomainsAction extends Action<string> {
domains: ShlinkDomain[];
}
const initialState: DomainsList = {
domains: [],
loading: false,
error: false,
};
export default buildReducer<DomainsList, ListDomainsAction>({
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
[LIST_DOMAINS_ERROR]: () => ({ ...initialState, error: true }),
[LIST_DOMAINS]: (_, { domains }) => ({ ...initialState, domains }),
}, initialState);
export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async (
dispatch: Dispatch,
getState: GetState,
) => {
dispatch({ type: LIST_DOMAINS_START });
const { listDomains } = buildShlinkApiClient(getState);
try {
const domains = await listDomains();
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, domains });
} catch (e) {
dispatch({ type: LIST_DOMAINS_ERROR });
}
};

View File

@@ -0,0 +1,15 @@
import Bottle from 'bottlejs';
import { ConnectDecorator } from '../../container/types';
import { listDomains } from '../reducers/domainsList';
import { DomainsDropdown } from '../DomainsDropdown';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory('DomainsDropdown', () => DomainsDropdown);
bottle.decorator('DomainsDropdown', connect([ 'domainsList' ], [ 'listDomains' ]));
// Actions
bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient');
};
export default provideServices;