Created new feature checkers

This commit is contained in:
Alejandro Celaya 2021-03-06 09:58:29 +01:00
parent 70ce099913
commit 5224e7b4ef
8 changed files with 56 additions and 42 deletions

View file

@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames'; import classNames from 'classnames';
import { withSelectedServer } from '../servers/helpers/withSelectedServer'; import { withSelectedServer } from '../servers/helpers/withSelectedServer';
import { useSwipeable, useToggle } from '../utils/helpers/hooks'; import { useSwipeable, useToggle } from '../utils/helpers/hooks';
import { versionMatch } from '../utils/helpers/version'; import { supportsOrphanVisits, supportsTagVisits } from '../utils/helpers/features';
import { isReachableServer } from '../servers/data'; import { isReachableServer } from '../servers/data';
import NotFound from './NotFound'; import NotFound from './NotFound';
import { AsideMenuProps } from './AsideMenu'; import { AsideMenuProps } from './AsideMenu';
@ -30,8 +30,8 @@ const MenuLayout = (
return <ServerError />; return <ServerError />;
} }
const addTagsVisitsRoute = versionMatch(selectedServer.version, { minVersion: '2.2.0' }); const addTagsVisitsRoute = supportsTagVisits(selectedServer);
const addOrphanVisitsRoute = versionMatch(selectedServer.version, { minVersion: '2.6.0' }); const addOrphanVisitsRoute = supportsOrphanVisits(selectedServer);
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible }); const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
const swipeableProps = useSwipeable(showSidebar, hideSidebar); const swipeableProps = useSwipeable(showSidebar, hideSidebar);

View file

