mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Created component to allow selecting from existing domains list
This commit is contained in:
parent
2a7c2474cd
commit
983e4db3b1
12 changed files with 210 additions and 4 deletions
|
@ -11,6 +11,7 @@ import provideTagsServices from '../tags/services/provideServices';
|
|||
import provideUtilsServices from '../utils/services/provideServices';
|
||||
import provideMercureServices from '../mercure/services/provideServices';
|
||||
import provideSettingsServices from '../settings/services/provideServices';
|
||||
import provideDomainsServices from '../domains/services/provideServices';
|
||||
import { ConnectDecorator } from './types';
|
||||
|
||||
type LazyActionMap = Record<string, Function>;
|
||||
|
@ -41,5 +42,6 @@ provideVisitsServices(bottle, connect);
|
|||
provideUtilsServices(bottle);
|
||||
provideMercureServices(bottle);
|
||||
provideSettingsServices(bottle, connect);
|
||||
provideDomainsServices(bottle, connect);
|
||||
|
||||
export default container;
|
||||
|
|
|
@ -14,6 +14,7 @@ import { TagsList } from '../tags/reducers/tagsList';
|
|||
import { ShortUrlDetail } from '../visits/reducers/shortUrlDetail';
|
||||
import { ShortUrlVisits } from '../visits/reducers/shortUrlVisits';
|
||||
import { TagVisits } from '../visits/reducers/tagVisits';
|
||||
import { DomainsList } from '../domains/reducers/domainsList';
|
||||
|
||||
export interface ShlinkState {
|
||||
servers: ServersMap;
|
||||
|
@ -33,6 +34,7 @@ export interface ShlinkState {
|
|||
tagEdit: TagEdition;
|
||||
mercureInfo: MercureInfo;
|
||||
settings: Settings;
|
||||
domainsList: DomainsList;
|
||||
}
|
||||
|
||||
export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any;
|
||||
|
|
20
src/domains/DomainsDropdown.scss
Normal file
20
src/domains/DomainsDropdown.scss
Normal 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%;
|
||||
}
|
82
src/domains/DomainsDropdown.tsx
Normal file
82
src/domains/DomainsDropdown.tsx
Normal 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>
|
||||
);
|
||||
};
|
49
src/domains/reducers/domainsList.ts
Normal file
49
src/domains/reducers/domainsList.ts
Normal 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 });
|
||||
}
|
||||
};
|
15
src/domains/services/provideServices.ts
Normal file
15
src/domains/services/provideServices.ts
Normal 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;
|
|
@ -16,6 +16,7 @@ import tagDeleteReducer from '../tags/reducers/tagDelete';
|
|||
import tagEditReducer from '../tags/reducers/tagEdit';
|
||||
import mercureInfoReducer from '../mercure/reducers/mercureInfo';
|
||||
import settingsReducer from '../settings/reducers/settings';
|
||||
import domainsListReducer from '../domains/reducers/domainsList';
|
||||
import { ShlinkState } from '../container/types';
|
||||
|
||||
export default combineReducers<ShlinkState>({
|
||||
|
@ -36,4 +37,5 @@ export default combineReducers<ShlinkState>({
|
|||
tagEdit: tagEditReducer,
|
||||
mercureInfo: mercureInfoReducer,
|
||||
settings: settingsReducer,
|
||||
domainsList: domainsListReducer,
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import { useToggle } from '../utils/helpers/hooks';
|
|||
import { isReachableServer, SelectedServer } from '../servers/data';
|
||||
import { formatIsoDate } from '../utils/helpers/date';
|
||||
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
||||
import { DomainsDropdownProps } from '../domains/DomainsDropdown';
|
||||
import { ShortUrlData } from './data';
|
||||
import { ShortUrlCreation } from './reducers/shortUrlCreation';
|
||||
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
|
||||
|
@ -46,6 +47,7 @@ const CreateShortUrl = (
|
|||
TagsSelector: FC<TagsSelectorProps>,
|
||||
CreateShortUrlResult: FC<CreateShortUrlResultProps>,
|
||||
ForServerVersion: FC<Versions>,
|
||||
DomainsDropdown: FC<DomainsDropdownProps>,
|
||||
) => ({ createShortUrl, shortUrlCreationResult, resetCreateShortUrl, selectedServer }: CreateShortUrlProps) => {
|
||||
const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState);
|
||||
const [ moreOptionsVisible, toggleMoreOptionsVisible ] = useToggle();
|
||||
|
@ -87,6 +89,7 @@ const CreateShortUrl = (
|
|||
|
||||
const currentServerVersion = isReachableServer(selectedServer) ? selectedServer.version : '';
|
||||
const disableDomain = !versionMatch(currentServerVersion, { minVersion: '1.19.0-beta.1' });
|
||||
const showDomainsDropdown = versionMatch(currentServerVersion, { minVersion: '2.4.0' });
|
||||
const disableShortCodeLength = !versionMatch(currentServerVersion, { minVersion: '2.1.0' });
|
||||
|
||||
return (
|
||||
|
@ -123,10 +126,18 @@ const CreateShortUrl = (
|
|||
})}
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
{renderOptionalInput('domain', 'Domain', 'text', {
|
||||
{!showDomainsDropdown && renderOptionalInput('domain', 'Domain', 'text', {
|
||||
disabled: disableDomain,
|
||||
...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' },
|
||||
})}
|
||||
{showDomainsDropdown && (
|
||||
<FormGroup>
|
||||
<DomainsDropdown
|
||||
value={shortUrlCreation.domain}
|
||||
onChange={(domain?: string) => setShortUrlCreation({ ...shortUrlCreation, domain })}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -51,7 +51,14 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
);
|
||||
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useStateFlagTimeout');
|
||||
|
||||
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector', 'CreateShortUrlResult', 'ForServerVersion');
|
||||
bottle.serviceFactory(
|
||||
'CreateShortUrl',
|
||||
CreateShortUrl,
|
||||
'TagsSelector',
|
||||
'CreateShortUrlResult',
|
||||
'ForServerVersion',
|
||||
'DomainsDropdown',
|
||||
);
|
||||
bottle.decorator(
|
||||
'CreateShortUrl',
|
||||
connect([ 'shortUrlCreationResult', 'selectedServer' ], [ 'createShortUrl', 'resetCreateShortUrl' ]),
|
||||
|
|
|
@ -17,6 +17,8 @@ interface TagsSelectorConnectProps extends TagsSelectorProps {
|
|||
tagsList: TagsList;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||
{ tags, onChange, listTags, tagsList, placeholder = 'Add tags to the URL' }: TagsSelectorConnectProps,
|
||||
) => {
|
||||
|
@ -55,8 +57,8 @@ const TagsSelector = (colorGenerator: ColorGenerator) => (
|
|||
{suggestion}
|
||||
</>
|
||||
)}
|
||||
onSuggestionsFetchRequested={() => {}}
|
||||
onSuggestionsClearRequested={() => {}}
|
||||
onSuggestionsFetchRequested={noop}
|
||||
onSuggestionsClearRequested={noop}
|
||||
onSuggestionSelected={(_, { suggestion }: SuggestionSelectedEventData<string>) => {
|
||||
addTag(suggestion);
|
||||
}}
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
ShlinkVisits,
|
||||
ShlinkVisitsParams,
|
||||
ShlinkShortUrlMeta,
|
||||
ShlinkDomain,
|
||||
ShlinkDomainsResponse,
|
||||
} from './types';
|
||||
|
||||
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
|
||||
|
@ -93,6 +95,9 @@ export default class ShlinkApiClient {
|
|||
this.performRequest<ShlinkMercureInfo>('/mercure-info', 'GET')
|
||||
.then((resp) => resp.data);
|
||||
|
||||
public readonly listDomains = async (): Promise<ShlinkDomain[]> =>
|
||||
this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains.data);
|
||||
|
||||
private readonly performRequest = async <T>(url: string, method: Method = 'GET', query = {}, body = {}): Promise<AxiosResponse<T>> => {
|
||||
try {
|
||||
return await this.axios({
|
||||
|
|
|
@ -64,3 +64,12 @@ export interface ProblemDetailsError {
|
|||
message?: string; // Deprecated
|
||||
[extraProps: string]: any;
|
||||
}
|
||||
|
||||
export interface ShlinkDomain {
|
||||
domain: string;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface ShlinkDomainsResponse {
|
||||
data: ShlinkDomain[];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue