mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Extract initial Shlink logic to ShlinkWebComponent
This commit is contained in:
parent
d82c0dc75e
commit
682de08204
20 changed files with 197 additions and 157 deletions
|
@ -10,12 +10,10 @@ import classNames from 'classnames';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { NavLinkProps } from 'react-router-dom';
|
import type { NavLinkProps } from 'react-router-dom';
|
||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
import type { SelectedServer } from '../servers/data';
|
|
||||||
import { isServerWithId } from '../servers/data';
|
|
||||||
import './AsideMenu.scss';
|
import './AsideMenu.scss';
|
||||||
|
|
||||||
export interface AsideMenuProps {
|
export interface AsideMenuProps {
|
||||||
selectedServer: SelectedServer;
|
routePrefix: string;
|
||||||
showOnMobile?: boolean;
|
showOnMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,14 +32,12 @@ const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...res
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AsideMenu: FC<AsideMenuProps> = ({ selectedServer, showOnMobile = false }) => {
|
export const AsideMenu: FC<AsideMenuProps> = ({ routePrefix, showOnMobile = false }) => {
|
||||||
const hasId = isServerWithId(selectedServer);
|
|
||||||
const serverId = hasId ? selectedServer.id : '';
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const asideClass = classNames('aside-menu', {
|
const asideClass = classNames('aside-menu', {
|
||||||
'aside-menu--hidden': !showOnMobile,
|
'aside-menu--hidden': !showOnMobile,
|
||||||
});
|
});
|
||||||
const buildPath = (suffix: string) => `/server/${serverId}${suffix}`;
|
const buildPath = (suffix: string) => `${routePrefix}${suffix}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={asideClass}>
|
<aside className={asideClass}>
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
|
||||||
import { isReachableServer } from '../servers/data';
|
import { isReachableServer } from '../servers/data';
|
||||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
||||||
import { useFeature } from '../utils/helpers/features';
|
import type { ShlinkWebComponentType } from '../shlink-web-component';
|
||||||
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
|
|
||||||
import { AsideMenu } from './AsideMenu';
|
|
||||||
import { NotFound } from './NotFound';
|
|
||||||
import './MenuLayout.scss';
|
import './MenuLayout.scss';
|
||||||
|
|
||||||
interface MenuLayoutProps {
|
interface MenuLayoutProps {
|
||||||
|
@ -18,24 +11,11 @@ interface MenuLayoutProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MenuLayout = (
|
export const MenuLayout = (
|
||||||
TagsList: FC,
|
|
||||||
ShortUrlsList: FC,
|
|
||||||
CreateShortUrl: FC,
|
|
||||||
ShortUrlVisits: FC,
|
|
||||||
TagVisits: FC,
|
|
||||||
DomainVisits: FC,
|
|
||||||
OrphanVisits: FC,
|
|
||||||
NonOrphanVisits: FC,
|
|
||||||
ServerError: FC,
|
ServerError: FC,
|
||||||
Overview: FC,
|
ShlinkWebComponent: ShlinkWebComponentType,
|
||||||
EditShortUrl: FC,
|
|
||||||
ManageDomains: FC,
|
|
||||||
) => withSelectedServer<MenuLayoutProps>(({ selectedServer, sidebarNotPresent, sidebarPresent }) => {
|
) => withSelectedServer<MenuLayoutProps>(({ selectedServer, sidebarNotPresent, sidebarPresent }) => {
|
||||||
const location = useLocation();
|
|
||||||
const [sidebarVisible, toggleSidebar, showSidebar, hideSidebar] = useToggle();
|
|
||||||
const showContent = isReachableServer(selectedServer);
|
const showContent = isReachableServer(selectedServer);
|
||||||
|
|
||||||
useEffect(() => hideSidebar(), [location]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
showContent && sidebarPresent();
|
showContent && sidebarPresent();
|
||||||
return () => sidebarNotPresent();
|
return () => sidebarNotPresent();
|
||||||
|
@ -45,42 +25,10 @@ export const MenuLayout = (
|
||||||
return <ServerError />;
|
return <ServerError />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addNonOrphanVisitsRoute = useFeature('nonOrphanVisits', selectedServer);
|
|
||||||
const addDomainVisitsRoute = useFeature('domainVisits', selectedServer);
|
|
||||||
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
|
|
||||||
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ShlinkWebComponent
|
||||||
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
|
serverVersion={selectedServer.version}
|
||||||
|
routesPrefix={`/server/${selectedServer.id}`}
|
||||||
<div {...swipeableProps} className="menu-layout__swipeable">
|
/>
|
||||||
<div className="menu-layout__swipeable-inner">
|
|
||||||
<AsideMenu selectedServer={selectedServer} showOnMobile={sidebarVisible} />
|
|
||||||
<div className="menu-layout__container" onClick={() => hideSidebar()}>
|
|
||||||
<div className="container-xl">
|
|
||||||
<Routes>
|
|
||||||
<Route index element={<Navigate replace to="overview" />} />
|
|
||||||
<Route path="/overview" element={<Overview />} />
|
|
||||||
<Route path="/list-short-urls/:page" element={<ShortUrlsList />} />
|
|
||||||
<Route path="/create-short-url" element={<CreateShortUrl />} />
|
|
||||||
<Route path="/short-code/:shortCode/visits/*" element={<ShortUrlVisits />} />
|
|
||||||
<Route path="/short-code/:shortCode/edit" element={<EditShortUrl />} />
|
|
||||||
<Route path="/tag/:tag/visits/*" element={<TagVisits />} />
|
|
||||||
{addDomainVisitsRoute && <Route path="/domain/:domain/visits/*" element={<DomainVisits />} />}
|
|
||||||
<Route path="/orphan-visits/*" element={<OrphanVisits />} />
|
|
||||||
{addNonOrphanVisitsRoute && <Route path="/non-orphan-visits/*" element={<NonOrphanVisits />} />}
|
|
||||||
<Route path="/manage-tags" element={<TagsList />} />
|
|
||||||
<Route path="/manage-domains" element={<ManageDomains />} />
|
|
||||||
<Route
|
|
||||||
path="*"
|
|
||||||
element={<NotFound to={`/server/${selectedServer.id}/list-short-urls/1`}>List short URLs</NotFound>}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}, ServerError);
|
}, ServerError);
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
// TODO This is only used for some components to have extra paddings/styles if existing section has a side menu
|
||||||
|
// Now that's basically the route which renders ShlinkWebComponent, so maybe there's some way to re-think this
|
||||||
|
// logic, and perhaps get rid of a reducer just for that
|
||||||
|
|
||||||
export interface Sidebar {
|
export interface Sidebar {
|
||||||
sidebarPresent: boolean;
|
sidebarPresent: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,22 +31,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.decorator('Home', withoutSelectedServer);
|
bottle.decorator('Home', withoutSelectedServer);
|
||||||
bottle.decorator('Home', connect(['servers'], ['resetSelectedServer']));
|
bottle.decorator('Home', connect(['servers'], ['resetSelectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory(
|
bottle.serviceFactory('MenuLayout', MenuLayout, 'ServerError', 'ShlinkWebComponent');
|
||||||
'MenuLayout',
|
|
||||||
MenuLayout,
|
|
||||||
'TagsList',
|
|
||||||
'ShortUrlsList',
|
|
||||||
'CreateShortUrl',
|
|
||||||
'ShortUrlVisits',
|
|
||||||
'TagVisits',
|
|
||||||
'DomainVisits',
|
|
||||||
'OrphanVisits',
|
|
||||||
'NonOrphanVisits',
|
|
||||||
'ServerError',
|
|
||||||
'Overview',
|
|
||||||
'EditShortUrl',
|
|
||||||
'ManageDomains',
|
|
||||||
);
|
|
||||||
bottle.decorator('MenuLayout', connect(['selectedServer'], ['selectServer', 'sidebarPresent', 'sidebarNotPresent']));
|
bottle.decorator('MenuLayout', connect(['selectedServer'], ['selectServer', 'sidebarPresent', 'sidebarNotPresent']));
|
||||||
|
|
||||||
bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer);
|
bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer);
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { provideServices as provideCommonServices } from '../common/services/pro
|
||||||
import { provideServices as provideMercureServices } from '../mercure/services/provideServices';
|
import { provideServices as provideMercureServices } from '../mercure/services/provideServices';
|
||||||
import { provideServices as provideServersServices } from '../servers/services/provideServices';
|
import { provideServices as provideServersServices } from '../servers/services/provideServices';
|
||||||
import { provideServices as provideSettingsServices } from '../settings/services/provideServices';
|
import { provideServices as provideSettingsServices } from '../settings/services/provideServices';
|
||||||
|
import { provideServices as provideWebComponentServices } from '../shlink-web-component/container';
|
||||||
import { provideServices as provideDomainsServices } from '../shlink-web-component/domains/services/provideServices';
|
import { provideServices as provideDomainsServices } from '../shlink-web-component/domains/services/provideServices';
|
||||||
import { provideServices as provideShortUrlsServices } from '../shlink-web-component/short-urls/services/provideServices';
|
import { provideServices as provideShortUrlsServices } from '../shlink-web-component/short-urls/services/provideServices';
|
||||||
import { provideServices as provideTagsServices } from '../shlink-web-component/tags/services/provideServices';
|
import { provideServices as provideTagsServices } from '../shlink-web-component/tags/services/provideServices';
|
||||||
|
@ -45,3 +46,6 @@ provideUtilsServices(bottle);
|
||||||
provideMercureServices(bottle);
|
provideMercureServices(bottle);
|
||||||
provideSettingsServices(bottle, connect);
|
provideSettingsServices(bottle, connect);
|
||||||
provideDomainsServices(bottle, connect);
|
provideDomainsServices(bottle, connect);
|
||||||
|
|
||||||
|
// TODO This should not be needed.
|
||||||
|
provideWebComponentServices(bottle);
|
||||||
|
|
78
src/shlink-web-component/ShlinkWebComponent.tsx
Normal file
78
src/shlink-web-component/ShlinkWebComponent.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
|
import { AsideMenu } from '../common/AsideMenu';
|
||||||
|
import { NotFound } from '../common/NotFound';
|
||||||
|
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
|
||||||
|
import type { SemVer } from '../utils/helpers/version';
|
||||||
|
import { FeaturesProvider, useFeatures } from './utils/features';
|
||||||
|
|
||||||
|
type ShlinkWebComponentProps = {
|
||||||
|
routesPrefix?: string;
|
||||||
|
serverVersion: SemVer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShlinkWebComponent = (
|
||||||
|
TagsList: FC,
|
||||||
|
ShortUrlsList: FC,
|
||||||
|
CreateShortUrl: FC,
|
||||||
|
ShortUrlVisits: FC,
|
||||||
|
TagVisits: FC,
|
||||||
|
DomainVisits: FC,
|
||||||
|
OrphanVisits: FC,
|
||||||
|
NonOrphanVisits: FC,
|
||||||
|
Overview: FC,
|
||||||
|
EditShortUrl: FC,
|
||||||
|
ManageDomains: FC,
|
||||||
|
): FC<ShlinkWebComponentProps> => ({ routesPrefix = '', serverVersion }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [sidebarVisible, toggleSidebar, showSidebar, hideSidebar] = useToggle();
|
||||||
|
useEffect(() => hideSidebar(), [location]);
|
||||||
|
|
||||||
|
const features = useFeatures(serverVersion);
|
||||||
|
const addNonOrphanVisitsRoute = features.nonOrphanVisits;
|
||||||
|
const addDomainVisitsRoute = features.domainVisits;
|
||||||
|
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
|
||||||
|
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
|
||||||
|
|
||||||
|
// TODO Check if this is already wrapped by a router, and wrap otherwise
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeaturesProvider value={features}>
|
||||||
|
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
|
||||||
|
|
||||||
|
<div {...swipeableProps} className="menu-layout__swipeable">
|
||||||
|
<div className="menu-layout__swipeable-inner">
|
||||||
|
<AsideMenu routePrefix={routesPrefix} showOnMobile={sidebarVisible} />
|
||||||
|
<div className="menu-layout__container" onClick={() => hideSidebar()}>
|
||||||
|
<div className="container-xl">
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<Navigate replace to="overview" />} />
|
||||||
|
<Route path="/overview" element={<Overview />} />
|
||||||
|
<Route path="/list-short-urls/:page" element={<ShortUrlsList />} />
|
||||||
|
<Route path="/create-short-url" element={<CreateShortUrl />} />
|
||||||
|
<Route path="/short-code/:shortCode/visits/*" element={<ShortUrlVisits />} />
|
||||||
|
<Route path="/short-code/:shortCode/edit" element={<EditShortUrl />} />
|
||||||
|
<Route path="/tag/:tag/visits/*" element={<TagVisits />} />
|
||||||
|
{addDomainVisitsRoute && <Route path="/domain/:domain/visits/*" element={<DomainVisits />} />}
|
||||||
|
<Route path="/orphan-visits/*" element={<OrphanVisits />} />
|
||||||
|
{addNonOrphanVisitsRoute && <Route path="/non-orphan-visits/*" element={<NonOrphanVisits />} />}
|
||||||
|
<Route path="/manage-tags" element={<TagsList />} />
|
||||||
|
<Route path="/manage-domains" element={<ManageDomains />} />
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={<NotFound to={`${routesPrefix}/list-short-urls/1`}>List short URLs</NotFound>}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FeaturesProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShlinkWebComponentType = ReturnType<typeof ShlinkWebComponent>;
|
22
src/shlink-web-component/container/index.ts
Normal file
22
src/shlink-web-component/container/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import type Bottle from 'bottlejs';
|
||||||
|
import { ShlinkWebComponent } from '../ShlinkWebComponent';
|
||||||
|
|
||||||
|
// TODO Build sub-container
|
||||||
|
|
||||||
|
export const provideServices = (bottle: Bottle) => {
|
||||||
|
bottle.serviceFactory(
|
||||||
|
'ShlinkWebComponent',
|
||||||
|
ShlinkWebComponent,
|
||||||
|
'TagsList',
|
||||||
|
'ShortUrlsList',
|
||||||
|
'CreateShortUrl',
|
||||||
|
'ShortUrlVisits',
|
||||||
|
'TagVisits',
|
||||||
|
'DomainVisits',
|
||||||
|
'OrphanVisits',
|
||||||
|
'NonOrphanVisits',
|
||||||
|
'Overview',
|
||||||
|
'EditShortUrl',
|
||||||
|
'ManageDomains',
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,9 +5,9 @@ import { Link } from 'react-router-dom';
|
||||||
import { DropdownItem } from 'reactstrap';
|
import { DropdownItem } from 'reactstrap';
|
||||||
import type { SelectedServer } from '../../../servers/data';
|
import type { SelectedServer } from '../../../servers/data';
|
||||||
import { getServerId } from '../../../servers/data';
|
import { getServerId } from '../../../servers/data';
|
||||||
import { useFeature } from '../../../utils/helpers/features';
|
|
||||||
import { useToggle } from '../../../utils/helpers/hooks';
|
import { useToggle } from '../../../utils/helpers/hooks';
|
||||||
import { RowDropdownBtn } from '../../../utils/RowDropdownBtn';
|
import { RowDropdownBtn } from '../../../utils/RowDropdownBtn';
|
||||||
|
import { useFeature } from '../../utils/features';
|
||||||
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||||
import type { Domain } from '../data';
|
import type { Domain } from '../data';
|
||||||
import type { EditDomainRedirects } from '../reducers/domainRedirects';
|
import type { EditDomainRedirects } from '../reducers/domainRedirects';
|
||||||
|
@ -22,8 +22,8 @@ interface DomainDropdownProps {
|
||||||
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects, selectedServer }) => {
|
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects, selectedServer }) => {
|
||||||
const [isModalOpen, toggleModal] = useToggle();
|
const [isModalOpen, toggleModal] = useToggle();
|
||||||
const { isDefault } = domain;
|
const { isDefault } = domain;
|
||||||
const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition', selectedServer);
|
const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition');
|
||||||
const withVisits = useFeature('domainVisits', selectedServer);
|
const withVisits = useFeature('domainVisits');
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
1
src/shlink-web-component/index.ts
Normal file
1
src/shlink-web-component/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './ShlinkWebComponent';
|
|
@ -10,13 +10,13 @@ import { getServerId } from '../../servers/data';
|
||||||
import { HighlightCard } from '../../servers/helpers/HighlightCard';
|
import { HighlightCard } from '../../servers/helpers/HighlightCard';
|
||||||
import { VisitsHighlightCard } from '../../servers/helpers/VisitsHighlightCard';
|
import { VisitsHighlightCard } from '../../servers/helpers/VisitsHighlightCard';
|
||||||
import type { Settings } from '../../settings/reducers/settings';
|
import type { Settings } from '../../settings/reducers/settings';
|
||||||
import { useFeature } from '../../utils/helpers/features';
|
|
||||||
import { prettify } from '../../utils/helpers/numbers';
|
import { prettify } from '../../utils/helpers/numbers';
|
||||||
import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
|
import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
|
||||||
import type { ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
|
import type { ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
|
||||||
import { ITEMS_IN_OVERVIEW_PAGE } from '../short-urls/reducers/shortUrlsList';
|
import { ITEMS_IN_OVERVIEW_PAGE } from '../short-urls/reducers/shortUrlsList';
|
||||||
import type { ShortUrlsTableType } from '../short-urls/ShortUrlsTable';
|
import type { ShortUrlsTableType } from '../short-urls/ShortUrlsTable';
|
||||||
import type { TagsList } from '../tags/reducers/tagsList';
|
import type { TagsList } from '../tags/reducers/tagsList';
|
||||||
|
import { useFeature } from '../utils/features';
|
||||||
import type { VisitsOverview } from '../visits/reducers/visitsOverview';
|
import type { VisitsOverview } from '../visits/reducers/visitsOverview';
|
||||||
|
|
||||||
interface OverviewConnectProps {
|
interface OverviewConnectProps {
|
||||||
|
@ -47,7 +47,7 @@ export const Overview = (
|
||||||
const { loading: loadingTags } = tagsList;
|
const { loading: loadingTags } = tagsList;
|
||||||
const { loading: loadingVisits, nonOrphanVisits, orphanVisits } = visitsOverview;
|
const { loading: loadingVisits, nonOrphanVisits, orphanVisits } = visitsOverview;
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
const linkToNonOrphanVisits = useFeature('nonOrphanVisits', selectedServer);
|
const linkToNonOrphanVisits = useFeature('nonOrphanVisits');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { SelectedServer } from '../../servers/data';
|
|
||||||
import type { Settings, ShortUrlCreationSettings } from '../../settings/reducers/settings';
|
import type { Settings, ShortUrlCreationSettings } from '../../settings/reducers/settings';
|
||||||
import type { ShortUrlData } from './data';
|
import type { ShortUrlData } from './data';
|
||||||
import type { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
|
import type { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
|
||||||
|
@ -14,7 +13,6 @@ export interface CreateShortUrlProps {
|
||||||
interface CreateShortUrlConnectProps extends CreateShortUrlProps {
|
interface CreateShortUrlConnectProps extends CreateShortUrlProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
shortUrlCreation: ShortUrlCreation;
|
shortUrlCreation: ShortUrlCreation;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
createShortUrl: (data: ShortUrlData) => Promise<void>;
|
createShortUrl: (data: ShortUrlData) => Promise<void>;
|
||||||
resetCreateShortUrl: () => void;
|
resetCreateShortUrl: () => void;
|
||||||
}
|
}
|
||||||
|
@ -41,7 +39,6 @@ export const CreateShortUrl = (
|
||||||
createShortUrl,
|
createShortUrl,
|
||||||
shortUrlCreation,
|
shortUrlCreation,
|
||||||
resetCreateShortUrl,
|
resetCreateShortUrl,
|
||||||
selectedServer,
|
|
||||||
basicMode = false,
|
basicMode = false,
|
||||||
settings: { shortUrlCreation: shortUrlCreationSettings },
|
settings: { shortUrlCreation: shortUrlCreationSettings },
|
||||||
}: CreateShortUrlConnectProps) => {
|
}: CreateShortUrlConnectProps) => {
|
||||||
|
@ -52,7 +49,6 @@ export const CreateShortUrl = (
|
||||||
<ShortUrlForm
|
<ShortUrlForm
|
||||||
initialState={initialState}
|
initialState={initialState}
|
||||||
saving={shortUrlCreation.saving}
|
saving={shortUrlCreation.saving}
|
||||||
selectedServer={selectedServer}
|
|
||||||
mode={basicMode ? 'create-basic' : 'create'}
|
mode={basicMode ? 'create-basic' : 'create'}
|
||||||
onSave={async (data: ShortUrlData) => {
|
onSave={async (data: ShortUrlData) => {
|
||||||
resetCreateShortUrl();
|
resetCreateShortUrl();
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { ExternalLink } from 'react-external-link';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { Button, Card } from 'reactstrap';
|
import { Button, Card } from 'reactstrap';
|
||||||
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
import type { SelectedServer } from '../../servers/data';
|
|
||||||
import type { Settings } from '../../settings/reducers/settings';
|
import type { Settings } from '../../settings/reducers/settings';
|
||||||
import { useGoBack } from '../../utils/helpers/hooks';
|
import { useGoBack } from '../../utils/helpers/hooks';
|
||||||
import { parseQuery } from '../../utils/helpers/query';
|
import { parseQuery } from '../../utils/helpers/query';
|
||||||
|
@ -20,7 +19,6 @@ import type { ShortUrlFormProps } from './ShortUrlForm';
|
||||||
|
|
||||||
interface EditShortUrlConnectProps {
|
interface EditShortUrlConnectProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
shortUrlDetail: ShortUrlDetail;
|
shortUrlDetail: ShortUrlDetail;
|
||||||
shortUrlEdition: ShortUrlEdition;
|
shortUrlEdition: ShortUrlEdition;
|
||||||
getShortUrlDetail: (shortUrl: ShortUrlIdentifier) => void;
|
getShortUrlDetail: (shortUrl: ShortUrlIdentifier) => void;
|
||||||
|
@ -29,7 +27,6 @@ interface EditShortUrlConnectProps {
|
||||||
|
|
||||||
export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
|
export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
|
||||||
settings: { shortUrlCreation: shortUrlCreationSettings },
|
settings: { shortUrlCreation: shortUrlCreationSettings },
|
||||||
selectedServer,
|
|
||||||
shortUrlDetail,
|
shortUrlDetail,
|
||||||
getShortUrlDetail,
|
getShortUrlDetail,
|
||||||
shortUrlEdition,
|
shortUrlEdition,
|
||||||
|
@ -80,7 +77,6 @@ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
|
||||||
<ShortUrlForm
|
<ShortUrlForm
|
||||||
initialState={initialState}
|
initialState={initialState}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
selectedServer={selectedServer}
|
|
||||||
mode="edit"
|
mode="edit"
|
||||||
onSave={async (shortUrlData) => {
|
onSave={async (shortUrlData) => {
|
||||||
if (!shortUrl) {
|
if (!shortUrl) {
|
||||||
|
|
|
@ -8,17 +8,16 @@ import type { ChangeEvent, FC } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
||||||
import type { InputType } from 'reactstrap/types/lib/Input';
|
import type { InputType } from 'reactstrap/types/lib/Input';
|
||||||
import type { SelectedServer } from '../../servers/data';
|
|
||||||
import { Checkbox } from '../../utils/Checkbox';
|
import { Checkbox } from '../../utils/Checkbox';
|
||||||
import type { DateTimeInputProps } from '../../utils/dates/DateTimeInput';
|
import type { DateTimeInputProps } from '../../utils/dates/DateTimeInput';
|
||||||
import { DateTimeInput } from '../../utils/dates/DateTimeInput';
|
import { DateTimeInput } from '../../utils/dates/DateTimeInput';
|
||||||
import { formatIsoDate } from '../../utils/helpers/date';
|
import { formatIsoDate } from '../../utils/helpers/date';
|
||||||
import { useFeature } from '../../utils/helpers/features';
|
|
||||||
import { IconInput } from '../../utils/IconInput';
|
import { IconInput } from '../../utils/IconInput';
|
||||||
import { SimpleCard } from '../../utils/SimpleCard';
|
import { SimpleCard } from '../../utils/SimpleCard';
|
||||||
import { handleEventPreventingDefault, hasValue } from '../../utils/utils';
|
import { handleEventPreventingDefault, hasValue } from '../../utils/utils';
|
||||||
import type { DomainSelectorProps } from '../domains/DomainSelector';
|
import type { DomainSelectorProps } from '../domains/DomainSelector';
|
||||||
import type { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
import type { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
||||||
|
import { useFeature } from '../utils/features';
|
||||||
import type { DeviceLongUrls, ShortUrlData } from './data';
|
import type { DeviceLongUrls, ShortUrlData } from './data';
|
||||||
import { ShortUrlFormCheckboxGroup } from './helpers/ShortUrlFormCheckboxGroup';
|
import { ShortUrlFormCheckboxGroup } from './helpers/ShortUrlFormCheckboxGroup';
|
||||||
import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon';
|
import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon';
|
||||||
|
@ -34,7 +33,6 @@ export interface ShortUrlFormProps {
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
initialState: ShortUrlData;
|
initialState: ShortUrlData;
|
||||||
onSave: (shortUrlData: ShortUrlData) => Promise<unknown>;
|
onSave: (shortUrlData: ShortUrlData) => Promise<unknown>;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
||||||
|
@ -43,10 +41,10 @@ const toDate = (date?: string | Date): Date | undefined => (typeof date === 'str
|
||||||
export const ShortUrlForm = (
|
export const ShortUrlForm = (
|
||||||
TagsSelector: FC<TagsSelectorProps>,
|
TagsSelector: FC<TagsSelectorProps>,
|
||||||
DomainSelector: FC<DomainSelectorProps>,
|
DomainSelector: FC<DomainSelectorProps>,
|
||||||
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer }) => {
|
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState }) => {
|
||||||
const [shortUrlData, setShortUrlData] = useState(initialState);
|
const [shortUrlData, setShortUrlData] = useState(initialState);
|
||||||
const reset = () => setShortUrlData(initialState);
|
const reset = () => setShortUrlData(initialState);
|
||||||
const supportsDeviceLongUrls = useFeature('deviceLongUrls', selectedServer);
|
const supportsDeviceLongUrls = useFeature('deviceLongUrls');
|
||||||
|
|
||||||
const isEdit = mode === 'edit';
|
const isEdit = mode === 'edit';
|
||||||
const isBasicMode = mode === 'create-basic';
|
const isBasicMode = mode === 'create-basic';
|
||||||
|
@ -136,7 +134,7 @@ export const ShortUrlForm = (
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const showForwardQueryControl = useFeature('forwardQuery', selectedServer);
|
const showForwardQueryControl = useFeature('forwardQuery');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form name="shortUrlForm" className="short-url-form" onSubmit={submit}>
|
<form name="shortUrlForm" className="short-url-form" onSubmit={submit}>
|
||||||
|
|
|
@ -4,17 +4,16 @@ import classNames from 'classnames';
|
||||||
import { isEmpty, pipe } from 'ramda';
|
import { isEmpty, pipe } from 'ramda';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Button, InputGroup, Row, UncontrolledTooltip } from 'reactstrap';
|
import { Button, InputGroup, Row, UncontrolledTooltip } from 'reactstrap';
|
||||||
import type { SelectedServer } from '../../servers/data';
|
|
||||||
import type { Settings } from '../../settings/reducers/settings';
|
import type { Settings } from '../../settings/reducers/settings';
|
||||||
import { DateRangeSelector } from '../../utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../../utils/dates/DateRangeSelector';
|
||||||
import { formatIsoDate } from '../../utils/helpers/date';
|
import { formatIsoDate } from '../../utils/helpers/date';
|
||||||
import type { DateRange } from '../../utils/helpers/dateIntervals';
|
import type { DateRange } from '../../utils/helpers/dateIntervals';
|
||||||
import { datesToDateRange } from '../../utils/helpers/dateIntervals';
|
import { datesToDateRange } from '../../utils/helpers/dateIntervals';
|
||||||
import { useFeature } from '../../utils/helpers/features';
|
|
||||||
import type { OrderDir } from '../../utils/helpers/ordering';
|
import type { OrderDir } from '../../utils/helpers/ordering';
|
||||||
import { OrderingDropdown } from '../../utils/OrderingDropdown';
|
import { OrderingDropdown } from '../../utils/OrderingDropdown';
|
||||||
import { SearchField } from '../../utils/SearchField';
|
import { SearchField } from '../../utils/SearchField';
|
||||||
import type { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
import type { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
||||||
|
import { useFeature } from '../utils/features';
|
||||||
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
||||||
import { SHORT_URLS_ORDERABLE_FIELDS } from './data';
|
import { SHORT_URLS_ORDERABLE_FIELDS } from './data';
|
||||||
import type { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
|
import type { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
|
||||||
|
@ -23,7 +22,6 @@ import { ShortUrlsFilterDropdown } from './helpers/ShortUrlsFilterDropdown';
|
||||||
import './ShortUrlsFilteringBar.scss';
|
import './ShortUrlsFilteringBar.scss';
|
||||||
|
|
||||||
interface ShortUrlsFilteringProps {
|
interface ShortUrlsFilteringProps {
|
||||||
selectedServer: SelectedServer;
|
|
||||||
order: ShortUrlsOrder;
|
order: ShortUrlsOrder;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
handleOrderBy: (orderField?: ShortUrlsOrderableFields, orderDir?: OrderDir) => void;
|
handleOrderBy: (orderField?: ShortUrlsOrderableFields, orderDir?: OrderDir) => void;
|
||||||
|
@ -34,7 +32,7 @@ interface ShortUrlsFilteringProps {
|
||||||
export const ShortUrlsFilteringBar = (
|
export const ShortUrlsFilteringBar = (
|
||||||
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
||||||
TagsSelector: FC<TagsSelectorProps>,
|
TagsSelector: FC<TagsSelectorProps>,
|
||||||
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy, settings }) => {
|
): FC<ShortUrlsFilteringProps> => ({ className, shortUrlsAmount, order, handleOrderBy, settings }) => {
|
||||||
const [filter, toFirstPage] = useShortUrlsQuery();
|
const [filter, toFirstPage] = useShortUrlsQuery();
|
||||||
const {
|
const {
|
||||||
search,
|
search,
|
||||||
|
@ -46,7 +44,7 @@ export const ShortUrlsFilteringBar = (
|
||||||
excludePastValidUntil,
|
excludePastValidUntil,
|
||||||
tagsMode = 'any',
|
tagsMode = 'any',
|
||||||
} = filter;
|
} = filter;
|
||||||
const supportsDisabledFiltering = useFeature('filterDisabledUrls', selectedServer);
|
const supportsDisabledFiltering = useFeature('filterDisabledUrls');
|
||||||
|
|
||||||
const setDates = pipe(
|
const setDates = pipe(
|
||||||
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
|
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
|
||||||
|
@ -60,7 +58,7 @@ export const ShortUrlsFilteringBar = (
|
||||||
(searchTerm) => toFirstPage({ search: searchTerm }),
|
(searchTerm) => toFirstPage({ search: searchTerm }),
|
||||||
);
|
);
|
||||||
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
|
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
|
||||||
const canChangeTagsMode = useFeature('allTagsFiltering', selectedServer);
|
const canChangeTagsMode = useFeature('allTagsFiltering');
|
||||||
const toggleTagsMode = pipe(
|
const toggleTagsMode = pipe(
|
||||||
() => (tagsMode === 'any' ? 'all' : 'any'),
|
() => (tagsMode === 'any' ? 'all' : 'any'),
|
||||||
(mode) => toFirstPage({ tagsMode: mode }),
|
(mode) => toFirstPage({ tagsMode: mode }),
|
||||||
|
|
|
@ -9,10 +9,10 @@ import type { SelectedServer } from '../../servers/data';
|
||||||
import { getServerId } from '../../servers/data';
|
import { getServerId } from '../../servers/data';
|
||||||
import type { Settings } from '../../settings/reducers/settings';
|
import type { Settings } from '../../settings/reducers/settings';
|
||||||
import { DEFAULT_SHORT_URLS_ORDERING } from '../../settings/reducers/settings';
|
import { DEFAULT_SHORT_URLS_ORDERING } from '../../settings/reducers/settings';
|
||||||
import { useFeature } from '../../utils/helpers/features';
|
|
||||||
import type { OrderDir } from '../../utils/helpers/ordering';
|
import type { OrderDir } from '../../utils/helpers/ordering';
|
||||||
import { determineOrderDir } from '../../utils/helpers/ordering';
|
import { determineOrderDir } from '../../utils/helpers/ordering';
|
||||||
import { TableOrderIcon } from '../../utils/table/TableOrderIcon';
|
import { TableOrderIcon } from '../../utils/table/TableOrderIcon';
|
||||||
|
import { useFeature } from '../utils/features';
|
||||||
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
||||||
import { useShortUrlsQuery } from './helpers/hooks';
|
import { useShortUrlsQuery } from './helpers/hooks';
|
||||||
import { Paginator } from './Paginator';
|
import { Paginator } from './Paginator';
|
||||||
|
@ -52,7 +52,7 @@ export const ShortUrlsList = (
|
||||||
);
|
);
|
||||||
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
||||||
const doExcludeBots = excludeBots ?? settings.visits?.excludeBots;
|
const doExcludeBots = excludeBots ?? settings.visits?.excludeBots;
|
||||||
const supportsExcludingBots = useFeature('excludeBotsOnShortUrls', selectedServer);
|
const supportsExcludingBots = useFeature('excludeBotsOnShortUrls');
|
||||||
const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => {
|
const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => {
|
||||||
toFirstPage({ orderBy: { field, dir } });
|
toFirstPage({ orderBy: { field, dir } });
|
||||||
setActualOrderBy({ field, dir });
|
setActualOrderBy({ field, dir });
|
||||||
|
@ -101,7 +101,6 @@ export const ShortUrlsList = (
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShortUrlsFilteringBar
|
<ShortUrlsFilteringBar
|
||||||
selectedServer={selectedServer}
|
|
||||||
shortUrlsAmount={shortUrlsList.shortUrls?.pagination.totalItems}
|
shortUrlsAmount={shortUrlsList.shortUrls?.pagination.totalItems}
|
||||||
order={actualOrderBy}
|
order={actualOrderBy}
|
||||||
handleOrderBy={handleOrderBy}
|
handleOrderBy={handleOrderBy}
|
||||||
|
|
|
@ -4,28 +4,23 @@ import { useMemo, useState } from 'react';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { Button, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
|
import { Button, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
|
||||||
import type { ImageDownloader } from '../../../common/services/ImageDownloader';
|
import type { ImageDownloader } from '../../../common/services/ImageDownloader';
|
||||||
import type { SelectedServer } from '../../../servers/data';
|
|
||||||
import { CopyToClipboardIcon } from '../../../utils/CopyToClipboardIcon';
|
import { CopyToClipboardIcon } from '../../../utils/CopyToClipboardIcon';
|
||||||
import { useFeature } from '../../../utils/helpers/features';
|
|
||||||
import type { QrCodeFormat, QrErrorCorrection } from '../../../utils/helpers/qrCodes';
|
import type { QrCodeFormat, QrErrorCorrection } from '../../../utils/helpers/qrCodes';
|
||||||
import { buildQrCodeUrl } from '../../../utils/helpers/qrCodes';
|
import { buildQrCodeUrl } from '../../../utils/helpers/qrCodes';
|
||||||
|
import { useFeature } from '../../utils/features';
|
||||||
import type { ShortUrlModalProps } from '../data';
|
import type { ShortUrlModalProps } from '../data';
|
||||||
import { QrErrorCorrectionDropdown } from './qr-codes/QrErrorCorrectionDropdown';
|
import { QrErrorCorrectionDropdown } from './qr-codes/QrErrorCorrectionDropdown';
|
||||||
import { QrFormatDropdown } from './qr-codes/QrFormatDropdown';
|
import { QrFormatDropdown } from './qr-codes/QrFormatDropdown';
|
||||||
import './QrCodeModal.scss';
|
import './QrCodeModal.scss';
|
||||||
|
|
||||||
interface QrCodeModalConnectProps extends ShortUrlModalProps {
|
|
||||||
selectedServer: SelectedServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
export const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
||||||
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps,
|
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen }: ShortUrlModalProps,
|
||||||
) => {
|
) => {
|
||||||
const [size, setSize] = useState(300);
|
const [size, setSize] = useState(300);
|
||||||
const [margin, setMargin] = useState(0);
|
const [margin, setMargin] = useState(0);
|
||||||
const [format, setFormat] = useState<QrCodeFormat>('png');
|
const [format, setFormat] = useState<QrCodeFormat>('png');
|
||||||
const [errorCorrection, setErrorCorrection] = useState<QrErrorCorrection>('L');
|
const [errorCorrection, setErrorCorrection] = useState<QrErrorCorrection>('L');
|
||||||
const displayDownloadBtn = useFeature('nonRestCors', selectedServer);
|
const displayDownloadBtn = useFeature('nonRestCors');
|
||||||
const qrCodeUrl = useMemo(
|
const qrCodeUrl = useMemo(
|
||||||
() => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }),
|
() => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }),
|
||||||
[shortUrl, size, format, margin, errorCorrection],
|
[shortUrl, size, format, margin, errorCorrection],
|
||||||
|
@ -46,7 +41,7 @@ export const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Row>
|
<Row>
|
||||||
<FormGroup className="d-grid col-md-4">
|
<FormGroup className="d-grid col-md-6">
|
||||||
<label>Size: {size}px</label>
|
<label>Size: {size}px</label>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
|
@ -58,7 +53,7 @@ export const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
||||||
onChange={(e) => setSize(Number(e.target.value))}
|
onChange={(e) => setSize(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup className="d-grid col-md-4">
|
<FormGroup className="d-grid col-md-6">
|
||||||
<label htmlFor="marginControl">Margin: {margin}px</label>
|
<label htmlFor="marginControl">Margin: {margin}px</label>
|
||||||
<input
|
<input
|
||||||
id="marginControl"
|
id="marginControl"
|
||||||
|
@ -71,7 +66,7 @@ export const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
||||||
onChange={(e) => setMargin(Number(e.target.value))}
|
onChange={(e) => setMargin(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup className="d-grid col-md-4">
|
<FormGroup className="d-grid col-md-6">
|
||||||
<QrFormatDropdown format={format} setFormat={setFormat} />
|
<QrFormatDropdown format={format} setFormat={setFormat} />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup className="col-md-6">
|
<FormGroup className="col-md-6">
|
||||||
|
|
|
@ -39,12 +39,12 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'ShortUrlForm', 'CreateShortUrlResult');
|
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'ShortUrlForm', 'CreateShortUrlResult');
|
||||||
bottle.decorator(
|
bottle.decorator(
|
||||||
'CreateShortUrl',
|
'CreateShortUrl',
|
||||||
connect(['shortUrlCreation', 'selectedServer', 'settings'], ['createShortUrl', 'resetCreateShortUrl']),
|
connect(['shortUrlCreation', 'settings'], ['createShortUrl', 'resetCreateShortUrl']),
|
||||||
);
|
);
|
||||||
|
|
||||||
bottle.serviceFactory('EditShortUrl', EditShortUrl, 'ShortUrlForm');
|
bottle.serviceFactory('EditShortUrl', EditShortUrl, 'ShortUrlForm');
|
||||||
bottle.decorator('EditShortUrl', connect(
|
bottle.decorator('EditShortUrl', connect(
|
||||||
['shortUrlDetail', 'shortUrlEdition', 'selectedServer', 'settings'],
|
['shortUrlDetail', 'shortUrlEdition', 'settings'],
|
||||||
['getShortUrlDetail', 'editShortUrl'],
|
['getShortUrlDetail', 'editShortUrl'],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -55,8 +55,6 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader');
|
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader');
|
||||||
bottle.decorator('QrCodeModal', connect(['selectedServer']));
|
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ExportShortUrlsBtn', 'TagsSelector');
|
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ExportShortUrlsBtn', 'TagsSelector');
|
||||||
|
|
||||||
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'buildShlinkApiClient', 'ReportExporter');
|
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'buildShlinkApiClient', 'ReportExporter');
|
||||||
|
|
|
@ -4,9 +4,10 @@ import type { ShlinkApiClientBuilder } from '../../../api/services/ShlinkApiClie
|
||||||
import type { ShlinkTags } from '../../../api/types';
|
import type { ShlinkTags } from '../../../api/types';
|
||||||
import type { ProblemDetailsError } from '../../../api/types/errors';
|
import type { ProblemDetailsError } from '../../../api/types/errors';
|
||||||
import { parseApiError } from '../../../api/utils';
|
import { parseApiError } from '../../../api/utils';
|
||||||
import { supportedFeatures } from '../../../utils/helpers/features';
|
import { isReachableServer } from '../../../servers/data';
|
||||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||||
import type { createShortUrl } from '../../short-urls/reducers/shortUrlCreation';
|
import type { createShortUrl } from '../../short-urls/reducers/shortUrlCreation';
|
||||||
|
import { isFeatureEnabledForVersion } from '../../utils/features';
|
||||||
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
||||||
import type { CreateVisit } from '../../visits/types';
|
import type { CreateVisit } from '../../visits/types';
|
||||||
import type { TagStats } from '../data';
|
import type { TagStats } from '../data';
|
||||||
|
@ -94,7 +95,9 @@ export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = t
|
||||||
|
|
||||||
const { listTags: shlinkListTags, tagsStats } = buildShlinkApiClient(getState);
|
const { listTags: shlinkListTags, tagsStats } = buildShlinkApiClient(getState);
|
||||||
const { tags, stats }: ShlinkTags = await (
|
const { tags, stats }: ShlinkTags = await (
|
||||||
supportedFeatures.tagsStats(selectedServer) ? tagsStats() : shlinkListTags()
|
isReachableServer(selectedServer) && isFeatureEnabledForVersion('tagsStats', selectedServer.version)
|
||||||
|
? tagsStats()
|
||||||
|
: shlinkListTags()
|
||||||
);
|
);
|
||||||
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, ...rest }) => {
|
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, ...rest }) => {
|
||||||
acc[tag] = rest;
|
acc[tag] = rest;
|
||||||
|
|
50
src/shlink-web-component/utils/features.ts
Normal file
50
src/shlink-web-component/utils/features.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { createContext, useContext, useMemo } from 'react';
|
||||||
|
import type { SemVer } from '../../utils/helpers/version';
|
||||||
|
import { versionMatch } from '../../utils/helpers/version';
|
||||||
|
|
||||||
|
const supportedFeatures = {
|
||||||
|
forwardQuery: '2.9.0',
|
||||||
|
nonRestCors: '2.9.0',
|
||||||
|
defaultDomainRedirectsEdition: '2.10.0',
|
||||||
|
nonOrphanVisits: '3.0.0',
|
||||||
|
allTagsFiltering: '3.0.0',
|
||||||
|
tagsStats: '3.0.0',
|
||||||
|
domainVisits: '3.1.0',
|
||||||
|
excludeBotsOnShortUrls: '3.4.0',
|
||||||
|
filterDisabledUrls: '3.4.0',
|
||||||
|
deviceLongUrls: '3.5.0',
|
||||||
|
} as const satisfies Record<string, SemVer>;
|
||||||
|
|
||||||
|
Object.freeze(supportedFeatures);
|
||||||
|
|
||||||
|
export type Feature = keyof typeof supportedFeatures;
|
||||||
|
|
||||||
|
export const isFeatureEnabledForVersion = (feature: Feature, serverVersion: SemVer): boolean =>
|
||||||
|
versionMatch(serverVersion, { minVersion: supportedFeatures[feature] });
|
||||||
|
|
||||||
|
const getFeaturesForVersion = (serverVersion: SemVer): Record<Feature, boolean> => ({
|
||||||
|
forwardQuery: isFeatureEnabledForVersion('forwardQuery', serverVersion),
|
||||||
|
nonRestCors: isFeatureEnabledForVersion('nonRestCors', serverVersion),
|
||||||
|
defaultDomainRedirectsEdition: isFeatureEnabledForVersion('defaultDomainRedirectsEdition', serverVersion),
|
||||||
|
nonOrphanVisits: isFeatureEnabledForVersion('nonOrphanVisits', serverVersion),
|
||||||
|
allTagsFiltering: isFeatureEnabledForVersion('allTagsFiltering', serverVersion),
|
||||||
|
tagsStats: isFeatureEnabledForVersion('tagsStats', serverVersion),
|
||||||
|
domainVisits: isFeatureEnabledForVersion('domainVisits', serverVersion),
|
||||||
|
excludeBotsOnShortUrls: isFeatureEnabledForVersion('excludeBotsOnShortUrls', serverVersion),
|
||||||
|
filterDisabledUrls: isFeatureEnabledForVersion('filterDisabledUrls', serverVersion),
|
||||||
|
deviceLongUrls: isFeatureEnabledForVersion('deviceLongUrls', serverVersion),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useFeatures = (serverVersion: SemVer) => useMemo(
|
||||||
|
() => getFeaturesForVersion(serverVersion),
|
||||||
|
[serverVersion],
|
||||||
|
);
|
||||||
|
|
||||||
|
const FeaturesContext = createContext(getFeaturesForVersion('0.0.0'));
|
||||||
|
|
||||||
|
export const FeaturesProvider = FeaturesContext.Provider;
|
||||||
|
|
||||||
|
export const useFeature = (feature: Feature) => {
|
||||||
|
const features = useContext(FeaturesContext);
|
||||||
|
return features[feature];
|
||||||
|
};
|
|
@ -1,31 +0,0 @@
|
||||||
import { useMemo } from 'react';
|
|
||||||
import type { SelectedServer } from '../../servers/data';
|
|
||||||
import { isReachableServer } from '../../servers/data';
|
|
||||||
import { selectServer } from '../../servers/reducers/selectedServer';
|
|
||||||
import type { SemVerPattern } from './version';
|
|
||||||
import { versionMatch } from './version';
|
|
||||||
|
|
||||||
const matchesMinVersion = (minVersion: SemVerPattern) => (selectedServer: SelectedServer): boolean =>
|
|
||||||
isReachableServer(selectedServer) && versionMatch(selectedServer.version, { minVersion });
|
|
||||||
|
|
||||||
export const supportedFeatures = {
|
|
||||||
forwardQuery: matchesMinVersion('2.9.0'),
|
|
||||||
nonRestCors: matchesMinVersion('2.9.0'),
|
|
||||||
defaultDomainRedirectsEdition: matchesMinVersion('2.10.0'),
|
|
||||||
nonOrphanVisits: matchesMinVersion('3.0.0'),
|
|
||||||
allTagsFiltering: matchesMinVersion('3.0.0'),
|
|
||||||
tagsStats: matchesMinVersion('3.0.0'),
|
|
||||||
domainVisits: matchesMinVersion('3.1.0'),
|
|
||||||
excludeBotsOnShortUrls: matchesMinVersion('3.4.0'),
|
|
||||||
filterDisabledUrls: matchesMinVersion('3.4.0'),
|
|
||||||
deviceLongUrls: matchesMinVersion('3.5.0'),
|
|
||||||
} as const satisfies Record<string, ReturnType<typeof matchesMinVersion>>;
|
|
||||||
|
|
||||||
Object.freeze(supportedFeatures);
|
|
||||||
|
|
||||||
type Features = keyof typeof supportedFeatures;
|
|
||||||
|
|
||||||
export const useFeature = (feature: Features, selectedServer: SelectedServer) => useMemo(
|
|
||||||
() => supportedFeatures[feature](selectedServer),
|
|
||||||
[feature, selectServer],
|
|
||||||
);
|
|
Loading…
Reference in a new issue