@ -5,9 +5,10 @@ import { InputType } from 'reactstrap/lib/Input';
import * as m from 'moment'; import * as m from 'moment';
import DateInput, { DateInputProps } from '../utils/DateInput'; import DateInput, { DateInputProps } from '../utils/DateInput';
import Checkbox from '../utils/Checkbox'; import Checkbox from '../utils/Checkbox';
import { versionMatch, Versions } from '../utils/helpers/version'; import { Versions } from '../utils/helpers/version';
import { supportsListingDomains, supportsSettingShortCodeLength } from '../utils/helpers/features';
import { handleEventPreventingDefault, hasValue } from '../utils/utils'; import { handleEventPreventingDefault, hasValue } from '../utils/utils';
import { isReachableServer, SelectedServer } from '../servers/data'; import { SelectedServer } from '../servers/data';
import { formatIsoDate } from '../utils/helpers/date'; import { formatIsoDate } from '../utils/helpers/date';
import { TagsSelectorProps } from '../tags/helpers/TagsSelector'; import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { DomainSelectorProps } from '../domains/DomainSelector'; import { DomainSelectorProps } from '../domains/DomainSelector';
@ -117,9 +118,8 @@ const CreateShortUrl = (
</> </>
); );
const currentServerVersion = isReachableServer(selectedServer) ? selectedServer.version : ''; const showDomainSelector = supportsListingDomains(selectedServer);
const showDomainSelector = versionMatch(currentServerVersion, { minVersion: '2.4.0' }); const disableShortCodeLength = !supportsSettingShortCodeLength(selectedServer);
const disableShortCodeLength = !versionMatch(currentServerVersion, { minVersion: '2.1.0' });
return ( return (
<form className="create-short-url" onSubmit={save}> <form className="create-short-url" onSubmit={save}>

View file

@ -2,7 +2,7 @@ import { FC, ReactNode } from 'react';
import { isEmpty } from 'ramda'; import { isEmpty } from 'ramda';
import classNames from 'classnames'; import classNames from 'classnames';
import { SelectedServer } from '../servers/data'; import { SelectedServer } from '../servers/data';
import { titleIsSupported } from '../utils/helpers/features'; import { supportsShortUrlTitle } from '../utils/helpers/features';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { ShortUrlsRowProps } from './helpers/ShortUrlsRow'; import { ShortUrlsRowProps } from './helpers/ShortUrlsRow';
import { OrderableFields } from './reducers/shortUrlsListParams'; import { OrderableFields } from './reducers/shortUrlsListParams';
@ -29,7 +29,7 @@ export const ShortUrlsTable = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
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', className);
const supportsTitle = titleIsSupported(selectedServer); const supportsTitle = supportsShortUrlTitle(selectedServer);
const renderShortUrls = () => { const renderShortUrls = () => {
if (error) { if (error) {

View file

@ -3,15 +3,15 @@ import { Modal, DropdownItem, FormGroup, ModalBody, ModalHeader, Row } from 'rea
import { ExternalLink } from 'react-external-link'; import { ExternalLink } from 'react-external-link';
import classNames from 'classnames'; import classNames from 'classnames';
import { ShortUrlModalProps } from '../data'; import { ShortUrlModalProps } from '../data';
import { ReachableServer } from '../../servers/data'; import { SelectedServer } from '../../servers/data';
import { versionMatch } from '../../utils/helpers/version';
import { DropdownBtn } from '../../utils/DropdownBtn'; import { DropdownBtn } from '../../utils/DropdownBtn';
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon'; import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
import { buildQrCodeUrl, QrCodeCapabilities, QrCodeFormat } from '../../utils/helpers/qrCodes'; import { buildQrCodeUrl, QrCodeCapabilities, QrCodeFormat } from '../../utils/helpers/qrCodes';
import { supportsQrCodeSizeInQuery, supportsQrCodeSvgFormat, supportsQrCodeMargin } from '../../utils/helpers/features';
import './QrCodeModal.scss'; import './QrCodeModal.scss';
interface QrCodeModalConnectProps extends ShortUrlModalProps { interface QrCodeModalConnectProps extends ShortUrlModalProps {
selectedServer: ReachableServer; selectedServer: SelectedServer;
} }
const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps) => { const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps) => {
@ -19,9 +19,9 @@ const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }:
const [ margin, setMargin ] = useState(0); const [ margin, setMargin ] = useState(0);
const [ format, setFormat ] = useState<QrCodeFormat>('png'); const [ format, setFormat ] = useState<QrCodeFormat>('png');
const capabilities: QrCodeCapabilities = useMemo(() => ({ const capabilities: QrCodeCapabilities = useMemo(() => ({
useSizeInPath: !versionMatch(selectedServer.version, { minVersion: '2.5.0' }), useSizeInPath: !supportsQrCodeSizeInQuery(selectedServer),
svgIsSupported: versionMatch(selectedServer.version, { minVersion: '2.4.0' }), svgIsSupported: supportsQrCodeSvgFormat(selectedServer),
marginIsSupported: versionMatch(selectedServer.version, { minVersion: '2.6.0' }), marginIsSupported: supportsQrCodeMargin(selectedServer),
}), [ selectedServer ]); }), [ selectedServer ]);
const qrCodeUrl = useMemo( const qrCodeUrl = useMemo(
() => buildQrCodeUrl(shortUrl, { size, format, margin }, capabilities), () => buildQrCodeUrl(shortUrl, { size, format, margin }, capabilities),

View file

@ -7,8 +7,7 @@ import { ShortUrlIdentifier } from '../data';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { ProblemDetailsError } from '../../api/types'; import { ProblemDetailsError } from '../../api/types';
import { parseApiError } from '../../api/utils'; import { parseApiError } from '../../api/utils';
import { isReachableServer } from '../../servers/data'; import { supportsTagsInPatch } from '../../utils/helpers/features';
import { versionMatch } from '../../utils/helpers/version';
/* eslint-disable padding-line-between-statements */ /* eslint-disable padding-line-between-statements */
export const EDIT_SHORT_URL_TAGS_START = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_START'; export const EDIT_SHORT_URL_TAGS_START = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_START';
@ -54,15 +53,12 @@ export const editShortUrlTags = (buildShlinkApiClient: ShlinkApiClientBuilder) =
) => async (dispatch: Dispatch, getState: GetState) => { ) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({ type: EDIT_SHORT_URL_TAGS_START }); dispatch({ type: EDIT_SHORT_URL_TAGS_START });
const { selectedServer } = getState(); const { selectedServer } = getState();
const supportsTagsInPatch = isReachableServer(selectedServer) && versionMatch( const tagsInPatch = supportsTagsInPatch(selectedServer);
selectedServer.version,
{ minVersion: '2.6.0' },
);
const { updateShortUrlTags, updateShortUrlMeta } = buildShlinkApiClient(getState); const { updateShortUrlTags, updateShortUrlMeta } = buildShlinkApiClient(getState);
try { try {
const normalizedTags = await ( const normalizedTags = await (
supportsTagsInPatch tagsInPatch
? updateShortUrlMeta(shortCode, domain, { tags }).then(prop('tags')) ? updateShortUrlMeta(shortCode, domain, { tags }).then(prop('tags'))
: updateShortUrlTags(shortCode, domain, tags) : updateShortUrlTags(shortCode, domain, tags)
); );

View file

@ -4,4 +4,20 @@ import { versionMatch, Versions } from './version';
const serverMatchesVersions = (versions: Versions) => (selectedServer: SelectedServer): boolean => const serverMatchesVersions = (versions: Versions) => (selectedServer: SelectedServer): boolean =>
isReachableServer(selectedServer) && versionMatch(selectedServer.version, versions); isReachableServer(selectedServer) && versionMatch(selectedServer.version, versions);
export const titleIsSupported = serverMatchesVersions({ minVersion: '2.6.0' }); export const supportsSettingShortCodeLength = serverMatchesVersions({ minVersion: '2.1.0' });
export const supportsTagVisits = serverMatchesVersions({ minVersion: '2.2.0' });
export const supportsListingDomains = serverMatchesVersions({ minVersion: '2.4.0' });
export const supportsQrCodeSvgFormat = supportsListingDomains;
export const supportsQrCodeSizeInQuery = serverMatchesVersions({ minVersion: '2.5.0' });
export const supportsShortUrlTitle = serverMatchesVersions({ minVersion: '2.6.0' });
export const supportsOrphanVisits = supportsShortUrlTitle;
export const supportsQrCodeMargin = supportsShortUrlTitle;
export const supportsTagsInPatch = supportsShortUrlTitle;

View file

@ -13,7 +13,7 @@ export interface Versions {
minVersion?: SemVerPattern; minVersion?: SemVerPattern;
} }
export type SemVer = `${bigint}.${bigint}.${bigint}`; export type SemVer = `${bigint}.${bigint}.${bigint}` | 'latest';
export const versionMatch = (versionToMatch: SemVer | Empty, { maxVersion, minVersion }: Versions): boolean => { export const versionMatch = (versionToMatch: SemVer | Empty, { maxVersion, minVersion }: Versions): boolean => {
if (!hasValue(versionToMatch)) { if (!hasValue(versionToMatch)) {
@ -26,7 +26,7 @@ export const versionMatch = (versionToMatch: SemVer | Empty, { maxVersion, minVe
return matchesMaxVersion && matchesMinVersion; return matchesMaxVersion && matchesMinVersion;
}; };
const versionIsValidSemVer = memoizeWith(identity, (version: string): version is SemVerPattern => { const versionIsValidSemVer = memoizeWith(identity, (version: string): version is SemVer => {
try { try {
return compare(version, version, '='); return compare(version, version, '=');
} catch (e) { } catch (e) {
@ -36,5 +36,5 @@ const versionIsValidSemVer = memoizeWith(identity, (version: string): version is
export const versionToPrintable = (version: string) => !versionIsValidSemVer(version) ? version : `v${version}`; export const versionToPrintable = (version: string) => !versionIsValidSemVer(version) ? version : `v${version}`;
export const versionToSemVer = (defaultValue = 'latest') => export const versionToSemVer = (defaultValue: SemVer = 'latest') =>
(version: string) => versionIsValidSemVer(version) ? version : defaultValue; (version: string): SemVer => versionIsValidSemVer(version) ? version : defaultValue;

View file

@ -1,21 +1,23 @@
import { versionMatch } from '../../../src/utils/helpers/version'; import { Mock } from 'ts-mockery';
import { SemVer, versionMatch, Versions } from '../../../src/utils/helpers/version';
import { Empty } from '../../../src/utils/utils';
describe('version', () => { describe('version', () => {
describe('versionMatch', () => { describe('versionMatch', () => {
it.each([ it.each([
[ undefined, {}, false ], [ undefined, Mock.all<Versions>(), false ],
[ null, {}, false ], [ null, Mock.all<Versions>(), false ],
[ '', {}, false ], [ '' as Empty, Mock.all<Versions>(), false ],
[[], {}, false ], [[], Mock.all<Versions>(), false ],
[ '2.8.3', {}, true ], [ '2.8.3' as SemVer, Mock.all<Versions>(), true ],
[ '2.8.3', { minVersion: '2.0.0' }, true ], [ '2.8.3' as SemVer, Mock.of<Versions>({ minVersion: '2.0.0' }), true ],
[ '2.0.0', { minVersion: '2.0.0' }, true ], [ '2.0.0' as SemVer, Mock.of<Versions>({ minVersion: '2.0.0' }), true ],
[ '1.8.0', { maxVersion: '1.8.0' }, true ], [ '1.8.0' as SemVer, Mock.of<Versions>({ maxVersion: '1.8.0' }), true ],
[ '1.7.1', { maxVersion: '1.8.0' }, true ], [ '1.7.1' as SemVer, Mock.of<Versions>({ maxVersion: '1.8.0' }), true ],
[ '1.7.3', { minVersion: '1.7.0', maxVersion: '1.8.0' }, true ], [ '1.7.3' as SemVer, Mock.of<Versions>({ minVersion: '1.7.0', maxVersion: '1.8.0' }), true ],
[ '1.8.3', { minVersion: '2.0.0' }, false ], [ '1.8.3' as SemVer, Mock.of<Versions>({ minVersion: '2.0.0' }), false ],
[ '1.8.3', { maxVersion: '1.8.0' }, false ], [ '1.8.3' as SemVer, Mock.of<Versions>({ maxVersion: '1.8.0' }), false ],
[ '1.8.3', { minVersion: '1.7.0', maxVersion: '1.8.0' }, false ], [ '1.8.3' as SemVer, Mock.of<Versions>({ minVersion: '1.7.0', maxVersion: '1.8.0' }), false ],
])('properly matches versions based on what is provided', (version, versionConstraints, expected) => { ])('properly matches versions based on what is provided', (version, versionConstraints, expected) => {
expect(versionMatch(version, versionConstraints)).toEqual(expected); expect(versionMatch(version, versionConstraints)).toEqual(expected);
}); });