mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-12-12 23:59:10 -06:00
Merge pull request #591 from acelaya-forks/feature/bootstrap5
Feature/bootstrap5
This commit is contained in:
commit
ec4b777429
@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [#543](https://github.com/shlinkio/shlink-web-client/pull/543) Redesigned settings section.
|
* [#543](https://github.com/shlinkio/shlink-web-client/pull/543) Redesigned settings section.
|
||||||
|
* [#448](https://github.com/shlinkio/shlink-web-client/pull/448) Updated to bootstrap 5.
|
||||||
* [#567](https://github.com/shlinkio/shlink-web-client/pull/567) Improved Shlink 3.0.0 compatibility by checking the `INVALID_SHORT_URL_DELETION` error code when deleting short URLs.
|
* [#567](https://github.com/shlinkio/shlink-web-client/pull/567) Improved Shlink 3.0.0 compatibility by checking the `INVALID_SHORT_URL_DELETION` error code when deleting short URLs.
|
||||||
* [#524](https://github.com/shlinkio/shlink-web-client/pull/524) Updated to react-router v6.
|
* [#524](https://github.com/shlinkio/shlink-web-client/pull/524) Updated to react-router v6.
|
||||||
* [#579](https://github.com/shlinkio/shlink-web-client/pull/579) Replaced react-color with react-colorful.
|
* [#579](https://github.com/shlinkio/shlink-web-client/pull/579) Replaced react-color with react-colorful.
|
||||||
|
|||||||
15
babel.config.js
Normal file
15
babel.config.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'react-app',
|
||||||
|
{
|
||||||
|
runtime: 'automatic',
|
||||||
|
typescript: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
|
'@babel/plugin-proposal-nullish-coalescing-operator',
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -30,6 +30,8 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^.+\\.module\\.scss$': 'identity-obj-proxy',
|
'^.+\\.module\\.scss$': 'identity-obj-proxy',
|
||||||
|
// Reactstrap module resolution does not work in jest for some reason. Manually mapping it solves the problem
|
||||||
|
'reactstrap': '<rootDir>/node_modules/reactstrap/dist/reactstrap.umd.js',
|
||||||
},
|
},
|
||||||
moduleFileExtensions: [ 'js', 'ts', 'tsx', 'json' ],
|
moduleFileExtensions: [ 'js', 'ts', 'tsx', 'json' ],
|
||||||
};
|
};
|
||||||
|
|||||||
2633
package-lock.json
generated
2633
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@ -29,7 +29,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||||
"axios": "^0.21.2",
|
"axios": "^0.21.2",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^5.1.3",
|
||||||
"bottlejs": "^2.0.0",
|
"bottlejs": "^2.0.0",
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"chart.js": "^3.5.1",
|
"chart.js": "^3.5.1",
|
||||||
@ -54,7 +54,7 @@
|
|||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.2.1",
|
||||||
"react-swipeable": "^6.0.1",
|
"react-swipeable": "^6.0.1",
|
||||||
"react-tag-autocomplete": "^6.1.0",
|
"react-tag-autocomplete": "^6.1.0",
|
||||||
"reactstrap": "^8.9.0",
|
"reactstrap": "^9.0.1",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-localstorage-simple": "^2.4.0",
|
"redux-localstorage-simple": "^2.4.0",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
@ -66,16 +66,16 @@
|
|||||||
"workbox-strategies": "^6.1.5"
|
"workbox-strategies": "^6.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.13.8",
|
"@babel/core": "^7.17.5",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
|
"@babel/plugin-proposal-optional-chaining": "^7.16.7",
|
||||||
"@shlinkio/eslint-config-js-coding-standard": "~1.2.2",
|
"@shlinkio/eslint-config-js-coding-standard": "~1.2.2",
|
||||||
"@stryker-mutator/core": "^5.4.1",
|
"@stryker-mutator/core": "^5.4.1",
|
||||||
"@stryker-mutator/jest-runner": "^5.4.1",
|
"@stryker-mutator/jest-runner": "^5.4.1",
|
||||||
"@stryker-mutator/typescript-checker": "^5.4.1",
|
"@stryker-mutator/typescript-checker": "^5.4.1",
|
||||||
"@svgr/webpack": "^5.5.0",
|
"@svgr/webpack": "^5.5.0",
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
"@types/enzyme": "^3.10.10",
|
"@types/enzyme": "^3.10.11",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/leaflet": "^1.5.23",
|
"@types/leaflet": "^1.5.23",
|
||||||
"@types/qs": "^6.9.5",
|
"@types/qs": "^6.9.5",
|
||||||
@ -92,9 +92,8 @@
|
|||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.5",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.5",
|
||||||
"adm-zip": "^0.4.16",
|
"adm-zip": "^0.4.16",
|
||||||
"autoprefixer": "^10.0.2",
|
"autoprefixer": "^10.0.2",
|
||||||
"babel-core": "7.0.0-bridge.0",
|
|
||||||
"babel-jest": "^27.5.1",
|
"babel-jest": "^27.5.1",
|
||||||
"babel-loader": "^8.2.1",
|
"babel-loader": "^8.2.3",
|
||||||
"babel-plugin-named-asset-import": "^0.3.7",
|
"babel-plugin-named-asset-import": "^0.3.7",
|
||||||
"babel-preset-react-app": "^10.0.0",
|
"babel-preset-react-app": "^10.0.0",
|
||||||
"babel-runtime": "^6.26.0",
|
"babel-runtime": "^6.26.0",
|
||||||
@ -113,7 +112,7 @@
|
|||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
"html-webpack-plugin": "^4.5.0",
|
"html-webpack-plugin": "^4.5.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^27.3.1",
|
"jest": "^27.5.1",
|
||||||
"mini-css-extract-plugin": "^1.3.1",
|
"mini-css-extract-plugin": "^1.3.1",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
||||||
@ -147,20 +146,6 @@
|
|||||||
"whatwg-fetch": "^3.5.0",
|
"whatwg-fetch": "^3.5.0",
|
||||||
"workbox-webpack-plugin": "^6.1.5"
|
"workbox-webpack-plugin": "^6.1.5"
|
||||||
},
|
},
|
||||||
"babel": {
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"react-app",
|
|
||||||
{
|
|
||||||
"runtime": "automatic"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-proposal-optional-chaining",
|
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
"not dead",
|
"not dead",
|
||||||
|
|||||||
@ -24,8 +24,8 @@ export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, toggle, forc
|
|||||||
<h4 className="mb-4">This app has just been updated!</h4>
|
<h4 className="mb-4">This app has just been updated!</h4>
|
||||||
<p className="mb-0">
|
<p className="mb-0">
|
||||||
Restart it to enjoy the new features.
|
Restart it to enjoy the new features.
|
||||||
<Button disabled={isUpdating} className="ml-2" color="secondary" size="sm" onClick={update}>
|
<Button disabled={isUpdating} className="ms-2" color="secondary" size="sm" onClick={update}>
|
||||||
{!isUpdating && <>Restart now <FontAwesomeIcon icon={reloadIcon} className="ml-1" /></>}
|
{!isUpdating && <>Restart now <FontAwesomeIcon icon={reloadIcon} className="ms-1" /></>}
|
||||||
{isUpdating && <>Restarting...</>}
|
{isUpdating && <>Restarting...</>}
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
@import '../utils/base';
|
@import '../utils/base';
|
||||||
@import '../utils/mixins/vertical-align';
|
@import '../utils/mixins/vertical-align';
|
||||||
|
|
||||||
|
$mainCardWidth: 720px;
|
||||||
|
$totalColumns: 12;
|
||||||
|
$logoColumns: 5;
|
||||||
|
|
||||||
.home {
|
.home {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
@ -21,12 +25,12 @@
|
|||||||
.home__logo {
|
.home__logo {
|
||||||
@include vertical-align();
|
@include vertical-align();
|
||||||
|
|
||||||
width: calc(100% - 3rem);
|
width: calc(#{$mainCardWidth / $totalColumns * $logoColumns} - 3rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home__main-card {
|
.home__main-card {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 720px;
|
max-width: $mainCardWidth;
|
||||||
|
|
||||||
@media (min-width: $mdMin) {
|
@media (min-width: $mdMin) {
|
||||||
@include vertical-align();
|
@include vertical-align();
|
||||||
|
|||||||
@ -46,14 +46,14 @@ const Home = ({ servers }: HomeProps) => {
|
|||||||
<div className="p-4 text-center">
|
<div className="p-4 text-center">
|
||||||
<p className="mb-5">This application will help you manage your Shlink servers.</p>
|
<p className="mb-5">This application will help you manage your Shlink servers.</p>
|
||||||
<p>
|
<p>
|
||||||
<Link to="/server/create" className="btn btn-outline-primary btn-lg mr-2">
|
<Link to="/server/create" className="btn btn-outline-primary btn-lg me-2">
|
||||||
<FontAwesomeIcon icon={faPlus} /> <span className="ml-1">Add a server</span>
|
<FontAwesomeIcon icon={faPlus} /> <span className="ms-1">Add a server</span>
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-0 mt-5">
|
<p className="mb-0 mt-5">
|
||||||
<ExternalLink href="https://shlink.io/documentation">
|
<ExternalLink href="https://shlink.io/documentation">
|
||||||
<small>
|
<small>
|
||||||
<span className="mr-1">Learn more about Shlink</span> <FontAwesomeIcon icon={faExternalLinkAlt} />
|
<span className="me-1">Learn more about Shlink</span> <FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||||
</small>
|
</small>
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const MainHeader = (ServersDropdown: FC) => () => {
|
|||||||
</NavbarToggler>
|
</NavbarToggler>
|
||||||
|
|
||||||
<Collapse navbar isOpen={isOpen}>
|
<Collapse navbar isOpen={isOpen}>
|
||||||
<Nav navbar className="ml-auto">
|
<Nav navbar className="ms-auto">
|
||||||
<NavItem>
|
<NavItem>
|
||||||
<NavLink tag={Link} to={settingsPath} active={pathname.startsWith(settingsPath)}>
|
<NavLink tag={Link} to={settingsPath} active={pathname.startsWith(settingsPath)}>
|
||||||
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
||||||
|
|||||||
@ -1,13 +1,7 @@
|
|||||||
@import '../utils/base';
|
@import '../utils/base';
|
||||||
|
|
||||||
.menu-layout__swipeable {
|
.menu-layout__swipeable {
|
||||||
$offset: 15px;
|
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-right: -$offset;
|
|
||||||
margin-left: -$offset;
|
|
||||||
padding-left: $offset;
|
|
||||||
padding-right: $offset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-layout__swipeable-inner {
|
.menu-layout__swipeable-inner {
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export const DomainRow: FC<DomainRowProps> = (
|
|||||||
<td className="responsive-table__cell text-lg-center" data-th="Status">
|
<td className="responsive-table__cell text-lg-center" data-th="Status">
|
||||||
<DomainStatusIcon status={status} />
|
<DomainStatusIcon status={status} />
|
||||||
</td>
|
</td>
|
||||||
<td className="responsive-table__cell text-right">
|
<td className="responsive-table__cell text-end">
|
||||||
<span id={!canEditDomain ? 'defaultDomainBtn' : undefined}>
|
<span id={!canEditDomain ? 'defaultDomainBtn' : undefined}>
|
||||||
<Button outline size="sm" disabled={!canEditDomain} onClick={!canEditDomain ? undefined : toggle}>
|
<Button outline size="sm" disabled={!canEditDomain} onClick={!canEditDomain ? undefined : toggle}>
|
||||||
<FontAwesomeIcon fixedWidth icon={!canEditDomain ? forbiddenIcon : editIcon} />
|
<FontAwesomeIcon fixedWidth icon={!canEditDomain ? forbiddenIcon : editIcon} />
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Button, DropdownItem, Input, InputGroup, InputGroupAddon, UncontrolledTooltip } from 'reactstrap';
|
import { Button, DropdownItem, Input, InputGroup, UncontrolledTooltip, InputProps } from 'reactstrap';
|
||||||
import { InputProps } from 'reactstrap/lib/Input';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faUndo } from '@fortawesome/free-solid-svg-icons';
|
import { faUndo } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { isEmpty, pipe } from 'ramda';
|
import { isEmpty, pipe } from 'ramda';
|
||||||
@ -32,24 +31,22 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
|
|||||||
return inputDisplayed ? (
|
return inputDisplayed ? (
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Input
|
<Input
|
||||||
value={value}
|
value={value ?? ''}
|
||||||
placeholder="Domain"
|
placeholder="Domain"
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<InputGroupAddon addonType="append">
|
<Button
|
||||||
<Button
|
id="backToDropdown"
|
||||||
id="backToDropdown"
|
outline
|
||||||
outline
|
type="button"
|
||||||
type="button"
|
className="domains-dropdown__back-btn"
|
||||||
className="domains-dropdown__back-btn"
|
onClick={pipe(unselectDomain, hideInput)}
|
||||||
onClick={pipe(unselectDomain, hideInput)}
|
>
|
||||||
>
|
<FontAwesomeIcon icon={faUndo} />
|
||||||
<FontAwesomeIcon icon={faUndo} />
|
</Button>
|
||||||
</Button>
|
<UncontrolledTooltip target="backToDropdown" placement="left" trigger="hover">
|
||||||
<UncontrolledTooltip target="backToDropdown" placement="left" trigger="hover">
|
Existing domains
|
||||||
Existing domains
|
</UncontrolledTooltip>
|
||||||
</UncontrolledTooltip>
|
|
||||||
</InputGroupAddon>
|
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
) : (
|
) : (
|
||||||
<DropdownBtn
|
<DropdownBtn
|
||||||
@ -63,7 +60,7 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
|
|||||||
onClick={() => onChange(domain)}
|
onClick={() => onChange(domain)}
|
||||||
>
|
>
|
||||||
{domain}
|
{domain}
|
||||||
{isDefault && <span className="float-right text-muted">default</span>}
|
{isDefault && <span className="float-end text-muted">default</span>}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
))}
|
))}
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export const ManageDomains: FC<ManageDomainsProps> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleCard>
|
<SimpleCard>
|
||||||
<table className="table table-hover mb-0">
|
<table className="table table-hover responsive-table mb-0">
|
||||||
<thead className="responsive-table__header">
|
<thead className="responsive-table__header">
|
||||||
<tr>{headers.map((column, index) => <th key={index}>{column}</th>)}</tr>
|
<tr>{headers.map((column, index) => <th key={index}>{column}</th>)}</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import { ShlinkDomain, ShlinkDomainRedirects } from '../../api/types';
|
import { ShlinkDomain, ShlinkDomainRedirects } from '../../api/types';
|
||||||
import { FormGroupContainer, FormGroupContainerProps } from '../../utils/FormGroupContainer';
|
import { InputFormGroup, InputFormGroupProps } from '../../utils/forms/InputFormGroup';
|
||||||
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
|
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
|
||||||
import { InfoTooltip } from '../../utils/InfoTooltip';
|
import { InfoTooltip } from '../../utils/InfoTooltip';
|
||||||
|
|
||||||
@ -12,8 +12,8 @@ interface EditDomainRedirectsModalProps {
|
|||||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormGroup: FC<FormGroupContainerProps & { isLast?: boolean }> = ({ isLast, ...rest }) => (
|
const FormGroup: FC<InputFormGroupProps & { isLast?: boolean }> = ({ isLast, ...rest }) => (
|
||||||
<FormGroupContainer
|
<InputFormGroup
|
||||||
{...rest}
|
{...rest}
|
||||||
required={false}
|
required={false}
|
||||||
type="url"
|
type="url"
|
||||||
@ -42,20 +42,20 @@ export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
|
|||||||
<ModalHeader toggle={toggle}>Edit redirects for <b>{domain.domain}</b></ModalHeader>
|
<ModalHeader toggle={toggle}>Edit redirects for <b>{domain.domain}</b></ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<FormGroup value={baseUrlRedirect} onChange={setBaseUrlRedirect}>
|
<FormGroup value={baseUrlRedirect} onChange={setBaseUrlRedirect}>
|
||||||
<InfoTooltip className="mr-2" placement="bottom">
|
<InfoTooltip className="me-2" placement="bottom">
|
||||||
Visitors accessing the base url, as in <b>https://{domain.domain}/</b>, will be redirected to this URL.
|
Visitors accessing the base url, as in <b>https://{domain.domain}/</b>, will be redirected to this URL.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
Base URL
|
Base URL
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup value={regular404Redirect} onChange={setRegular404Redirect}>
|
<FormGroup value={regular404Redirect} onChange={setRegular404Redirect}>
|
||||||
<InfoTooltip className="mr-2" placement="bottom">
|
<InfoTooltip className="me-2" placement="bottom">
|
||||||
Visitors accessing a url not matching a short URL pattern, as in <b>https://{domain.domain}/???/[...]</b>,
|
Visitors accessing a url not matching a short URL pattern, as in <b>https://{domain.domain}/???/[...]</b>,
|
||||||
will be redirected to this URL.
|
will be redirected to this URL.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
Regular 404
|
Regular 404
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup value={invalidShortUrlRedirect} isLast onChange={setInvalidShortUrlRedirect}>
|
<FormGroup value={invalidShortUrlRedirect} isLast onChange={setInvalidShortUrlRedirect}>
|
||||||
<InfoTooltip className="mr-2" placement="bottom">
|
<InfoTooltip className="me-2" placement="bottom">
|
||||||
Visitors accessing a url matching a short URL pattern, but not matching an existing short code, will be
|
Visitors accessing a url matching a short URL pattern, but not matching an existing short code, will be
|
||||||
redirected to this URL.
|
redirected to this URL.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
|
|||||||
@ -11,6 +11,10 @@
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
@ -19,6 +23,17 @@ body,
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.btn-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* stylelint-disable-next-line selector-max-pseudo-class */
|
||||||
|
a:not(.nav-link):not(.navbar-brand):not(.page-link):not(.highlight-card):not(.btn):hover,
|
||||||
|
.btn-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.bg-main {
|
.bg-main {
|
||||||
background-color: $mainColor !important;
|
background-color: $mainColor !important;
|
||||||
}
|
}
|
||||||
@ -74,7 +89,8 @@ hr {
|
|||||||
border-color: var(--table-border-color);
|
border-color: var(--table-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-link:hover {
|
.page-link:hover,
|
||||||
|
.page-link:focus {
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +114,22 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Deprecated. Brought from bootstrap 4 */
|
||||||
|
.btn-block {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-primary:hover,
|
||||||
|
.btn-primary:active,
|
||||||
|
.btn-primary.active,
|
||||||
|
.btn-outline-primary:hover,
|
||||||
|
.btn-outline-primary:active,
|
||||||
|
.btn-outline-primary.active, {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-item,
|
.dropdown-item,
|
||||||
.dropdown-item-text {
|
.dropdown-item-text {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
@ -133,10 +165,15 @@ hr {
|
|||||||
.close,
|
.close,
|
||||||
.close:hover,
|
.close:hover,
|
||||||
.table,
|
.table,
|
||||||
.table-hover tbody tr:hover {
|
.table-hover > tbody > tr:hover > *,
|
||||||
|
.table-hover > tbody > tr > * {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
filter: var(--btn-close-filter);
|
||||||
|
}
|
||||||
|
|
||||||
.table-hover tbody tr:hover {
|
.table-hover tbody tr:hover {
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,7 +61,7 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
|
|||||||
{!hasServers &&
|
{!hasServers &&
|
||||||
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onImportError={setErrorImporting} />}
|
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onImportError={setErrorImporting} />}
|
||||||
{hasServers && <Button outline onClick={goBack}>Cancel</Button>}
|
{hasServers && <Button outline onClick={goBack}>Cancel</Button>}
|
||||||
<Button outline color="primary" className="ml-2">Create server</Button>
|
<Button outline color="primary" className="ms-2">Create server</Button>
|
||||||
</ServerForm>
|
</ServerForm>
|
||||||
|
|
||||||
{serversImported && <ImportResult type="success" />}
|
{serversImported && <ImportResult type="success" />}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProp
|
|||||||
initialValues={selectedServer}
|
initialValues={selectedServer}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<Button outline className="mr-2" onClick={goBack}>Cancel</Button>
|
<Button outline className="me-2" onClick={goBack}>Cancel</Button>
|
||||||
<Button outline color="primary">Save</Button>
|
<Button outline color="primary">Save</Button>
|
||||||
</ServerForm>
|
</ServerForm>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
|
|||||||
@ -45,12 +45,12 @@ export const ManageServers = (
|
|||||||
<div className="col-md-6 d-flex d-md-block mb-2 mb-md-0">
|
<div className="col-md-6 d-flex d-md-block mb-2 mb-md-0">
|
||||||
<ImportServersBtn className="flex-fill" onImportError={setErrorImporting}>Import servers</ImportServersBtn>
|
<ImportServersBtn className="flex-fill" onImportError={setErrorImporting}>Import servers</ImportServersBtn>
|
||||||
{allServers.length > 0 && (
|
{allServers.length > 0 && (
|
||||||
<Button outline className="ml-2 flex-fill" onClick={async () => serversExporter.exportServers()}>
|
<Button outline className="ms-2 flex-fill" onClick={async () => serversExporter.exportServers()}>
|
||||||
<FontAwesomeIcon icon={exportIcon} fixedWidth /> Export servers
|
<FontAwesomeIcon icon={exportIcon} fixedWidth /> Export servers
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6 text-md-right d-flex d-md-block">
|
<div className="col-md-6 text-md-end d-flex d-md-block">
|
||||||
<Button outline color="primary" className="flex-fill" tag={Link} to="/server/create">
|
<Button outline color="primary" className="flex-fill" tag={Link} to="/server/create">
|
||||||
<FontAwesomeIcon icon={plusIcon} fixedWidth /> Add a server
|
<FontAwesomeIcon icon={plusIcon} fixedWidth /> Add a server
|
||||||
</Button>
|
</Button>
|
||||||
@ -58,7 +58,7 @@ export const ManageServers = (
|
|||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<SimpleCard>
|
<SimpleCard>
|
||||||
<table className="table table-hover mb-0">
|
<table className="table table-hover responsive-table mb-0">
|
||||||
<thead className="responsive-table__header">
|
<thead className="responsive-table__header">
|
||||||
<tr>
|
<tr>
|
||||||
{hasAutoConnect && <th style={{ width: '50px' }} />}
|
{hasAutoConnect && <th style={{ width: '50px' }} />}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const ManageServersRow = (
|
|||||||
<Link to={`/server/${server.id}`}>{server.name}</Link>
|
<Link to={`/server/${server.id}`}>{server.name}</Link>
|
||||||
</th>
|
</th>
|
||||||
<td className="responsive-table__cell" data-th="Base URL">{server.url}</td>
|
<td className="responsive-table__cell" data-th="Base URL">{server.url}</td>
|
||||||
<td className="responsive-table__cell text-right">
|
<td className="responsive-table__cell text-end">
|
||||||
<ManageServersRowDropdown server={server} />
|
<ManageServersRowDropdown server={server} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export const Overview = (
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<span className="d-sm-none">Create a short URL</span>
|
<span className="d-sm-none">Create a short URL</span>
|
||||||
<h5 className="d-none d-sm-inline">Create a short URL</h5>
|
<h5 className="d-none d-sm-inline">Create a short URL</h5>
|
||||||
<Link className="float-right" to={`/server/${serverId}/create-short-url`}>Advanced options »</Link>
|
<Link className="float-end" to={`/server/${serverId}/create-short-url`}>Advanced options »</Link>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<CreateShortUrl basicMode />
|
<CreateShortUrl basicMode />
|
||||||
@ -96,7 +96,7 @@ export const Overview = (
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<span className="d-sm-none">Recently created URLs</span>
|
<span className="d-sm-none">Recently created URLs</span>
|
||||||
<h5 className="d-none d-sm-inline">Recently created URLs</h5>
|
<h5 className="d-none d-sm-inline">Recently created URLs</h5>
|
||||||
<Link className="float-right" to={`/server/${serverId}/list-short-urls/1`}>See all »</Link>
|
<Link className="float-end" to={`/server/${serverId}/list-short-urls/1`}>See all »</Link>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<ShortUrlsTable
|
<ShortUrlsTable
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
|
|||||||
if (isEmpty(serversList)) {
|
if (isEmpty(serversList)) {
|
||||||
return (
|
return (
|
||||||
<DropdownItem tag={Link} to="/server/create">
|
<DropdownItem tag={Link} to="/server/create">
|
||||||
<FontAwesomeIcon icon={plusIcon} /> <span className="ml-1">Add a server</span>
|
<FontAwesomeIcon icon={plusIcon} /> <span className="ms-1">Add a server</span>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -31,7 +31,7 @@ const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
|
|||||||
))}
|
))}
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
<DropdownItem tag={Link} to="/manage-servers">
|
<DropdownItem tag={Link} to="/manage-servers">
|
||||||
<FontAwesomeIcon icon={serverIcon} /> <span className="ml-1">Manage servers</span>
|
<FontAwesomeIcon icon={serverIcon} /> <span className="ms-1">Manage servers</span>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -40,9 +40,9 @@ const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
|
|||||||
return (
|
return (
|
||||||
<UncontrolledDropdown nav inNavbar>
|
<UncontrolledDropdown nav inNavbar>
|
||||||
<DropdownToggle nav caret>
|
<DropdownToggle nav caret>
|
||||||
<FontAwesomeIcon icon={serverIcon} /> <span className="ml-1">Servers</span>
|
<FontAwesomeIcon icon={serverIcon} /> <span className="ms-1">Servers</span>
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu right>{renderServers()}</DropdownMenu>
|
<DropdownMenu right style={{ right: 0 }}>{renderServers()}</DropdownMenu>
|
||||||
</UncontrolledDropdown>
|
</UncontrolledDropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,6 +17,10 @@
|
|||||||
padding: .75rem 2.5rem .75rem 1rem;
|
padding: .75rem 2.5rem .75rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.servers-list__server-item:not(:hover) {
|
||||||
|
color: $mainColor;
|
||||||
|
}
|
||||||
|
|
||||||
.servers-list__server-item:hover {
|
.servers-list__server-item:hover {
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
@import '../../utils/base';
|
|
||||||
|
|
||||||
.server-form .form-group:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-form__label {
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
@ -1,9 +1,8 @@
|
|||||||
import { FC, ReactNode, useEffect, useState } from 'react';
|
import { FC, ReactNode, useEffect, useState } from 'react';
|
||||||
import { FormGroupContainer, FormGroupContainerProps } from '../../utils/FormGroupContainer';
|
import { InputFormGroup } from '../../utils/forms/InputFormGroup';
|
||||||
import { handleEventPreventingDefault } from '../../utils/utils';
|
import { handleEventPreventingDefault } from '../../utils/utils';
|
||||||
import { ServerData } from '../data';
|
import { ServerData } from '../data';
|
||||||
import { SimpleCard } from '../../utils/SimpleCard';
|
import { SimpleCard } from '../../utils/SimpleCard';
|
||||||
import './ServerForm.scss';
|
|
||||||
|
|
||||||
interface ServerFormProps {
|
interface ServerFormProps {
|
||||||
onSubmit: (server: ServerData) => void;
|
onSubmit: (server: ServerData) => void;
|
||||||
@ -11,9 +10,6 @@ interface ServerFormProps {
|
|||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormGroup: FC<FormGroupContainerProps> = (props) =>
|
|
||||||
<FormGroupContainer {...props} labelClassName="server-form__label" />;
|
|
||||||
|
|
||||||
export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, children, title }) => {
|
export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, children, title }) => {
|
||||||
const [ name, setName ] = useState('');
|
const [ name, setName ] = useState('');
|
||||||
const [ url, setUrl ] = useState('');
|
const [ url, setUrl ] = useState('');
|
||||||
@ -29,12 +25,12 @@ export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, child
|
|||||||
return (
|
return (
|
||||||
<form className="server-form" onSubmit={handleSubmit}>
|
<form className="server-form" onSubmit={handleSubmit}>
|
||||||
<SimpleCard className="mb-3" title={title}>
|
<SimpleCard className="mb-3" title={title}>
|
||||||
<FormGroup value={name} onChange={setName}>Name</FormGroup>
|
<InputFormGroup value={name} onChange={setName}>Name</InputFormGroup>
|
||||||
<FormGroup type="url" value={url} onChange={setUrl}>URL</FormGroup>
|
<InputFormGroup type="url" value={url} onChange={setUrl}>URL</InputFormGroup>
|
||||||
<FormGroup value={apiKey} onChange={setApiKey}>API key</FormGroup>
|
<InputFormGroup value={apiKey} onChange={setApiKey}>API key</InputFormGroup>
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
|
|
||||||
<div className="text-right">{children}</div>
|
<div className="text-end">{children}</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,8 @@ 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 { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
import { FormText } from '../utils/forms/FormText';
|
||||||
|
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||||
import { Settings } from './reducers/settings';
|
import { Settings } from './reducers/settings';
|
||||||
|
|
||||||
interface RealTimeUpdatesProps {
|
interface RealTimeUpdatesProps {
|
||||||
@ -19,15 +21,16 @@ const RealTimeUpdatesSettings = (
|
|||||||
<FormGroup>
|
<FormGroup>
|
||||||
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
|
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
|
||||||
Enable or disable real-time updates.
|
Enable or disable real-time updates.
|
||||||
<small className="form-text text-muted">
|
<FormText>
|
||||||
Real-time updates are currently being <b>{realTimeUpdates.enabled ? 'processed' : 'ignored'}</b>.
|
Real-time updates are currently being <b>{realTimeUpdates.enabled ? 'processed' : 'ignored'}</b>.
|
||||||
</small>
|
</FormText>
|
||||||
</ToggleSwitch>
|
</ToggleSwitch>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup className="mb-0">
|
<LabeledFormGroup
|
||||||
<label className={classNames({ 'text-muted': !realTimeUpdates.enabled })}>
|
noMargin
|
||||||
Real-time updates frequency (in minutes):
|
label="Real-time updates frequency (in minutes):"
|
||||||
</label>
|
labelClassName={classNames('form-label', { 'text-muted': !realTimeUpdates.enabled })}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
@ -37,16 +40,16 @@ const RealTimeUpdatesSettings = (
|
|||||||
onChange={({ target }) => setRealTimeUpdatesInterval(Number(target.value))}
|
onChange={({ target }) => setRealTimeUpdatesInterval(Number(target.value))}
|
||||||
/>
|
/>
|
||||||
{realTimeUpdates.enabled && (
|
{realTimeUpdates.enabled && (
|
||||||
<small className="form-text text-muted">
|
<FormText>
|
||||||
{realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && (
|
{realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && (
|
||||||
<span>
|
<span>
|
||||||
Updates will be reflected in the UI every <b>{realTimeUpdates.interval}</b> minute{realTimeUpdates.interval > 1 && 's'}.
|
Updates will be reflected in the UI every <b>{realTimeUpdates.interval}</b> minute{realTimeUpdates.interval > 1 && 's'}.
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'}
|
{!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'}
|
||||||
</small>
|
</FormText>
|
||||||
)}
|
)}
|
||||||
</FormGroup>
|
</LabeledFormGroup>
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -18,16 +18,16 @@ const Settings = (
|
|||||||
Tags: FC,
|
Tags: FC,
|
||||||
) => () => (
|
) => () => (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<NavPills>
|
<NavPills className="mb-3">
|
||||||
<NavPillItem to="general">General</NavPillItem>
|
<NavPillItem to="general">General</NavPillItem>
|
||||||
<NavPillItem to="short-urls">Short URLs</NavPillItem>
|
<NavPillItem to="short-urls">Short URLs</NavPillItem>
|
||||||
<NavPillItem to="secondary-items">Secondary items</NavPillItem>
|
<NavPillItem to="other-items">Other items</NavPillItem>
|
||||||
</NavPills>
|
</NavPills>
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="general" element={<SettingsSections items={[ <UserInterface key="one" />, <RealTimeUpdates key="two" /> ]} />} />
|
<Route path="general" element={<SettingsSections items={[ <UserInterface key="one" />, <RealTimeUpdates key="two" /> ]} />} />
|
||||||
<Route path="short-urls" element={<SettingsSections items={[ <ShortUrlCreation key="one" />, <ShortUrlsList key="two" /> ]} />} />
|
<Route path="short-urls" element={<SettingsSections items={[ <ShortUrlCreation key="one" />, <ShortUrlsList key="two" /> ]} />} />
|
||||||
<Route path="secondary-items" element={<SettingsSections items={[ <Tags key="one" />, <Visits key="two" /> ]} />} />
|
<Route path="other-items" element={<SettingsSections items={[ <Tags key="one" />, <Visits key="two" /> ]} />} />
|
||||||
<Route path="*" element={<Navigate replace to="general" />} />
|
<Route path="*" element={<Navigate replace to="general" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { DropdownItem, FormGroup } from 'reactstrap';
|
|||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import ToggleSwitch from '../utils/ToggleSwitch';
|
import ToggleSwitch from '../utils/ToggleSwitch';
|
||||||
import { DropdownBtn } from '../utils/DropdownBtn';
|
import { DropdownBtn } from '../utils/DropdownBtn';
|
||||||
|
import { FormText } from '../utils/forms/FormText';
|
||||||
|
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||||
import { Settings, ShortUrlCreationSettings as ShortUrlsSettings, TagFilteringMode } from './reducers/settings';
|
import { Settings, ShortUrlCreationSettings as ShortUrlsSettings, TagFilteringMode } from './reducers/settings';
|
||||||
|
|
||||||
interface ShortUrlCreationProps {
|
interface ShortUrlCreationProps {
|
||||||
@ -31,10 +33,10 @@ export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ settings,
|
|||||||
onChange={(validateUrls) => setShortUrlCreationSettings({ ...shortUrlCreation, validateUrls })}
|
onChange={(validateUrls) => setShortUrlCreationSettings({ ...shortUrlCreation, validateUrls })}
|
||||||
>
|
>
|
||||||
Request validation on long URLs when creating new short URLs.
|
Request validation on long URLs when creating new short URLs.
|
||||||
<small className="form-text text-muted">
|
<FormText>
|
||||||
The initial state of the <b>Validate URL</b> checkbox will
|
The initial state of the <b>Validate URL</b> checkbox will
|
||||||
be <b>{shortUrlCreation.validateUrls ? 'checked' : 'unchecked'}</b>.
|
be <b>{shortUrlCreation.validateUrls ? 'checked' : 'unchecked'}</b>.
|
||||||
</small>
|
</FormText>
|
||||||
</ToggleSwitch>
|
</ToggleSwitch>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
@ -43,14 +45,13 @@ export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ settings,
|
|||||||
onChange={(forwardQuery) => setShortUrlCreationSettings({ ...shortUrlCreation, forwardQuery })}
|
onChange={(forwardQuery) => setShortUrlCreationSettings({ ...shortUrlCreation, forwardQuery })}
|
||||||
>
|
>
|
||||||
Make all new short URLs forward their query params to the long URL.
|
Make all new short URLs forward their query params to the long URL.
|
||||||
<small className="form-text text-muted">
|
<FormText>
|
||||||
The initial state of the <b>Forward query params on redirect</b> checkbox will
|
The initial state of the <b>Forward query params on redirect</b> checkbox will
|
||||||
be <b>{shortUrlCreation.forwardQuery ?? true ? 'checked' : 'unchecked'}</b>.
|
be <b>{shortUrlCreation.forwardQuery ?? true ? 'checked' : 'unchecked'}</b>.
|
||||||
</small>
|
</FormText>
|
||||||
</ToggleSwitch>
|
</ToggleSwitch>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup className="mb-0">
|
<LabeledFormGroup noMargin label="Tag suggestions search mode:">
|
||||||
<label>Tag suggestions search mode:</label>
|
|
||||||
<DropdownBtn text={tagFilteringModeText(shortUrlCreation.tagFilteringMode)}>
|
<DropdownBtn text={tagFilteringModeText(shortUrlCreation.tagFilteringMode)}>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
active={!shortUrlCreation.tagFilteringMode || shortUrlCreation.tagFilteringMode === 'startsWith'}
|
active={!shortUrlCreation.tagFilteringMode || shortUrlCreation.tagFilteringMode === 'startsWith'}
|
||||||
@ -65,10 +66,8 @@ export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ settings,
|
|||||||
{tagFilteringModeText('includes')}
|
{tagFilteringModeText('includes')}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownBtn>
|
</DropdownBtn>
|
||||||
<small className="form-text text-muted">
|
<FormText>{tagFilteringModeHint(shortUrlCreation.tagFilteringMode)}</FormText>
|
||||||
{tagFilteringModeHint(shortUrlCreation.tagFilteringMode)}
|
</LabeledFormGroup>
|
||||||
</small>
|
|
||||||
</FormGroup>
|
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { FormGroup } from 'reactstrap';
|
|
||||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
||||||
import { SHORT_URLS_ORDERABLE_FIELDS } from '../short-urls/data';
|
import { SHORT_URLS_ORDERABLE_FIELDS } from '../short-urls/data';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||||
import { DEFAULT_SHORT_URLS_ORDERING, Settings, ShortUrlsListSettings as ShortUrlsSettings } from './reducers/settings';
|
import { DEFAULT_SHORT_URLS_ORDERING, Settings, ShortUrlsListSettings as ShortUrlsSettings } from './reducers/settings';
|
||||||
|
|
||||||
interface ShortUrlsListProps {
|
interface ShortUrlsListProps {
|
||||||
@ -14,13 +14,12 @@ export const ShortUrlsListSettings: FC<ShortUrlsListProps> = (
|
|||||||
{ settings: { shortUrlsList }, setShortUrlsListSettings },
|
{ settings: { shortUrlsList }, setShortUrlsListSettings },
|
||||||
) => (
|
) => (
|
||||||
<SimpleCard title="Short URLs list" className="h-100">
|
<SimpleCard title="Short URLs list" className="h-100">
|
||||||
<FormGroup className="mb-0">
|
<LabeledFormGroup noMargin label="Default ordering for short URLs list:">
|
||||||
<label>Default ordering for short URLs list:</label>
|
|
||||||
<OrderingDropdown
|
<OrderingDropdown
|
||||||
items={SHORT_URLS_ORDERABLE_FIELDS}
|
items={SHORT_URLS_ORDERABLE_FIELDS}
|
||||||
order={shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING}
|
order={shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING}
|
||||||
onChange={(field, dir) => setShortUrlsListSettings({ defaultOrdering: { field, dir } })}
|
onChange={(field, dir) => setShortUrlsListSettings({ defaultOrdering: { field, dir } })}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</LabeledFormGroup>
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { FormGroup } from 'reactstrap';
|
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import { TagsModeDropdown } from '../tags/TagsModeDropdown';
|
import { TagsModeDropdown } from '../tags/TagsModeDropdown';
|
||||||
import { capitalize } from '../utils/utils';
|
import { capitalize } from '../utils/utils';
|
||||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
||||||
import { TAGS_ORDERABLE_FIELDS } from '../tags/data/TagsListChildrenProps';
|
import { TAGS_ORDERABLE_FIELDS } from '../tags/data/TagsListChildrenProps';
|
||||||
|
import { FormText } from '../utils/forms/FormText';
|
||||||
|
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||||
import { Settings, TagsSettings as TagsSettingsOptions } from './reducers/settings';
|
import { Settings, TagsSettings as TagsSettingsOptions } from './reducers/settings';
|
||||||
|
|
||||||
interface TagsProps {
|
interface TagsProps {
|
||||||
@ -14,22 +15,20 @@ interface TagsProps {
|
|||||||
|
|
||||||
export const TagsSettings: FC<TagsProps> = ({ settings: { tags }, setTagsSettings }) => (
|
export const TagsSettings: FC<TagsProps> = ({ settings: { tags }, setTagsSettings }) => (
|
||||||
<SimpleCard title="Tags" className="h-100">
|
<SimpleCard title="Tags" className="h-100">
|
||||||
<FormGroup>
|
<LabeledFormGroup label="Default display mode when managing tags:">
|
||||||
<label>Default display mode when managing tags:</label>
|
|
||||||
<TagsModeDropdown
|
<TagsModeDropdown
|
||||||
mode={tags?.defaultMode ?? 'cards'}
|
mode={tags?.defaultMode ?? 'cards'}
|
||||||
renderTitle={(tagsMode) => capitalize(tagsMode)}
|
renderTitle={(tagsMode) => capitalize(tagsMode)}
|
||||||
onChange={(defaultMode) => setTagsSettings({ ...tags, defaultMode })}
|
onChange={(defaultMode) => setTagsSettings({ ...tags, defaultMode })}
|
||||||
/>
|
/>
|
||||||
<small className="form-text text-muted">Tags will be displayed as <b>{tags?.defaultMode ?? 'cards'}</b>.</small>
|
<FormText>Tags will be displayed as <b>{tags?.defaultMode ?? 'cards'}</b>.</FormText>
|
||||||
</FormGroup>
|
</LabeledFormGroup>
|
||||||
<FormGroup className="mb-0">
|
<LabeledFormGroup noMargin label="Default ordering for tags list:">
|
||||||
<label>Default ordering for tags list:</label>
|
|
||||||
<OrderingDropdown
|
<OrderingDropdown
|
||||||
items={TAGS_ORDERABLE_FIELDS}
|
items={TAGS_ORDERABLE_FIELDS}
|
||||||
order={tags?.defaultOrdering ?? {}}
|
order={tags?.defaultOrdering ?? {}}
|
||||||
onChange={(field, dir) => setTagsSettings({ ...tags, defaultOrdering: { field, dir } })}
|
onChange={(field, dir) => setTagsSettings({ ...tags, defaultOrdering: { field, dir } })}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</LabeledFormGroup>
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { FormGroup } from 'reactstrap';
|
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import { DateIntervalSelector } from '../utils/dates/DateIntervalSelector';
|
import { DateIntervalSelector } from '../utils/dates/DateIntervalSelector';
|
||||||
|
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||||
import { Settings, VisitsSettings as VisitsSettingsConfig } from './reducers/settings';
|
import { Settings, VisitsSettings as VisitsSettingsConfig } from './reducers/settings';
|
||||||
|
|
||||||
interface VisitsProps {
|
interface VisitsProps {
|
||||||
@ -11,13 +11,12 @@ interface VisitsProps {
|
|||||||
|
|
||||||
export const VisitsSettings: FC<VisitsProps> = ({ settings, setVisitsSettings }) => (
|
export const VisitsSettings: FC<VisitsProps> = ({ settings, setVisitsSettings }) => (
|
||||||
<SimpleCard title="Visits" className="h-100">
|
<SimpleCard title="Visits" className="h-100">
|
||||||
<FormGroup className="mb-0">
|
<LabeledFormGroup noMargin label="Default interval to load on visits sections:">
|
||||||
<label>Default interval to load on visits sections:</label>
|
|
||||||
<DateIntervalSelector
|
<DateIntervalSelector
|
||||||
allText="All visits"
|
allText="All visits"
|
||||||
active={settings.visits?.defaultInterval ?? 'last30Days'}
|
active={settings.visits?.defaultInterval ?? 'last30Days'}
|
||||||
onChange={(defaultInterval) => setVisitsSettings({ defaultInterval })}
|
onChange={(defaultInterval) => setVisitsSettings({ defaultInterval })}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</LabeledFormGroup>
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
|
|||||||
<header className="mb-3">
|
<header className="mb-3">
|
||||||
<Card body>
|
<Card body>
|
||||||
<h2 className="d-sm-flex justify-content-between align-items-center mb-0">
|
<h2 className="d-sm-flex justify-content-between align-items-center mb-0">
|
||||||
<Button color="link" size="lg" className="p-0 mr-3" onClick={goBack}>
|
<Button color="link" size="lg" className="p-0 me-3" onClick={goBack}>
|
||||||
<FontAwesomeIcon icon={faArrowLeft} />
|
<FontAwesomeIcon icon={faArrowLeft} />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-center">
|
<span className="text-center">
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
@import '../utils/base';
|
@import '../utils/base';
|
||||||
|
|
||||||
.short-url-form .card-body > .form-group:last-child,
|
|
||||||
.short-url-form p:last-child {
|
.short-url-form p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { InputType } from 'reactstrap/lib/Input';
|
import { InputType } from 'reactstrap/types/lib/Input';
|
||||||
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
||||||
import { cond, isEmpty, pipe, replace, trim, T } from 'ramda';
|
import { cond, isEmpty, pipe, replace, trim, T } from 'ramda';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@ -86,15 +86,13 @@ export const ShortUrlForm = (
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
);
|
||||||
const renderDateInput = (id: DateFields, placeholder: string, props: Partial<DateInputProps> = {}) => (
|
const renderDateInput = (id: DateFields, placeholder: string, props: Partial<DateInputProps> = {}) => (
|
||||||
<div className="form-group">
|
<DateInput
|
||||||
<DateInput
|
selected={shortUrlData[id] ? toDate(shortUrlData[id] as string | Date) : null}
|
||||||
selected={shortUrlData[id] ? toDate(shortUrlData[id] as string | Date) : null}
|
placeholderText={placeholder}
|
||||||
placeholderText={placeholder}
|
isClearable
|
||||||
isClearable
|
onChange={(date) => setShortUrlData({ ...shortUrlData, [id]: date })}
|
||||||
onChange={(date) => setShortUrlData({ ...shortUrlData, [id]: date })}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
const basicComponents = (
|
const basicComponents = (
|
||||||
<>
|
<>
|
||||||
@ -110,9 +108,9 @@ export const ShortUrlForm = (
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
<Row>
|
<Row>
|
||||||
{isBasicMode && renderOptionalInput('customSlug', 'Custom slug', 'text', { bsSize: 'lg' }, { className: 'col-lg-6' })}
|
{isBasicMode && renderOptionalInput('customSlug', 'Custom slug', 'text', { bsSize: 'lg' }, { className: 'col-lg-6' })}
|
||||||
<FormGroup className={isBasicMode ? 'col-lg-6' : 'col-12 mb-0'}>
|
<div className={isBasicMode ? 'col-lg-6' : 'col-12 mb-0'}>
|
||||||
<TagsSelector selectedTags={shortUrlData.tags ?? []} onChange={changeTags} />
|
<TagsSelector selectedTags={shortUrlData.tags ?? []} onChange={changeTags} />
|
||||||
</FormGroup>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -154,12 +152,10 @@ export const ShortUrlForm = (
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
<FormGroup>
|
<DomainSelector
|
||||||
<DomainSelector
|
value={shortUrlData.domain}
|
||||||
value={shortUrlData.domain}
|
onChange={(domain?: string) => setShortUrlData({ ...shortUrlData, domain })}
|
||||||
onChange={(domain?: string) => setShortUrlData({ ...shortUrlData, domain })}
|
/>
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
@ -169,7 +165,9 @@ export const ShortUrlForm = (
|
|||||||
<div className={limitAccessCardClasses}>
|
<div className={limitAccessCardClasses}>
|
||||||
<SimpleCard title="Limit access to the short URL">
|
<SimpleCard title="Limit access to the short URL">
|
||||||
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
|
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
|
||||||
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlData.validUntil ? toDate(shortUrlData.validUntil) : undefined })}
|
<div className="mb-3">
|
||||||
|
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlData.validUntil ? toDate(shortUrlData.validUntil) : undefined })}
|
||||||
|
</div>
|
||||||
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlData.validSince ? toDate(shortUrlData.validSince) : undefined })}
|
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlData.validSince ? toDate(shortUrlData.validSince) : undefined })}
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
</div>
|
</div>
|
||||||
@ -189,7 +187,7 @@ export const ShortUrlForm = (
|
|||||||
<p>
|
<p>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
inline
|
inline
|
||||||
className="mr-2"
|
className="me-2"
|
||||||
checked={shortUrlData.findIfExists}
|
checked={shortUrlData.findIfExists}
|
||||||
onChange={(findIfExists) => setShortUrlData({ ...shortUrlData, findIfExists })}
|
onChange={(findIfExists) => setShortUrlData({ ...shortUrlData, findIfExists })}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -67,7 +67,7 @@ const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => ({ selectedSer
|
|||||||
{selectedTags.length > 0 && (
|
{selectedTags.length > 0 && (
|
||||||
<h4 className="mt-3">
|
<h4 className="mt-3">
|
||||||
{canChangeTagsMode && selectedTags.length > 1 && (
|
{canChangeTagsMode && selectedTags.length > 1 && (
|
||||||
<div className="float-right ml-2 mt-1">
|
<div className="float-end ms-2 mt-1">
|
||||||
<TooltipToggleSwitch
|
<TooltipToggleSwitch
|
||||||
checked={tagsMode === 'all'}
|
checked={tagsMode === 'all'}
|
||||||
tooltip={{ placement: 'left' }}
|
tooltip={{ placement: 'left' }}
|
||||||
@ -77,7 +77,7 @@ const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => ({ selectedSer
|
|||||||
</TooltipToggleSwitch>
|
</TooltipToggleSwitch>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<FontAwesomeIcon icon={tagsIcon} className="short-urls-filtering-bar__tags-icon mr-1" />
|
<FontAwesomeIcon icon={tagsIcon} className="short-urls-filtering-bar__tags-icon me-1" />
|
||||||
{selectedTags.map((tag) =>
|
{selectedTags.map((tag) =>
|
||||||
<Tag colorGenerator={colorGenerator} key={tag} text={tag} clearable onClose={() => removeTag(tag)} />)}
|
<Tag colorGenerator={colorGenerator} key={tag} text={tag} clearable onClose={() => removeTag(tag)} />)}
|
||||||
</h4>
|
</h4>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export const ShortUrlsTable = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
|
|||||||
const { error, loading, shortUrls } = shortUrlsList;
|
const { error, loading, shortUrls } = shortUrlsList;
|
||||||
const actionableFieldClasses = classNames({ 'short-urls-table__header-cell--with-action': !!orderByColumn });
|
const actionableFieldClasses = classNames({ 'short-urls-table__header-cell--with-action': !!orderByColumn });
|
||||||
const orderableColumnsClasses = classNames('short-urls-table__header-cell', actionableFieldClasses);
|
const orderableColumnsClasses = classNames('short-urls-table__header-cell', actionableFieldClasses);
|
||||||
const tableClasses = classNames('table table-hover', className);
|
const tableClasses = classNames('table table-hover responsive-table', className);
|
||||||
const supportsTitle = supportsShortUrlTitle(selectedServer);
|
const supportsTitle = supportsShortUrlTitle(selectedServer);
|
||||||
|
|
||||||
const renderShortUrls = () => {
|
const renderShortUrls = () => {
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Result type="error" className="mt-3">
|
<Result type="error" className="mt-3">
|
||||||
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-right pointer" onClick={resetCreateShortUrl} />}
|
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-end pointer" onClick={resetCreateShortUrl} />}
|
||||||
<ShlinkApiError errorData={errorData} fallbackMessage="An error occurred while creating the URL :(" />
|
<ShlinkApiError errorData={errorData} fallbackMessage="An error occurred while creating the URL :(" />
|
||||||
</Result>
|
</Result>
|
||||||
);
|
);
|
||||||
@ -42,7 +42,7 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Result type="success" className="mt-3">
|
<Result type="success" className="mt-3">
|
||||||
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-right pointer" onClick={resetCreateShortUrl} />}
|
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-end pointer" onClick={resetCreateShortUrl} />}
|
||||||
<b>Great!</b> The short URL is <b>{shortUrl}</b>
|
<b>Great!</b> The short URL is <b>{shortUrl}</b>
|
||||||
|
|
||||||
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
|
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { Modal, FormGroup, ModalBody, ModalHeader, Row, Button } from 'reactstra
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faFileDownload as downloadIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faFileDownload as downloadIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { ShortUrlModalProps } from '../data';
|
import { ShortUrlModalProps } from '../data';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
||||||
@ -56,10 +55,8 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Row>
|
<Row>
|
||||||
<FormGroup
|
<FormGroup className={`d-grid ${willRenderThreeControls ? 'col-md-4' : 'col-md-6'}`}>
|
||||||
className={classNames({ 'col-md-4': willRenderThreeControls, 'col-md-6': !willRenderThreeControls })}
|
<label>Size: {size}px</label>
|
||||||
>
|
|
||||||
<label className="mb-0">Size: {size}px</label>
|
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
className="form-control-range"
|
className="form-control-range"
|
||||||
@ -71,8 +68,8 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
{capabilities.marginIsSupported && (
|
{capabilities.marginIsSupported && (
|
||||||
<FormGroup className={willRenderThreeControls ? 'col-md-4' : 'col-md-6'}>
|
<FormGroup className={`d-grid ${willRenderThreeControls ? 'col-md-4' : 'col-md-6'}`}>
|
||||||
<label className="mb-0">Margin: {margin}px</label>
|
<label>Margin: {margin}px</label>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
className="form-control-range"
|
className="form-control-range"
|
||||||
@ -106,7 +103,7 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
|
|||||||
color="primary"
|
color="primary"
|
||||||
onClick={async () => imageDownloader.saveImage(qrCodeUrl, `${shortCode}-qr-code.${format}`)}
|
onClick={async () => imageDownloader.saveImage(qrCodeUrl, `${shortCode}-qr-code.${format}`)}
|
||||||
>
|
>
|
||||||
Download <FontAwesomeIcon icon={downloadIcon} className="ml-1" />
|
Download <FontAwesomeIcon icon={downloadIcon} className="ms-1" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ForServerVersion>
|
</ForServerVersion>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export const ShortUrlFormCheckboxGroup: FC<ShortUrlFormCheckboxGroupProps> = (
|
|||||||
{ children, infoTooltip, checked, onChange },
|
{ children, infoTooltip, checked, onChange },
|
||||||
) => (
|
) => (
|
||||||
<p>
|
<p>
|
||||||
<Checkbox inline checked={checked} className={infoTooltip ? 'mr-2' : ''} onChange={onChange}>
|
<Checkbox inline checked={checked} className={infoTooltip ? 'me-2' : ''} onChange={onChange}>
|
||||||
{children}
|
{children}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
{infoTooltip && <InfoTooltip placement="right">{infoTooltip}</InfoTooltip>}
|
{infoTooltip && <InfoTooltip placement="right">{infoTooltip}</InfoTooltip>}
|
||||||
|
|||||||
@ -59,7 +59,7 @@ const ShortUrlsRow = (
|
|||||||
<span className="indivisible short-urls-row__cell--relative">
|
<span className="indivisible short-urls-row__cell--relative">
|
||||||
<ExternalLink href={shortUrl.shortUrl} />
|
<ExternalLink href={shortUrl.shortUrl} />
|
||||||
<CopyToClipboardIcon text={shortUrl.shortUrl} onCopy={setCopiedToClipboard} />
|
<CopyToClipboardIcon text={shortUrl.shortUrl} onCopy={setCopiedToClipboard} />
|
||||||
<span className="badge badge-warning short-urls-row__copy-hint" hidden={!copiedToClipboard}>
|
<span className="badge bg-warning text-black short-urls-row__copy-hint" hidden={!copiedToClipboard}>
|
||||||
Copied short URL!
|
Copied short URL!
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -73,7 +73,7 @@ const ShortUrlsRow = (
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td className="responsive-table__cell short-urls-row__cell" data-th="Tags">{renderTags(shortUrl.tags)}</td>
|
<td className="responsive-table__cell short-urls-row__cell" data-th="Tags">{renderTags(shortUrl.tags)}</td>
|
||||||
<td className="responsive-table__cell short-urls-row__cell text-lg-right" data-th="Visits">
|
<td className="responsive-table__cell short-urls-row__cell text-lg-end" data-th="Visits">
|
||||||
<ShortUrlVisitsCount
|
<ShortUrlVisitsCount
|
||||||
visitsCount={shortUrl.visitsCount}
|
visitsCount={shortUrl.visitsCount}
|
||||||
shortUrl={shortUrl}
|
shortUrl={shortUrl}
|
||||||
|
|||||||
@ -64,14 +64,14 @@ const TagCard = (
|
|||||||
to={`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag.tag)}`}
|
to={`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag.tag)}`}
|
||||||
className="btn btn-outline-secondary btn-block d-flex justify-content-between align-items-center mb-1"
|
className="btn btn-outline-secondary btn-block d-flex justify-content-between align-items-center mb-1"
|
||||||
>
|
>
|
||||||
<span className="text-ellipsis"><FontAwesomeIcon icon={faLink} className="mr-2" />Short URLs</span>
|
<span className="text-ellipsis"><FontAwesomeIcon icon={faLink} className="me-2" />Short URLs</span>
|
||||||
<b>{prettify(tag.shortUrls)}</b>
|
<b>{prettify(tag.shortUrls)}</b>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to={`/server/${serverId}/tag/${tag.tag}/visits`}
|
to={`/server/${serverId}/tag/${tag.tag}/visits`}
|
||||||
className="btn btn-outline-secondary btn-block d-flex justify-content-between align-items-center"
|
className="btn btn-outline-secondary btn-block d-flex justify-content-between align-items-center"
|
||||||
>
|
>
|
||||||
<span className="text-ellipsis"><FontAwesomeIcon icon={faEye} className="mr-2" />Visits</span>
|
<span className="text-ellipsis"><FontAwesomeIcon icon={faEye} className="me-2" />Visits</span>
|
||||||
<b>{prettify(tag.visits)}</b>
|
<b>{prettify(tag.visits)}</b>
|
||||||
</Link>
|
</Link>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@ -14,10 +14,10 @@ interface TagsModeDropdownProps {
|
|||||||
export const TagsModeDropdown: FC<TagsModeDropdownProps> = ({ mode, onChange, renderTitle }) => (
|
export const TagsModeDropdown: FC<TagsModeDropdownProps> = ({ mode, onChange, renderTitle }) => (
|
||||||
<DropdownBtn text={renderTitle?.(mode) ?? `Display mode: ${mode}`}>
|
<DropdownBtn text={renderTitle?.(mode) ?? `Display mode: ${mode}`}>
|
||||||
<DropdownItem active={mode === 'cards'} onClick={() => onChange('cards')}>
|
<DropdownItem active={mode === 'cards'} onClick={() => onChange('cards')}>
|
||||||
<FontAwesomeIcon icon={cardsIcon} fixedWidth className="mr-1" /> Cards
|
<FontAwesomeIcon icon={cardsIcon} fixedWidth className="me-1" /> Cards
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem active={mode === 'list'} onClick={() => onChange('list')}>
|
<DropdownItem active={mode === 'list'} onClick={() => onChange('list')}>
|
||||||
<FontAwesomeIcon icon={listIcon} fixedWidth className="mr-1" /> List
|
<FontAwesomeIcon icon={listIcon} fixedWidth className="me-1" /> List
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownBtn>
|
</DropdownBtn>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -38,16 +38,16 @@ export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleCard key={page} bodyClassName={showPaginator ? 'pb-1' : ''}>
|
<SimpleCard key={page} bodyClassName={showPaginator ? 'pb-1' : ''}>
|
||||||
<table className="table table-hover mb-0">
|
<table className="table table-hover responsive-table mb-0">
|
||||||
<thead className="responsive-table__header">
|
<thead className="responsive-table__header">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="tags-table__header-cell" onClick={orderByColumn('tag')}>
|
<th className="tags-table__header-cell" onClick={orderByColumn('tag')}>
|
||||||
Tag <TableOrderIcon currentOrder={currentOrder} field="tag" />
|
Tag <TableOrderIcon currentOrder={currentOrder} field="tag" />
|
||||||
</th>
|
</th>
|
||||||
<th className="tags-table__header-cell text-lg-right" onClick={orderByColumn('shortUrls')}>
|
<th className="tags-table__header-cell text-lg-end" onClick={orderByColumn('shortUrls')}>
|
||||||
Short URLs <TableOrderIcon currentOrder={currentOrder} field="shortUrls" />
|
Short URLs <TableOrderIcon currentOrder={currentOrder} field="shortUrls" />
|
||||||
</th>
|
</th>
|
||||||
<th className="tags-table__header-cell text-lg-right" onClick={orderByColumn('visits')}>
|
<th className="tags-table__header-cell text-lg-end" onClick={orderByColumn('visits')}>
|
||||||
Visits <TableOrderIcon currentOrder={currentOrder} field="visits" />
|
Visits <TableOrderIcon currentOrder={currentOrder} field="visits" />
|
||||||
</th>
|
</th>
|
||||||
<th className="tags-table__header-cell" />
|
<th className="tags-table__header-cell" />
|
||||||
|
|||||||
@ -31,23 +31,23 @@ export const TagsTableRow = (
|
|||||||
<th className="responsive-table__cell" data-th="Tag">
|
<th className="responsive-table__cell" data-th="Tag">
|
||||||
<TagBullet tag={tag.tag} colorGenerator={colorGenerator} /> {tag.tag}
|
<TagBullet tag={tag.tag} colorGenerator={colorGenerator} /> {tag.tag}
|
||||||
</th>
|
</th>
|
||||||
<td className="responsive-table__cell text-lg-right" data-th="Short URLs">
|
<td className="responsive-table__cell text-lg-end" data-th="Short URLs">
|
||||||
<Link to={`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag.tag)}`}>
|
<Link to={`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag.tag)}`}>
|
||||||
{prettify(tag.shortUrls)}
|
{prettify(tag.shortUrls)}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="responsive-table__cell text-lg-right" data-th="Visits">
|
<td className="responsive-table__cell text-lg-end" data-th="Visits">
|
||||||
<Link to={`/server/${serverId}/tag/${tag.tag}/visits`}>
|
<Link to={`/server/${serverId}/tag/${tag.tag}/visits`}>
|
||||||
{prettify(tag.visits)}
|
{prettify(tag.visits)}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="responsive-table__cell text-lg-right">
|
<td className="responsive-table__cell text-lg-end">
|
||||||
<DropdownBtnMenu toggle={toggleDropdown} isOpen={isDropdownOpen}>
|
<DropdownBtnMenu toggle={toggleDropdown} isOpen={isDropdownOpen}>
|
||||||
<DropdownItem onClick={toggleEdit}>
|
<DropdownItem onClick={toggleEdit}>
|
||||||
<FontAwesomeIcon icon={editIcon} fixedWidth className="mr-1" /> Edit
|
<FontAwesomeIcon icon={editIcon} fixedWidth className="me-1" /> Edit
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem onClick={toggleDelete}>
|
<DropdownItem onClick={toggleDelete}>
|
||||||
<FontAwesomeIcon icon={deleteIcon} fixedWidth className="mr-1" /> Delete
|
<FontAwesomeIcon icon={deleteIcon} fixedWidth className="me-1" /> Delete
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownBtnMenu>
|
</DropdownBtnMenu>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Input, Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap';
|
import { Button, Input, Modal, ModalBody, ModalFooter, ModalHeader, Popover, InputGroup } from 'reactstrap';
|
||||||
import { HexColorPicker } from 'react-colorful';
|
import { HexColorPicker } from 'react-colorful';
|
||||||
import { faPalette as colorIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faPalette as colorIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
@ -37,14 +37,14 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
|
|||||||
<form onSubmit={saveTag}>
|
<form onSubmit={saveTag}>
|
||||||
<ModalHeader toggle={toggle}>Edit tag</ModalHeader>
|
<ModalHeader toggle={toggle}>Edit tag</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="input-group">
|
<InputGroup>
|
||||||
<div className="input-group-prepend" id="colorPickerBtn" onClick={toggleColorPicker}>
|
<div
|
||||||
<div
|
id="colorPickerBtn"
|
||||||
className="input-group-text edit-tag-modal__color-picker-toggle"
|
className="input-group-text edit-tag-modal__color-picker-toggle"
|
||||||
style={{ backgroundColor: color, borderColor: color }}
|
style={{ backgroundColor: color, borderColor: color }}
|
||||||
>
|
onClick={toggleColorPicker}
|
||||||
<FontAwesomeIcon icon={colorIcon} className="edit-tag-modal__color-icon" />
|
>
|
||||||
</div>
|
<FontAwesomeIcon icon={colorIcon} className="edit-tag-modal__color-icon" />
|
||||||
</div>
|
</div>
|
||||||
<Popover
|
<Popover
|
||||||
isOpen={showColorPicker}
|
isOpen={showColorPicker}
|
||||||
@ -62,7 +62,7 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
|
|||||||
required
|
required
|
||||||
onChange={({ target }) => setNewTagName(target.value)}
|
onChange={({ target }) => setNewTagName(target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</InputGroup>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Result type="error" small className="mt-2">
|
<Result type="error" small className="mt-2">
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag--light-bg {
|
.tag--light-bg {
|
||||||
color: #000;
|
color: #222 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag:not(:last-child) {
|
.tag:not(:last-child) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
$lightPrimaryColor: #ffffff;
|
$lightPrimaryColor: #ffffff;
|
||||||
$lightPrimaryColorAlfa: rgba($lightPrimaryColor, .5);
|
$lightPrimaryColorAlfa: rgba($lightPrimaryColor, .5);
|
||||||
$lightSecondaryColor: $lightColor;
|
$lightSecondaryColor: $lightColor;
|
||||||
$lightTextColor: #212529;
|
$lightTextColor: #232323;
|
||||||
$lightBorderColor: rgba(0, 0, 0, .125);
|
$lightBorderColor: rgba(0, 0, 0, .125);
|
||||||
$lightTableBorderColor: $mediumGrey;
|
$lightTableBorderColor: $mediumGrey;
|
||||||
$lightActiveColor: $lightGrey;
|
$lightActiveColor: $lightGrey;
|
||||||
@ -44,6 +44,7 @@ html:not([data-theme='dark']) {
|
|||||||
--input-text-color: #{$lightInputTextColor};
|
--input-text-color: #{$lightInputTextColor};
|
||||||
--table-border-color: #{$lightTableBorderColor};
|
--table-border-color: #{$lightTableBorderColor};
|
||||||
--table-highlight-color: #{$lightTableHighlightColor};
|
--table-highlight-color: #{$lightTableHighlightColor};
|
||||||
|
--btn-close-filter: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='dark'] {
|
html[data-theme='dark'] {
|
||||||
@ -60,4 +61,5 @@ html[data-theme='dark'] {
|
|||||||
--input-text-color: #{$darkInputTextColor};
|
--input-text-color: #{$darkInputTextColor};
|
||||||
--table-border-color: #{$darkTableBorderColor};
|
--table-border-color: #{$darkTableBorderColor};
|
||||||
--table-highlight-color: #{$darkTableHighlightColor};
|
--table-highlight-color: #{$darkTableHighlightColor};
|
||||||
|
--btn-close-filter: invert(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,15 +20,15 @@ const BooleanControl: FC<BooleanControlWithTypeProps> = (
|
|||||||
const { current: id } = useRef(uuid());
|
const { current: id } = useRef(uuid());
|
||||||
const onChecked = (e: ChangeEvent<HTMLInputElement>) => onChange(e.target.checked, e);
|
const onChecked = (e: ChangeEvent<HTMLInputElement>) => onChange(e.target.checked, e);
|
||||||
const typeClasses = {
|
const typeClasses = {
|
||||||
'custom-switch': type === 'switch',
|
'form-switch': type === 'switch',
|
||||||
'custom-checkbox': type === 'checkbox',
|
'form-checkbox': type === 'checkbox',
|
||||||
};
|
};
|
||||||
const style = inline ? { display: 'inline-block' } : {};
|
const style = inline ? { display: 'inline-block' } : {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={classNames('custom-control', typeClasses, className)} style={style}>
|
<span className={classNames('form-check', typeClasses, className)} style={style}>
|
||||||
<input type="checkbox" className="custom-control-input" id={id} checked={checked} onChange={onChecked} />
|
<input type="checkbox" className="form-check-input" id={id} checked={checked} onChange={onChecked} />
|
||||||
<label className="custom-control-label" htmlFor={id}>{children}</label>
|
<label className="form-check-label" htmlFor={id}>{children}</label>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,6 +11,6 @@ interface CopyToClipboardIconProps {
|
|||||||
|
|
||||||
export const CopyToClipboardIcon: FC<CopyToClipboardIconProps> = ({ text, onCopy }) => (
|
export const CopyToClipboardIcon: FC<CopyToClipboardIconProps> = ({ text, onCopy }) => (
|
||||||
<CopyToClipboard text={text} onCopy={onCopy}>
|
<CopyToClipboard text={text} onCopy={onCopy}>
|
||||||
<FontAwesomeIcon icon={copyIcon} className="ml-2 copy-to-clipboard-icon" />
|
<FontAwesomeIcon icon={copyIcon} className="ms-2 copy-to-clipboard-icon" />
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
import { FC, useRef } from 'react';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { InputType } from 'reactstrap/lib/Input';
|
|
||||||
import { FormGroup } from 'reactstrap';
|
|
||||||
|
|
||||||
export interface FormGroupContainerProps {
|
|
||||||
value: string;
|
|
||||||
onChange: (newValue: string) => void;
|
|
||||||
id?: string;
|
|
||||||
type?: InputType;
|
|
||||||
required?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
className?: string;
|
|
||||||
labelClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FormGroupContainer: FC<FormGroupContainerProps> = (
|
|
||||||
{ children, value, onChange, id, type, required, placeholder, className, labelClassName },
|
|
||||||
) => {
|
|
||||||
const forId = useRef<string>(id ?? uuid());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup className={className ?? ''}>
|
|
||||||
<label htmlFor={forId.current} className={labelClassName ?? ''}>
|
|
||||||
{children}:
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="form-control"
|
|
||||||
type={type ?? 'text'}
|
|
||||||
id={forId.current}
|
|
||||||
value={value}
|
|
||||||
required={required ?? true}
|
|
||||||
placeholder={placeholder}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -42,7 +42,7 @@ const Message: FC<MessageProps> = ({ className, children, loading = false, type
|
|||||||
<Card className={getClassForType(type)} body>
|
<Card className={getClassForType(type)} body>
|
||||||
<h3 className={classNames('text-center mb-0', getTextClassForType(type))}>
|
<h3 className={classNames('text-center mb-0', getTextClassForType(type))}>
|
||||||
{loading && <FontAwesomeIcon icon={preloader} spin />}
|
{loading && <FontAwesomeIcon icon={preloader} spin />}
|
||||||
{loading && <span className="ml-2">{children ?? 'Loading...'}</span>}
|
{loading && <span className="ms-2">{children ?? 'Loading...'}</span>}
|
||||||
{!loading && children}
|
{!loading && children}
|
||||||
</h3>
|
</h3>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -6,13 +6,14 @@
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-pills__nav-link {
|
.nav-pills__nav-link.nav-pills__nav-link {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
padding-bottom: calc(.5rem - 3px) !important;
|
padding-bottom: calc(.5rem - 3px) !important;
|
||||||
border-bottom: 3px solid transparent;
|
border-bottom: 3px solid transparent !important;
|
||||||
color: #5d6778;
|
color: #5d6778;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
@media (min-width: $smMin) and (max-width: $mdMax) {
|
@media (min-width: $smMin) and (max-width: $mdMax) {
|
||||||
font-size: 89%;
|
font-size: 89%;
|
||||||
@ -24,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-pills__nav-link.active {
|
.nav-pills__nav-link.active {
|
||||||
border-color: $mainColor;
|
border-color: $mainColor !important;
|
||||||
background-color: var(--primary-color) !important;
|
background-color: var(--primary-color) !important;
|
||||||
color: $mainColor !important;
|
color: $mainColor !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,11 @@ import { Card, Nav, NavLink } from 'reactstrap';
|
|||||||
import { NavLink as RouterNavLink } from 'react-router-dom';
|
import { NavLink as RouterNavLink } from 'react-router-dom';
|
||||||
import './NavPills.scss';
|
import './NavPills.scss';
|
||||||
|
|
||||||
|
interface NavPillsProps {
|
||||||
|
fill?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface NavPillProps {
|
interface NavPillProps {
|
||||||
to: string;
|
to: string;
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
@ -14,8 +19,8 @@ export const NavPillItem: FC<NavPillProps> = ({ children, ...rest }) => (
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const NavPills: FC<{ fill?: boolean }> = ({ children, fill = false }) => (
|
export const NavPills: FC<NavPillsProps> = ({ children, fill = false, className = '' }) => (
|
||||||
<Card className="nav-pills__nav p-0 overflow-hidden mb-3" body>
|
<Card className={`nav-pills__nav p-0 overflow-hidden ${className}`} body>
|
||||||
<Nav pills fill={fill}>
|
<Nav pills fill={fill}>
|
||||||
{Children.map(children, (child) => {
|
{Children.map(children, (child) => {
|
||||||
if (!isValidElement(child) || child.type !== NavPillItem) {
|
if (!isValidElement(child) || child.type !== NavPillItem) {
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { CardProps } from 'reactstrap/lib/Card';
|
import { Card, CardBody, CardHeader, CardProps } from 'reactstrap';
|
||||||
import { Card, CardBody, CardHeader } from 'reactstrap';
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
interface SimpleCardProps extends Omit<CardProps, 'title'> {
|
interface SimpleCardProps extends Omit<CardProps, 'title'> {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { FC, useRef } from 'react';
|
import { FC, useRef } from 'react';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip, UncontrolledTooltipProps } from 'reactstrap';
|
||||||
import { UncontrolledTooltipProps } from 'reactstrap/lib/Tooltip';
|
|
||||||
import { BooleanControlProps } from './BooleanControl';
|
import { BooleanControlProps } from './BooleanControl';
|
||||||
import ToggleSwitch from './ToggleSwitch';
|
import ToggleSwitch from './ToggleSwitch';
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,4 @@ $footer-height: 2.3rem;
|
|||||||
$footer-margin: .8rem;
|
$footer-margin: .8rem;
|
||||||
|
|
||||||
// Bootstrap overwrites
|
// Bootstrap overwrites
|
||||||
$theme-colors: (
|
$primary: $mainColor;
|
||||||
'primary': $mainColor
|
|
||||||
);
|
|
||||||
|
|||||||
3
src/utils/forms/FormText.tsx
Normal file
3
src/utils/forms/FormText.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
export const FormText: FC = ({ children }) => <small className="form-text text-muted d-block">{children}</small>;
|
||||||
29
src/utils/forms/InputFormGroup.tsx
Normal file
29
src/utils/forms/InputFormGroup.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { InputType } from 'reactstrap/types/lib/Input';
|
||||||
|
import { LabeledFormGroup } from './LabeledFormGroup';
|
||||||
|
|
||||||
|
export interface InputFormGroupProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (newValue: string) => void;
|
||||||
|
id?: string;
|
||||||
|
type?: InputType;
|
||||||
|
required?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
labelClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputFormGroup: FC<InputFormGroupProps> = (
|
||||||
|
{ children, value, onChange, type, required, placeholder, className, labelClassName },
|
||||||
|
) => (
|
||||||
|
<LabeledFormGroup label={<>{children}:</>} className={className ?? ''} labelClassName={labelClassName}>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type={type ?? 'text'}
|
||||||
|
value={value}
|
||||||
|
required={required ?? true}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</LabeledFormGroup>
|
||||||
|
);
|
||||||
17
src/utils/forms/LabeledFormGroup.tsx
Normal file
17
src/utils/forms/LabeledFormGroup.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface LabeledFormGroupProps {
|
||||||
|
label: ReactNode;
|
||||||
|
noMargin?: boolean;
|
||||||
|
className?: string;
|
||||||
|
labelClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LabeledFormGroup: FC<LabeledFormGroupProps> = (
|
||||||
|
{ children, label, className = '', labelClassName = '', noMargin = false },
|
||||||
|
) => (
|
||||||
|
<div className={`${className} ${noMargin ? '' : 'mb-3'}`}>
|
||||||
|
<label className={`form-label ${labelClassName}`}>{label}</label>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
@mixin sticky-cell($with-separators: true) {
|
@mixin sticky-cell($with-separators: true) {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
border: none !important;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
|||||||
@ -6,11 +6,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.responsive-table.table > :not(:first-child) {
|
||||||
|
@media (max-width: $responsiveTableBreakpoint) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.responsive-table__row {
|
.responsive-table__row {
|
||||||
@media (max-width: $responsiveTableBreakpoint) {
|
@media (max-width: $responsiveTableBreakpoint) {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
border-top: 2px solid var(--border-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,3 +47,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.responsive-table__cell.responsive-table__cell .btn-sm {
|
||||||
|
@media (max-width: $responsiveTableBreakpoint) {
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
margin-top: 0.16rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ interface TableOrderIconProps<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TableOrderIcon<T extends string = string>(
|
export function TableOrderIcon<T extends string = string>(
|
||||||
{ currentOrder, field, className = 'ml-1' }: TableOrderIconProps<T>,
|
{ currentOrder, field, className = 'ms-1' }: TableOrderIconProps<T>,
|
||||||
) {
|
) {
|
||||||
if (!currentOrder.dir || currentOrder.field !== field) {
|
if (!currentOrder.dir || currentOrder.field !== field) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -12,14 +12,8 @@ export const PRIMARY_DARK_COLOR = '#161b22';
|
|||||||
|
|
||||||
export type Theme = 'dark' | 'light';
|
export type Theme = 'dark' | 'light';
|
||||||
|
|
||||||
export const changeThemeInMarkup = (theme: Theme) => {
|
export const changeThemeInMarkup = (theme: Theme) =>
|
||||||
const html = document.getElementsByTagName('html');
|
document.getElementsByTagName('html')?.[0]?.setAttribute('data-theme', theme);
|
||||||
|
|
||||||
html?.[0]?.setAttribute('data-theme', theme);
|
export const isDarkThemeEnabled = (): boolean =>
|
||||||
};
|
document.getElementsByTagName('html')?.[0]?.getAttribute('data-theme') === 'dark';
|
||||||
|
|
||||||
export const isDarkThemeEnabled = (): boolean => {
|
|
||||||
const html = document.getElementsByTagName('html');
|
|
||||||
|
|
||||||
return html?.[0]?.getAttribute('data-theme') === 'dark';
|
|
||||||
};
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderP
|
|||||||
|
|
||||||
const visitsStatsTitle = (
|
const visitsStatsTitle = (
|
||||||
<span className="d-flex align-items-center justify-content-center">
|
<span className="d-flex align-items-center justify-content-center">
|
||||||
<span className="mr-2">Visits for</span>
|
<span className="me-2">Visits for</span>
|
||||||
<Tag text={tag} colorGenerator={colorGenerator} />
|
<Tag text={tag} colorGenerator={colorGenerator} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,13 +17,13 @@ const VisitsHeader: FC<VisitsHeaderProps> = ({ visits, goBack, shortUrl, childre
|
|||||||
<header>
|
<header>
|
||||||
<Card body>
|
<Card body>
|
||||||
<h2 className="d-flex justify-content-between align-items-center mb-0">
|
<h2 className="d-flex justify-content-between align-items-center mb-0">
|
||||||
<Button color="link" size="lg" className="p-0 mr-3" onClick={goBack}>
|
<Button color="link" size="lg" className="p-0 me-3" onClick={goBack}>
|
||||||
<FontAwesomeIcon icon={faArrowLeft} />
|
<FontAwesomeIcon icon={faArrowLeft} />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-center d-none d-sm-block">
|
<span className="text-center d-none d-sm-block">
|
||||||
<small>{title}</small>
|
<small>{title}</small>
|
||||||
</span>
|
</span>
|
||||||
<span className="badge badge-main ml-3">
|
<span className="badge badge-main ms-3">
|
||||||
Visits:{' '}
|
Visits:{' '}
|
||||||
<ShortUrlVisitsCount visitsCount={visits.length} shortUrl={shortUrl} />
|
<ShortUrlVisitsCount visitsCount={visits.length} shortUrl={shortUrl} />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -147,7 +147,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
|||||||
{Object.values(sections).map(({ title, icon, subPath }, index) => (
|
{Object.values(sections).map(({ title, icon, subPath }, index) => (
|
||||||
<NavPillItem key={index} to={buildSectionUrl(subPath)} replace>
|
<NavPillItem key={index} to={buildSectionUrl(subPath)} replace>
|
||||||
<FontAwesomeIcon icon={icon} />
|
<FontAwesomeIcon icon={icon} />
|
||||||
<span className="ml-2 d-none d-sm-inline">{title}</span>
|
<span className="ms-2 d-none d-sm-inline">{title}</span>
|
||||||
</NavPillItem>
|
</NavPillItem>
|
||||||
))}
|
))}
|
||||||
</NavPills>
|
</NavPills>
|
||||||
@ -289,7 +289,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<VisitsFilterDropdown
|
<VisitsFilterDropdown
|
||||||
className="ml-0 ml-md-2 mt-3 mt-md-0"
|
className="ms-0 ms-md-2 mt-3 mt-md-0"
|
||||||
isOrphanVisits={isOrphanVisits}
|
isOrphanVisits={isOrphanVisits}
|
||||||
botsSupported={botsSupported}
|
botsSupported={botsSupported}
|
||||||
selected={visitsFilter}
|
selected={visitsFilter}
|
||||||
@ -303,7 +303,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
disabled={highlightedVisits.length === 0}
|
disabled={highlightedVisits.length === 0}
|
||||||
className="btn-md-block mr-2"
|
className="btn-md-block me-2"
|
||||||
onClick={() => setSelectedVisits([])}
|
onClick={() => setSelectedVisits([])}
|
||||||
>
|
>
|
||||||
Clear selection {highlightedVisits.length > 0 && <>({prettify(highlightedVisits.length)})</>}
|
Clear selection {highlightedVisits.length > 0 && <>({prettify(highlightedVisits.length)})</>}
|
||||||
|
|||||||
@ -87,137 +87,139 @@ const VisitsTable = ({
|
|||||||
}, [ searchTerm ]);
|
}, [ searchTerm ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="table table-bordered table-hover table-sm table-responsive-sm visits-table">
|
<div className="table-responsive-md">
|
||||||
<thead className="visits-table__header">
|
<table className="table table-bordered table-hover table-sm visits-table">
|
||||||
<tr>
|
<thead className="visits-table__header">
|
||||||
<th
|
|
||||||
className={`${headerCellsClass} text-center`}
|
|
||||||
onClick={() => setSelectedVisits(
|
|
||||||
selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : [],
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
|
|
||||||
</th>
|
|
||||||
{supportsBots && (
|
|
||||||
<th className={`${headerCellsClass} text-center`} onClick={orderByColumn('potentialBot')}>
|
|
||||||
<FontAwesomeIcon icon={botIcon} />
|
|
||||||
{renderOrderIcon('potentialBot')}
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
<th className={headerCellsClass} onClick={orderByColumn('date')}>
|
|
||||||
Date
|
|
||||||
{renderOrderIcon('date')}
|
|
||||||
</th>
|
|
||||||
<th className={headerCellsClass} onClick={orderByColumn('country')}>
|
|
||||||
Country
|
|
||||||
{renderOrderIcon('country')}
|
|
||||||
</th>
|
|
||||||
<th className={headerCellsClass} onClick={orderByColumn('city')}>
|
|
||||||
City
|
|
||||||
{renderOrderIcon('city')}
|
|
||||||
</th>
|
|
||||||
<th className={headerCellsClass} onClick={orderByColumn('browser')}>
|
|
||||||
Browser
|
|
||||||
{renderOrderIcon('browser')}
|
|
||||||
</th>
|
|
||||||
<th className={headerCellsClass} onClick={orderByColumn('os')}>
|
|
||||||
OS
|
|
||||||
{renderOrderIcon('os')}
|
|
||||||
</th>
|
|
||||||
<th className={headerCellsClass} onClick={orderByColumn('referer')}>
|
|
||||||
Referrer
|
|
||||||
{renderOrderIcon('referer')}
|
|
||||||
</th>
|
|
||||||
{isOrphanVisits && (
|
|
||||||
<th className={headerCellsClass} onClick={orderByColumn('visitedUrl')}>
|
|
||||||
Visited URL
|
|
||||||
{renderOrderIcon('visitedUrl')}
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colSpan={fullSizeColSpan} className="p-0">
|
|
||||||
<SearchField noBorder large={false} onChange={setSearchTerm} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{!resultSet.visitsGroups[page - 1]?.length && (
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={fullSizeColSpan} className="text-center">
|
<th
|
||||||
No visits found with current filtering
|
className={`${headerCellsClass} text-center`}
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{resultSet.visitsGroups[page - 1]?.map((visit, index) => {
|
|
||||||
const isSelected = selectedVisits.includes(visit);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={index}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
className={classNames({ 'table-active': isSelected })}
|
|
||||||
onClick={() => setSelectedVisits(
|
onClick={() => setSelectedVisits(
|
||||||
isSelected ? selectedVisits.filter((v) => v !== visit) : [ ...selectedVisits, visit ],
|
selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : [],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<td className="text-center">
|
<FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
|
||||||
{isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
|
</th>
|
||||||
</td>
|
{supportsBots && (
|
||||||
{supportsBots && (
|
<th className={`${headerCellsClass} text-center`} onClick={orderByColumn('potentialBot')}>
|
||||||
<td className="text-center">
|
<FontAwesomeIcon icon={botIcon} />
|
||||||
{visit.potentialBot && (
|
{renderOrderIcon('potentialBot')}
|
||||||
<>
|
</th>
|
||||||
<FontAwesomeIcon icon={botIcon} id={`botIcon${index}`} />
|
)}
|
||||||
<UncontrolledTooltip placement="right" target={`botIcon${index}`}>
|
<th className={headerCellsClass} onClick={orderByColumn('date')}>
|
||||||
Potentially a visit from a bot or crawler
|
Date
|
||||||
</UncontrolledTooltip>
|
{renderOrderIcon('date')}
|
||||||
</>
|
</th>
|
||||||
)}
|
<th className={headerCellsClass} onClick={orderByColumn('country')}>
|
||||||
</td>
|
Country
|
||||||
)}
|
{renderOrderIcon('country')}
|
||||||
<td><Time date={visit.date} /></td>
|
</th>
|
||||||
<td>{visit.country}</td>
|
<th className={headerCellsClass} onClick={orderByColumn('city')}>
|
||||||
<td>{visit.city}</td>
|
City
|
||||||
<td>{visit.browser}</td>
|
{renderOrderIcon('city')}
|
||||||
<td>{visit.os}</td>
|
</th>
|
||||||
<td>{visit.referer}</td>
|
<th className={headerCellsClass} onClick={orderByColumn('browser')}>
|
||||||
{isOrphanVisits && <td>{(visit as NormalizedOrphanVisit).visitedUrl}</td>}
|
Browser
|
||||||
</tr>
|
{renderOrderIcon('browser')}
|
||||||
);
|
</th>
|
||||||
})}
|
<th className={headerCellsClass} onClick={orderByColumn('os')}>
|
||||||
</tbody>
|
OS
|
||||||
{resultSet.total > PAGE_SIZE && (
|
{renderOrderIcon('os')}
|
||||||
<tfoot>
|
</th>
|
||||||
|
<th className={headerCellsClass} onClick={orderByColumn('referer')}>
|
||||||
|
Referrer
|
||||||
|
{renderOrderIcon('referer')}
|
||||||
|
</th>
|
||||||
|
{isOrphanVisits && (
|
||||||
|
<th className={headerCellsClass} onClick={orderByColumn('visitedUrl')}>
|
||||||
|
Visited URL
|
||||||
|
{renderOrderIcon('visitedUrl')}
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={fullSizeColSpan} className="visits-table__footer-cell visits-table__sticky">
|
<td colSpan={fullSizeColSpan} className="p-0">
|
||||||
<div className="row">
|
<SearchField noBorder large={false} onChange={setSearchTerm} />
|
||||||
<div className="col-md-6">
|
|
||||||
<SimplePaginator
|
|
||||||
pagesCount={Math.ceil(resultSet.total / PAGE_SIZE)}
|
|
||||||
currentPage={page}
|
|
||||||
setCurrentPage={setPage}
|
|
||||||
centered={isMobileDevice}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={classNames('col-md-6', {
|
|
||||||
'd-flex align-items-center flex-row-reverse': !isMobileDevice,
|
|
||||||
'text-center mt-3': isMobileDevice,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
Visits <b>{prettify(start + 1)}</b> to{' '}
|
|
||||||
<b>{prettify(min(end, resultSet.total))}</b> of{' '}
|
|
||||||
<b>{prettify(resultSet.total)}</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</thead>
|
||||||
)}
|
<tbody>
|
||||||
</table>
|
{!resultSet.visitsGroups[page - 1]?.length && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={fullSizeColSpan} className="text-center">
|
||||||
|
No visits found with current filtering
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{resultSet.visitsGroups[page - 1]?.map((visit, index) => {
|
||||||
|
const isSelected = selectedVisits.includes(visit);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={index}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
className={classNames({ 'table-active': isSelected })}
|
||||||
|
onClick={() => setSelectedVisits(
|
||||||
|
isSelected ? selectedVisits.filter((v) => v !== visit) : [ ...selectedVisits, visit ],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<td className="text-center">
|
||||||
|
{isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
|
||||||
|
</td>
|
||||||
|
{supportsBots && (
|
||||||
|
<td className="text-center">
|
||||||
|
{visit.potentialBot && (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={botIcon} id={`botIcon${index}`} />
|
||||||
|
<UncontrolledTooltip placement="right" target={`botIcon${index}`}>
|
||||||
|
Potentially a visit from a bot or crawler
|
||||||
|
</UncontrolledTooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td><Time date={visit.date} /></td>
|
||||||
|
<td>{visit.country}</td>
|
||||||
|
<td>{visit.city}</td>
|
||||||
|
<td>{visit.browser}</td>
|
||||||
|
<td>{visit.os}</td>
|
||||||
|
<td>{visit.referer}</td>
|
||||||
|
{isOrphanVisits && <td>{(visit as NormalizedOrphanVisit).visitedUrl}</td>}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
{resultSet.total > PAGE_SIZE && (
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={fullSizeColSpan} className="visits-table__footer-cell visits-table__sticky">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<SimplePaginator
|
||||||
|
pagesCount={Math.ceil(resultSet.total / PAGE_SIZE)}
|
||||||
|
currentPage={page}
|
||||||
|
setCurrentPage={setPage}
|
||||||
|
centered={isMobileDevice}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames('col-md-6', {
|
||||||
|
'd-flex align-items-center flex-row-reverse': !isMobileDevice,
|
||||||
|
'text-center mt-3': isMobileDevice,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Visits <b>{prettify(start + 1)}</b> to{' '}
|
||||||
|
<b>{prettify(min(end, resultSet.total))}</b> of{' '}
|
||||||
|
<b>{prettify(resultSet.total)}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
)}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -232,7 +232,7 @@ const LineChartCard = (
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
{title}
|
{title}
|
||||||
<div className="float-right">
|
<div className="float-end">
|
||||||
<UncontrolledDropdown>
|
<UncontrolledDropdown>
|
||||||
<DropdownToggle caret color="link" className="btn-sm p-0">
|
<DropdownToggle caret color="link" className="btn-sm p-0">
|
||||||
Group by
|
Group by
|
||||||
@ -246,7 +246,7 @@ const LineChartCard = (
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</UncontrolledDropdown>
|
</UncontrolledDropdown>
|
||||||
</div>
|
</div>
|
||||||
<div className="float-right mr-2">
|
<div className="float-end me-2">
|
||||||
<ToggleSwitch checked={skipNoVisits} onChange={toggleSkipNoVisits}>
|
<ToggleSwitch checked={skipNoVisits} onChange={toggleSkipNoVisits}>
|
||||||
<small>Skip dates with no visits</small>
|
<small>Skip dates with no visits</small>
|
||||||
</ToggleSwitch>
|
</ToggleSwitch>
|
||||||
|
|||||||
@ -95,7 +95,7 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
|
|||||||
const computeTitle = () => (
|
const computeTitle = () => (
|
||||||
<>
|
<>
|
||||||
{title}
|
{title}
|
||||||
<div className="float-right">
|
<div className="float-end">
|
||||||
<OrderingDropdown
|
<OrderingDropdown
|
||||||
isButton={false}
|
isButton={false}
|
||||||
right
|
right
|
||||||
@ -108,9 +108,9 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{withPagination && Object.keys(stats).length > 50 && (
|
{withPagination && Object.keys(stats).length > 50 && (
|
||||||
<div className="float-right">
|
<div className="float-end">
|
||||||
<PaginationDropdown
|
<PaginationDropdown
|
||||||
toggleClassName="btn-sm p-0 mr-3"
|
toggleClassName="btn-sm p-0 me-3"
|
||||||
ranges={[ 50, 100, 200, 500 ]}
|
ranges={[ 50, 100, 200, 500 ]}
|
||||||
value={itemsPerPage}
|
value={itemsPerPage}
|
||||||
setValue={(itemsPerPage) => {
|
setValue={(itemsPerPage) => {
|
||||||
@ -121,7 +121,7 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{extraHeaderContent && (
|
{extraHeaderContent && (
|
||||||
<div className="float-right">
|
<div className="float-end">
|
||||||
{extraHeaderContent(pagination ? activeCities : undefined)}
|
{extraHeaderContent(pagination ? activeCities : undefined)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const MapModal = ({ toggle, isOpen, title, locations = [] }: MapModalProps) => (
|
|||||||
<ModalBody className="map-modal__modal-body">
|
<ModalBody className="map-modal__modal-body">
|
||||||
<h3 className="map-modal__modal-title">
|
<h3 className="map-modal__modal-title">
|
||||||
{title}
|
{title}
|
||||||
<button type="button" className="close" onClick={toggle}>×</button>
|
<button type="button" className="btn-close float-end" onClick={toggle} />
|
||||||
</h3>
|
</h3>
|
||||||
<MapContainer {...calculateMapProps(locations)}>
|
<MapContainer {...calculateMapProps(locations)}>
|
||||||
<OpenStreetMapTile />
|
<OpenStreetMapTile />
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DropdownItem, DropdownItemProps } from 'reactstrap'; // eslint-disable-line import/named
|
import { DropdownItem, DropdownItemProps } from 'reactstrap';
|
||||||
import { OrphanVisitType, VisitsFilter } from '../types';
|
import { OrphanVisitType, VisitsFilter } from '../types';
|
||||||
import { DropdownBtn } from '../../utils/DropdownBtn';
|
import { DropdownBtn } from '../../utils/DropdownBtn';
|
||||||
import { hasValue } from '../../utils/utils';
|
import { hasValue } from '../../utils/utils';
|
||||||
@ -26,7 +26,7 @@ export const VisitsFilterDropdown = (
|
|||||||
const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots });
|
const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownBtn text="Filters" dropdownClassName={className} className="mr-3" right minWidth={250}>
|
<DropdownBtn text="Filters" dropdownClassName={className} className="me-3" right minWidth={250}>
|
||||||
{botsSupported && (
|
{botsSupported && (
|
||||||
<>
|
<>
|
||||||
<DropdownItem header>Bots:</DropdownItem>
|
<DropdownItem header>Bots:</DropdownItem>
|
||||||
|
|||||||
@ -54,7 +54,7 @@ describe('<DomainSelector />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[ 0, 'default.com<span class="float-right text-muted">default</span>' ],
|
[ 0, 'default.com<span class="float-end text-muted">default</span>' ],
|
||||||
[ 1, 'foo.com' ],
|
[ 1, 'foo.com' ],
|
||||||
[ 2, 'bar.com' ],
|
[ 2, 'bar.com' ],
|
||||||
])('shows expected content on every item', (index, expectedContent) => {
|
])('shows expected content on every item', (index, expectedContent) => {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { ServerForm } from '../../../src/servers/helpers/ServerForm';
|
import { ServerForm } from '../../../src/servers/helpers/ServerForm';
|
||||||
|
import { InputFormGroup } from '../../../src/utils/forms/InputFormGroup';
|
||||||
|
|
||||||
describe('<ServerForm />', () => {
|
describe('<ServerForm />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
@ -13,7 +14,7 @@ describe('<ServerForm />', () => {
|
|||||||
afterEach(jest.resetAllMocks);
|
afterEach(jest.resetAllMocks);
|
||||||
|
|
||||||
it('renders components', () => {
|
it('renders components', () => {
|
||||||
expect(wrapper.find('FormGroup')).toHaveLength(3);
|
expect(wrapper.find(InputFormGroup)).toHaveLength(3);
|
||||||
expect(wrapper.find('span')).toHaveLength(1);
|
expect(wrapper.find('span')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { Input } from 'reactstrap';
|
import { Input } from 'reactstrap';
|
||||||
|
import { FormText } from '../../src/utils/forms/FormText';
|
||||||
import {
|
import {
|
||||||
RealTimeUpdatesSettings as RealTimeUpdatesSettingsOptions,
|
RealTimeUpdatesSettings as RealTimeUpdatesSettingsOptions,
|
||||||
Settings,
|
Settings,
|
||||||
} from '../../src/settings/reducers/settings';
|
} from '../../src/settings/reducers/settings';
|
||||||
import RealTimeUpdatesSettings from '../../src/settings/RealTimeUpdatesSettings';
|
import RealTimeUpdatesSettings from '../../src/settings/RealTimeUpdatesSettings';
|
||||||
import ToggleSwitch from '../../src/utils/ToggleSwitch';
|
import ToggleSwitch from '../../src/utils/ToggleSwitch';
|
||||||
|
import { LabeledFormGroup } from '../../src/utils/forms/LabeledFormGroup';
|
||||||
|
|
||||||
describe('<RealTimeUpdatesSettings />', () => {
|
describe('<RealTimeUpdatesSettings />', () => {
|
||||||
const toggleRealTimeUpdates = jest.fn();
|
const toggleRealTimeUpdates = jest.fn();
|
||||||
@ -32,31 +34,31 @@ describe('<RealTimeUpdatesSettings />', () => {
|
|||||||
it('renders enabled real time updates as expected', () => {
|
it('renders enabled real time updates as expected', () => {
|
||||||
const wrapper = createWrapper({ enabled: true });
|
const wrapper = createWrapper({ enabled: true });
|
||||||
const toggle = wrapper.find(ToggleSwitch);
|
const toggle = wrapper.find(ToggleSwitch);
|
||||||
const label = wrapper.find('label');
|
const label = wrapper.find(LabeledFormGroup);
|
||||||
const input = wrapper.find(Input);
|
const input = wrapper.find(Input);
|
||||||
const small = wrapper.find('small');
|
const formText = wrapper.find(FormText);
|
||||||
|
|
||||||
expect(toggle.prop('checked')).toEqual(true);
|
expect(toggle.prop('checked')).toEqual(true);
|
||||||
expect(toggle.html()).toContain('processed');
|
expect(toggle.html()).toContain('processed');
|
||||||
expect(toggle.html()).not.toContain('ignored');
|
expect(toggle.html()).not.toContain('ignored');
|
||||||
expect(label.prop('className')).toEqual('');
|
expect(label.prop('labelClassName')).not.toContain('text-muted');
|
||||||
expect(input.prop('disabled')).toEqual(false);
|
expect(input.prop('disabled')).toEqual(false);
|
||||||
expect(small).toHaveLength(2);
|
expect(formText).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders disabled real time updates as expected', () => {
|
it('renders disabled real time updates as expected', () => {
|
||||||
const wrapper = createWrapper({ enabled: false });
|
const wrapper = createWrapper({ enabled: false });
|
||||||
const toggle = wrapper.find(ToggleSwitch);
|
const toggle = wrapper.find(ToggleSwitch);
|
||||||
const label = wrapper.find('label');
|
const label = wrapper.find(LabeledFormGroup);
|
||||||
const input = wrapper.find(Input);
|
const input = wrapper.find(Input);
|
||||||
const small = wrapper.find('small');
|
const formText = wrapper.find(FormText);
|
||||||
|
|
||||||
expect(toggle.prop('checked')).toEqual(false);
|
expect(toggle.prop('checked')).toEqual(false);
|
||||||
expect(toggle.html()).not.toContain('processed');
|
expect(toggle.html()).not.toContain('processed');
|
||||||
expect(toggle.html()).toContain('ignored');
|
expect(toggle.html()).toContain('ignored');
|
||||||
expect(label.prop('className')).toEqual('text-muted');
|
expect(label.prop('labelClassName')).toContain('text-muted');
|
||||||
expect(input.prop('disabled')).toEqual(true);
|
expect(input.prop('disabled')).toEqual(true);
|
||||||
expect(small).toHaveLength(1);
|
expect(formText).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@ -79,11 +81,11 @@ describe('<RealTimeUpdatesSettings />', () => {
|
|||||||
it.each([[ undefined ], [ 0 ]])('shows expected children when interval is 0 or undefined', (interval) => {
|
it.each([[ undefined ], [ 0 ]])('shows expected children when interval is 0 or undefined', (interval) => {
|
||||||
const wrapper = createWrapper({ enabled: true, interval });
|
const wrapper = createWrapper({ enabled: true, interval });
|
||||||
const span = wrapper.find('span');
|
const span = wrapper.find('span');
|
||||||
const small = wrapper.find('small').at(1);
|
const formText = wrapper.find(FormText).at(1);
|
||||||
const input = wrapper.find(Input);
|
const input = wrapper.find(Input);
|
||||||
|
|
||||||
expect(span).toHaveLength(0);
|
expect(span).toHaveLength(0);
|
||||||
expect(small.html()).toContain('Updates will be reflected in the UI as soon as they happen.');
|
expect(formText.html()).toContain('Updates will be reflected in the UI as soon as they happen.');
|
||||||
expect(input.prop('value')).toEqual('');
|
expect(input.prop('value')).toEqual('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,6 @@ describe('<Settings />', () => {
|
|||||||
expect(items).toHaveLength(3);
|
expect(items).toHaveLength(3);
|
||||||
expect(items.first().prop('to')).toEqual('general');
|
expect(items.first().prop('to')).toEqual('general');
|
||||||
expect(items.at(1).prop('to')).toEqual('short-urls');
|
expect(items.at(1).prop('to')).toEqual('short-urls');
|
||||||
expect(items.last().prop('to')).toEqual('secondary-items');
|
expect(items.last().prop('to')).toEqual('other-items');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Mock } from 'ts-mockery';
|
|||||||
import { DropdownItem } from 'reactstrap';
|
import { DropdownItem } from 'reactstrap';
|
||||||
import { ShortUrlCreationSettings as ShortUrlsSettings, Settings } from '../../src/settings/reducers/settings';
|
import { ShortUrlCreationSettings as ShortUrlsSettings, Settings } from '../../src/settings/reducers/settings';
|
||||||
import { ShortUrlCreationSettings } from '../../src/settings/ShortUrlCreationSettings';
|
import { ShortUrlCreationSettings } from '../../src/settings/ShortUrlCreationSettings';
|
||||||
|
import { FormText } from '../../src/utils/forms/FormText';
|
||||||
import ToggleSwitch from '../../src/utils/ToggleSwitch';
|
import ToggleSwitch from '../../src/utils/ToggleSwitch';
|
||||||
import { DropdownBtn } from '../../src/utils/DropdownBtn';
|
import { DropdownBtn } from '../../src/utils/DropdownBtn';
|
||||||
|
|
||||||
@ -46,25 +47,25 @@ describe('<ShortUrlCreationSettings />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{ validateUrls: true }, 'Validate URL checkbox will be checked' ],
|
[{ validateUrls: true }, '<b>Validate URL</b> checkbox will be <b>checked</b>' ],
|
||||||
[{ validateUrls: false }, 'Validate URL checkbox will be unchecked' ],
|
[{ validateUrls: false }, '<b>Validate URL</b> checkbox will be <b>unchecked</b>' ],
|
||||||
[ undefined, 'Validate URL checkbox will be unchecked' ],
|
[ undefined, '<b>Validate URL</b> checkbox will be <b>unchecked</b>' ],
|
||||||
])('shows expected helper text for URL validation', (shortUrlCreation, expectedText) => {
|
])('shows expected helper text for URL validation', (shortUrlCreation, expectedText) => {
|
||||||
const wrapper = createWrapper(shortUrlCreation);
|
const wrapper = createWrapper(shortUrlCreation);
|
||||||
const validateUrlText = wrapper.find('.form-text').first();
|
const validateUrlText = wrapper.find(FormText).first();
|
||||||
|
|
||||||
expect(validateUrlText.text()).toContain(expectedText);
|
expect(validateUrlText.html()).toContain(expectedText);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{ forwardQuery: true }, 'Forward query params on redirect checkbox will be checked' ],
|
[{ forwardQuery: true }, '<b>Forward query params on redirect</b> checkbox will be <b>checked</b>' ],
|
||||||
[{ forwardQuery: false }, 'Forward query params on redirect checkbox will be unchecked' ],
|
[{ forwardQuery: false }, '<b>Forward query params on redirect</b> checkbox will be <b>unchecked</b>' ],
|
||||||
[{}, 'Forward query params on redirect checkbox will be checked' ],
|
[{}, '<b>Forward query params on redirect</b> checkbox will be <b>checked</b>' ],
|
||||||
])('shows expected helper text for query forwarding', (shortUrlCreation, expectedText) => {
|
])('shows expected helper text for query forwarding', (shortUrlCreation, expectedText) => {
|
||||||
const wrapper = createWrapper({ validateUrls: true, ...shortUrlCreation });
|
const wrapper = createWrapper({ validateUrls: true, ...shortUrlCreation });
|
||||||
const forwardQueryText = wrapper.find('.form-text').at(1);
|
const forwardQueryText = wrapper.find(FormText).at(1);
|
||||||
|
|
||||||
expect(forwardQueryText.text()).toContain(expectedText);
|
expect(forwardQueryText.html()).toContain(expectedText);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@ -77,11 +78,11 @@ describe('<ShortUrlCreationSettings />', () => {
|
|||||||
[ undefined, 'Suggest tags starting with input', 'starting with' ],
|
[ undefined, 'Suggest tags starting with input', 'starting with' ],
|
||||||
])('shows expected texts for tags suggestions', (shortUrlCreation, expectedText, expectedHint) => {
|
])('shows expected texts for tags suggestions', (shortUrlCreation, expectedText, expectedHint) => {
|
||||||
const wrapper = createWrapper(shortUrlCreation);
|
const wrapper = createWrapper(shortUrlCreation);
|
||||||
const hintText = wrapper.find('.form-text').last();
|
const hintText = wrapper.find(FormText).last();
|
||||||
const dropdown = wrapper.find(DropdownBtn);
|
const dropdown = wrapper.find(DropdownBtn);
|
||||||
|
|
||||||
expect(dropdown.prop('text')).toEqual(expectedText);
|
expect(dropdown.prop('text')).toEqual(expectedText);
|
||||||
expect(hintText.text()).toContain(expectedHint);
|
expect(hintText.html()).toContain(expectedHint);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([[ true ], [ false ]])('invokes setShortUrlCreationSettings when URL validation toggle value changes', (validateUrls) => {
|
it.each([[ true ], [ false ]])('invokes setShortUrlCreationSettings when URL validation toggle value changes', (validateUrls) => {
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { FormGroup } from 'reactstrap';
|
|
||||||
import { Settings, TagsMode, TagsSettings as TagsSettingsOptions } from '../../src/settings/reducers/settings';
|
import { Settings, TagsMode, TagsSettings as TagsSettingsOptions } from '../../src/settings/reducers/settings';
|
||||||
import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown';
|
import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown';
|
||||||
import { TagsSettings } from '../../src/settings/TagsSettings';
|
import { TagsSettings } from '../../src/settings/TagsSettings';
|
||||||
import { OrderingDropdown } from '../../src/utils/OrderingDropdown';
|
import { OrderingDropdown } from '../../src/utils/OrderingDropdown';
|
||||||
import { TagsOrder } from '../../src/tags/data/TagsListChildrenProps';
|
import { TagsOrder } from '../../src/tags/data/TagsListChildrenProps';
|
||||||
|
import { LabeledFormGroup } from '../../src/utils/forms/LabeledFormGroup';
|
||||||
|
import { FormText } from '../../src/utils/forms/FormText';
|
||||||
|
|
||||||
describe('<TagsSettings />', () => {
|
describe('<TagsSettings />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
@ -21,7 +22,7 @@ describe('<TagsSettings />', () => {
|
|||||||
|
|
||||||
it('renders expected amount of groups', () => {
|
it('renders expected amount of groups', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
const groups = wrapper.find(FormGroup);
|
const groups = wrapper.find(LabeledFormGroup);
|
||||||
|
|
||||||
expect(groups).toHaveLength(2);
|
expect(groups).toHaveLength(2);
|
||||||
});
|
});
|
||||||
@ -34,10 +35,10 @@ describe('<TagsSettings />', () => {
|
|||||||
])('shows expected tags displaying mode', (tags, expectedMode) => {
|
])('shows expected tags displaying mode', (tags, expectedMode) => {
|
||||||
const wrapper = createWrapper(tags);
|
const wrapper = createWrapper(tags);
|
||||||
const dropdown = wrapper.find(TagsModeDropdown);
|
const dropdown = wrapper.find(TagsModeDropdown);
|
||||||
const small = wrapper.find('small');
|
const formText = wrapper.find(FormText);
|
||||||
|
|
||||||
expect(dropdown.prop('mode')).toEqual(expectedMode);
|
expect(dropdown.prop('mode')).toEqual(expectedMode);
|
||||||
expect(small.html()).toContain(`Tags will be displayed as <b>${expectedMode}</b>.`);
|
expect(formText.html()).toContain(`Tags will be displayed as <b>${expectedMode}</b>.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Settings } from '../../src/settings/reducers/settings';
|
|||||||
import { VisitsSettings } from '../../src/settings/VisitsSettings';
|
import { VisitsSettings } from '../../src/settings/VisitsSettings';
|
||||||
import { SimpleCard } from '../../src/utils/SimpleCard';
|
import { SimpleCard } from '../../src/utils/SimpleCard';
|
||||||
import { DateIntervalSelector } from '../../src/utils/dates/DateIntervalSelector';
|
import { DateIntervalSelector } from '../../src/utils/dates/DateIntervalSelector';
|
||||||
|
import { LabeledFormGroup } from '../../src/utils/forms/LabeledFormGroup';
|
||||||
|
|
||||||
describe('<VisitsSettings />', () => {
|
describe('<VisitsSettings />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
@ -21,7 +22,7 @@ describe('<VisitsSettings />', () => {
|
|||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
|
|
||||||
expect(wrapper.find(SimpleCard).prop('title')).toEqual('Visits');
|
expect(wrapper.find(SimpleCard).prop('title')).toEqual('Visits');
|
||||||
expect(wrapper.find('label').prop('children')).toEqual('Default interval to load on visits sections:');
|
expect(wrapper.find(LabeledFormGroup).prop('label')).toEqual('Default interval to load on visits sections:');
|
||||||
expect(wrapper.find(DateIntervalSelector)).toHaveLength(1);
|
expect(wrapper.find(DateIntervalSelector)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -95,7 +95,7 @@ describe('<QrCodeModal />', () => {
|
|||||||
const firstCol = wrapper.find(Row).find(FormGroup).first();
|
const firstCol = wrapper.find(Row).find(FormGroup).first();
|
||||||
|
|
||||||
expect(dropdownsLength).toEqual(expectedAmountOfDropdowns);
|
expect(dropdownsLength).toEqual(expectedAmountOfDropdowns);
|
||||||
expect(firstCol.prop('className')).toEqual(expectedRangeClass);
|
expect(firstCol.prop('className')).toEqual(`d-grid ${expectedRangeClass}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('saves the QR code image when clicking the Download button', () => {
|
it('saves the QR code image when clicking the Download button', () => {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { InfoTooltip } from '../../../src/utils/InfoTooltip';
|
|||||||
describe('<ShortUrlFormCheckboxGroup />', () => {
|
describe('<ShortUrlFormCheckboxGroup />', () => {
|
||||||
it.each([
|
it.each([
|
||||||
[ undefined, '', 0 ],
|
[ undefined, '', 0 ],
|
||||||
[ 'This is the tooltip', 'mr-2', 1 ],
|
[ 'This is the tooltip', 'me-2', 1 ],
|
||||||
])('renders tooltip only when provided', (infoTooltip, expectedClassName, expectedAmountOfTooltips) => {
|
])('renders tooltip only when provided', (infoTooltip, expectedClassName, expectedAmountOfTooltips) => {
|
||||||
const wrapper = shallow(<ShortUrlFormCheckboxGroup infoTooltip={infoTooltip} />);
|
const wrapper = shallow(<ShortUrlFormCheckboxGroup infoTooltip={infoTooltip} />);
|
||||||
const checkbox = wrapper.find(Checkbox);
|
const checkbox = wrapper.find(Checkbox);
|
||||||
|
|||||||
@ -103,7 +103,7 @@ describe('<EditTagModal />', () => {
|
|||||||
expect(wrapper.find(Popover).prop('isOpen')).toEqual(false);
|
expect(wrapper.find(Popover).prop('isOpen')).toEqual(false);
|
||||||
(wrapper.find(Popover).prop('toggle') as Function)();
|
(wrapper.find(Popover).prop('toggle') as Function)();
|
||||||
expect(wrapper.find(Popover).prop('isOpen')).toEqual(true);
|
expect(wrapper.find(Popover).prop('isOpen')).toEqual(true);
|
||||||
wrapper.find('.input-group-prepend').simulate('click');
|
wrapper.find('div').simulate('click');
|
||||||
expect(wrapper.find(Popover).prop('isOpen')).toEqual(false);
|
expect(wrapper.find(Popover).prop('isOpen')).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -63,7 +63,7 @@ describe('<Checkbox />', () => {
|
|||||||
|
|
||||||
it('allows setting inline rendering', () => {
|
it('allows setting inline rendering', () => {
|
||||||
const wrapped = createComponent({ inline: true });
|
const wrapped = createComponent({ inline: true });
|
||||||
const control = wrapped.find('.custom-control');
|
const control = wrapped.find('.form-check');
|
||||||
|
|
||||||
expect(control.prop('style')).toEqual({ display: 'inline-block' });
|
expect(control.prop('style')).toEqual({ display: 'inline-block' });
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,6 +22,6 @@ describe('<CopyToClipboardIcon />', () => {
|
|||||||
expect(copyToClipboard.prop('onCopy')).toEqual(onCopy);
|
expect(copyToClipboard.prop('onCopy')).toEqual(onCopy);
|
||||||
expect(icon).toHaveLength(1);
|
expect(icon).toHaveLength(1);
|
||||||
expect(icon.prop('icon')).toEqual(copyIcon);
|
expect(icon.prop('icon')).toEqual(copyIcon);
|
||||||
expect(icon.prop('className')).toEqual('ml-2 copy-to-clipboard-icon');
|
expect(icon.prop('className')).toEqual('ms-2 copy-to-clipboard-icon');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -36,7 +36,7 @@ describe('<TableOrderIcon />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[ undefined, 'ml-1' ],
|
[ undefined, 'ms-1' ],
|
||||||
[ 'foo', 'foo' ],
|
[ 'foo', 'foo' ],
|
||||||
[ 'bar', 'bar' ],
|
[ 'bar', 'bar' ],
|
||||||
])('renders expected classname', (className, expectedClassName) => {
|
])('renders expected classname', (className, expectedClassName) => {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ describe('<MapModal />', () => {
|
|||||||
|
|
||||||
expect(modal.prop('toggle')).toEqual(toggle);
|
expect(modal.prop('toggle')).toEqual(toggle);
|
||||||
expect(modal.prop('isOpen')).toEqual(isOpen);
|
expect(modal.prop('isOpen')).toEqual(isOpen);
|
||||||
expect(header.find('.close').prop('onClick')).toEqual(toggle);
|
expect(header.find('.btn-close').prop('onClick')).toEqual(toggle);
|
||||||
expect(header.text()).toContain(title);
|
expect(header.text()).toContain(title);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -17,15 +17,14 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "esnext",
|
"target": "es2019",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true
|
"isolatedModules": true
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules"
|
||||||
"**/*.spec.*"
|
|
||||||
],
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"*.d.ts",
|
"*.d.ts",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user