mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-09 01:37:24 +03:00
Updated to airbnb coding styles
This commit is contained in:
parent
4e9b19afd1
commit
a2df486280
239 changed files with 2210 additions and 3549 deletions
55
.eslintrc
55
.eslintrc
|
@ -1,25 +1,46 @@
|
||||||
{
|
{
|
||||||
|
"root": true,
|
||||||
"extends": [
|
"extends": [
|
||||||
"@shlinkio/js-coding-standard"
|
"airbnb",
|
||||||
|
"airbnb-typescript",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
],
|
],
|
||||||
"plugins": ["jest"],
|
|
||||||
"env": {
|
|
||||||
"jest/globals": true
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"tsconfigRootDir": ".",
|
"project": "./tsconfig.json"
|
||||||
"createDefaultProgram": true
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"process": true,
|
|
||||||
"setImmediate": true
|
|
||||||
},
|
},
|
||||||
"ignorePatterns": ["src/service*.ts"],
|
"ignorePatterns": ["src/service*.ts"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"complexity": "off",
|
"object-curly-newline": "off",
|
||||||
"import/named": "off",
|
"implicit-arrow-linebreak": "off",
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
"no-restricted-globals": "off",
|
||||||
"@typescript-eslint/no-unsafe-return": "off",
|
"default-case": "off",
|
||||||
"@typescript-eslint/no-unsafe-call": "off"
|
"max-len": ["error", { "code": 120, "ignoreComments": true, "ignoreStrings": true }],
|
||||||
}
|
"import/no-cycle": "off",
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
"import/no-extraneous-dependencies": "off",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"react/require-default-props": "off",
|
||||||
|
"react/function-component-definition": "off",
|
||||||
|
"react/no-array-index-key": "off",
|
||||||
|
"react/no-unstable-nested-components": "off",
|
||||||
|
"react/jsx-one-expression-per-line": "off",
|
||||||
|
"react/jsx-props-no-spreading": "off",
|
||||||
|
"react/jsx-no-useless-fragment": "off",
|
||||||
|
"@typescript-eslint/no-unused-expressions": "off",
|
||||||
|
"@typescript-eslint/ban-types": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/lines-between-class-members": "off"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.test.*"],
|
||||||
|
"rules": {
|
||||||
|
"prefer-promise-reject-errors": "off",
|
||||||
|
"no-param-reassign": "off",
|
||||||
|
"react/no-children-prop": "off",
|
||||||
|
"@typescript-eslint/no-shadow": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
3297
package-lock.json
generated
3297
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -66,7 +66,6 @@
|
||||||
"workbox-strategies": "^6.5.1"
|
"workbox-strategies": "^6.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@shlinkio/eslint-config-js-coding-standard": "~1.2.2",
|
|
||||||
"@stryker-mutator/core": "^5.6.1",
|
"@stryker-mutator/core": "^5.6.1",
|
||||||
"@stryker-mutator/jest-runner": "^5.6.1",
|
"@stryker-mutator/jest-runner": "^5.6.1",
|
||||||
"@stryker-mutator/typescript-checker": "^5.6.1",
|
"@stryker-mutator/typescript-checker": "^5.6.1",
|
||||||
|
@ -85,6 +84,8 @@
|
||||||
"@types/react-redux": "^7.1.23",
|
"@types/react-redux": "^7.1.23",
|
||||||
"@types/react-tag-autocomplete": "^6.1.1",
|
"@types/react-tag-autocomplete": "^6.1.1",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
||||||
|
"@typescript-eslint/parser": "^5.16.0",
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "0.6.5",
|
"@wojtekmaj/enzyme-adapter-react-17": "0.6.5",
|
||||||
"adm-zip": "^0.5.9",
|
"adm-zip": "^0.5.9",
|
||||||
"babel-jest": "^27.5.1",
|
"babel-jest": "^27.5.1",
|
||||||
|
@ -92,6 +93,8 @@
|
||||||
"dart-sass": "^1.25.0",
|
"dart-sass": "^1.25.0",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"eslint": "^7.13.0",
|
"eslint": "^7.13.0",
|
||||||
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
|
"eslint-config-airbnb-typescript": "^16.1.4",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"react-scripts": "^5.0.0",
|
"react-scripts": "^5.0.0",
|
||||||
|
|
|
@ -9,8 +9,10 @@ export interface ShlinkApiErrorProps {
|
||||||
export const ShlinkApiError = ({ errorData, fallbackMessage }: ShlinkApiErrorProps) => (
|
export const ShlinkApiError = ({ errorData, fallbackMessage }: ShlinkApiErrorProps) => (
|
||||||
<>
|
<>
|
||||||
{errorData?.detail ?? fallbackMessage}
|
{errorData?.detail ?? fallbackMessage}
|
||||||
{isInvalidArgumentError(errorData) &&
|
{isInvalidArgumentError(errorData) && (
|
||||||
<p className="mb-0">Invalid elements: [{errorData.invalidElements.join(', ')}]</p>
|
<p className="mb-0">
|
||||||
}
|
Invalid elements: [{errorData.invalidElements.join(', ')}]
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
import { stringifyQuery } from '../../utils/helpers/query';
|
import { stringifyQuery } from '../../utils/helpers/query';
|
||||||
import { orderToString } from '../../utils/helpers/ordering';
|
import { orderToString } from '../../utils/helpers/ordering';
|
||||||
|
|
||||||
const buildShlinkBaseUrl = (url: string) => url ? `${url}/rest/v2` : '';
|
const buildShlinkBaseUrl = (url: string) => (url ? `${url}/rest/v2` : '');
|
||||||
const rejectNilProps = reject(isNil);
|
const rejectNilProps = reject(isNil);
|
||||||
const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => {
|
const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => {
|
||||||
const { orderBy = {}, ...rest } = params;
|
const { orderBy = {}, ...rest } = params;
|
||||||
|
@ -91,10 +91,9 @@ export default class ShlinkApiClient {
|
||||||
public readonly updateShortUrl = async (
|
public readonly updateShortUrl = async (
|
||||||
shortCode: string,
|
shortCode: string,
|
||||||
domain: OptionalString,
|
domain: OptionalString,
|
||||||
data: ShlinkShortUrlData,
|
edit: ShlinkShortUrlData,
|
||||||
): Promise<ShortUrl> =>
|
): Promise<ShortUrl> =>
|
||||||
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'PATCH', { domain }, data)
|
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'PATCH', { domain }, edit).then(({ data }) => data);
|
||||||
.then(({ data }) => data);
|
|
||||||
|
|
||||||
public readonly listTags = async (): Promise<ShlinkTags> =>
|
public readonly listTags = async (): Promise<ShlinkTags> =>
|
||||||
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })
|
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })
|
||||||
|
|
|
@ -23,7 +23,7 @@ const App = (
|
||||||
MenuLayout: FC,
|
MenuLayout: FC,
|
||||||
CreateServer: FC,
|
CreateServer: FC,
|
||||||
EditServer: FC,
|
EditServer: FC,
|
||||||
Settings: FC,
|
SettingsComp: FC,
|
||||||
ManageServers: FC,
|
ManageServers: FC,
|
||||||
ShlinkVersionsContainer: FC,
|
ShlinkVersionsContainer: FC,
|
||||||
) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => {
|
) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => {
|
||||||
|
@ -47,7 +47,7 @@ const App = (
|
||||||
<div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}>
|
<div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<Home />} />
|
<Route index element={<Home />} />
|
||||||
<Route path="/settings/*" element={<Settings />} />
|
<Route path="/settings/*" element={<SettingsComp />} />
|
||||||
<Route path="/manage-servers" element={<ManageServers />} />
|
<Route path="/manage-servers" element={<ManageServers />} />
|
||||||
<Route path="/server/create" element={<CreateServer />} />
|
<Route path="/server/create" element={<CreateServer />} />
|
||||||
<Route path="/server/:serverId/edit" element={<EditServer />} />
|
<Route path="/server/:serverId/edit" element={<EditServer />} />
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const APP_UPDATE_AVAILABLE = 'shlink/appUpdates/APP_UPDATE_AVAILABLE';
|
export const APP_UPDATE_AVAILABLE = 'shlink/appUpdates/APP_UPDATE_AVAILABLE';
|
||||||
export const RESET_APP_UPDATE = 'shlink/appUpdates/RESET_APP_UPDATE';
|
export const RESET_APP_UPDATE = 'shlink/appUpdates/RESET_APP_UPDATE';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
const initialState = false;
|
const initialState = false;
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
'ManageServers',
|
'ManageServers',
|
||||||
'ShlinkVersionsContainer',
|
'ShlinkVersionsContainer',
|
||||||
);
|
);
|
||||||
bottle.decorator('App', connect([ 'servers', 'settings', 'appUpdated' ], [ 'fetchServers', 'resetAppUpdate' ]));
|
bottle.decorator('App', connect(['servers', 'settings', 'appUpdated'], ['fetchServers', 'resetAppUpdate']));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable);
|
bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable);
|
||||||
|
|
|
@ -13,7 +13,7 @@ interface AppUpdateBannerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, toggle, forceUpdate }) => {
|
export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, toggle, forceUpdate }) => {
|
||||||
const [ isUpdating,, setUpdating ] = useToggle();
|
const [isUpdating,, setUpdating] = useToggle();
|
||||||
const update = () => {
|
const update = () => {
|
||||||
setUpdating();
|
setUpdating();
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
|
|
|
@ -17,7 +17,6 @@ import './AsideMenu.scss';
|
||||||
|
|
||||||
export interface AsideMenuProps {
|
export interface AsideMenuProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
className?: string;
|
|
||||||
showOnMobile?: boolean;
|
showOnMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ interface ErrorHandlerState {
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorHandler = (
|
const ErrorHandlerCreator = (
|
||||||
{ location }: Window,
|
{ location }: Window,
|
||||||
{ error }: Console,
|
{ error }: Console,
|
||||||
) => class ErrorHandler extends Component<any, ErrorHandlerState> {
|
) => class ErrorHandler extends Component<any, ErrorHandlerState> {
|
||||||
|
@ -26,7 +26,8 @@ const ErrorHandler = (
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
if (this.state.hasError) {
|
const { hasError } = this.state;
|
||||||
|
if (hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="home">
|
<div className="home">
|
||||||
<SimpleCard className="p-4">
|
<SimpleCard className="p-4">
|
||||||
|
@ -39,8 +40,9 @@ const ErrorHandler = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.props.children;
|
const { children } = this.props;
|
||||||
|
return children;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ErrorHandler;
|
export default ErrorHandlerCreator;
|
||||||
|
|
|
@ -22,7 +22,6 @@ const Home = ({ servers }: HomeProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Try to redirect to the first server marked as auto-connect
|
// Try to redirect to the first server marked as auto-connect
|
||||||
const autoConnectServer = serversList.find(({ autoConnect }) => autoConnect);
|
const autoConnectServer = serversList.find(({ autoConnect }) => autoConnect);
|
||||||
|
|
||||||
autoConnectServer && navigate(`/server/${autoConnectServer.id}`);
|
autoConnectServer && navigate(`/server/${autoConnectServer.id}`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,11 @@ import { ShlinkLogo } from './img/ShlinkLogo';
|
||||||
import './MainHeader.scss';
|
import './MainHeader.scss';
|
||||||
|
|
||||||
const MainHeader = (ServersDropdown: FC) => () => {
|
const MainHeader = (ServersDropdown: FC) => () => {
|
||||||
const [ isOpen, toggleOpen, , close ] = useToggle();
|
const [isOpen, toggleOpen, , close] = useToggle();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
|
|
||||||
useEffect(close, [ location ]);
|
useEffect(close, [location]);
|
||||||
|
|
||||||
const settingsPath = '/settings';
|
const settingsPath = '/settings';
|
||||||
const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen });
|
const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen });
|
||||||
|
|
|
@ -31,10 +31,10 @@ const MenuLayout = (
|
||||||
ManageDomains: FC,
|
ManageDomains: FC,
|
||||||
) => withSelectedServer<MenuLayoutProps>(({ selectedServer, sidebarNotPresent, sidebarPresent }) => {
|
) => withSelectedServer<MenuLayoutProps>(({ selectedServer, sidebarNotPresent, sidebarPresent }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
|
const [sidebarVisible, toggleSidebar, showSidebar, hideSidebar] = useToggle();
|
||||||
const showContent = isReachableServer(selectedServer);
|
const showContent = isReachableServer(selectedServer);
|
||||||
|
|
||||||
useEffect(() => hideSidebar(), [ location ]);
|
useEffect(() => hideSidebar(), [location]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
showContent && sidebarPresent();
|
showContent && sidebarPresent();
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ const ScrollToTop = (): FC => ({ children }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollTo(0, 0);
|
scrollTo(0, 0);
|
||||||
}, [ location ]);
|
}, [location]);
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,9 +22,9 @@ const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERS
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<small className="text-muted">
|
<small className="text-muted">
|
||||||
{isReachableServer(selectedServer) &&
|
{isReachableServer(selectedServer) && (
|
||||||
<>Server: <VersionLink project="shlink" version={selectedServer.printableVersion} /> - </>
|
<>Server: <VersionLink project="shlink" version={selectedServer.printableVersion} /> - </>
|
||||||
}
|
)}
|
||||||
Client: <VersionLink project="shlink-web-client" version={normalizedClientVersion} />
|
Client: <VersionLink project="shlink-web-client" version={normalizedClientVersion} />
|
||||||
</small>
|
</small>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const SIDEBAR_PRESENT = 'shlink/common/SIDEBAR_PRESENT';
|
export const SIDEBAR_PRESENT = 'shlink/common/SIDEBAR_PRESENT';
|
||||||
export const SIDEBAR_NOT_PRESENT = 'shlink/common/SIDEBAR_NOT_PRESENT';
|
export const SIDEBAR_NOT_PRESENT = 'shlink/common/SIDEBAR_NOT_PRESENT';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface Sidebar {
|
export interface Sidebar {
|
||||||
sidebarPresent: boolean;
|
sidebarPresent: boolean;
|
||||||
|
|
|
@ -29,7 +29,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
|
|
||||||
bottle.serviceFactory('Home', () => Home);
|
bottle.serviceFactory('Home', () => Home);
|
||||||
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',
|
||||||
|
@ -47,12 +47,12 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
'EditShortUrl',
|
'EditShortUrl',
|
||||||
'ManageDomains',
|
'ManageDomains',
|
||||||
);
|
);
|
||||||
bottle.decorator('MenuLayout', connect([ 'selectedServer' ], [ 'selectServer', 'sidebarPresent', 'sidebarNotPresent' ]));
|
bottle.decorator('MenuLayout', connect(['selectedServer'], ['selectServer', 'sidebarPresent', 'sidebarNotPresent']));
|
||||||
|
|
||||||
bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton');
|
bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton');
|
||||||
|
|
||||||
bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer);
|
bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer);
|
||||||
bottle.decorator('ShlinkVersionsContainer', connect([ 'selectedServer', 'sidebar' ]));
|
bottle.decorator('ShlinkVersionsContainer', connect(['selectedServer', 'sidebar']));
|
||||||
|
|
||||||
bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console');
|
bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console');
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,8 @@ const bottle = new Bottle();
|
||||||
|
|
||||||
export const { container } = bottle;
|
export const { container } = bottle;
|
||||||
|
|
||||||
const lazyService = <T extends Function, K>(container: IContainer, serviceName: string) =>
|
const lazyService = <T extends Function, K>(cont: IContainer, serviceName: string) =>
|
||||||
(...args: any[]) => (container[serviceName] as T)(...args) as K;
|
(...args: any[]) => (cont[serviceName] as T)(...args) as K;
|
||||||
const mapActionService = (map: LazyActionMap, actionName: string): LazyActionMap => ({
|
const mapActionService = (map: LazyActionMap, actionName: string): LazyActionMap => ({
|
||||||
...map,
|
...map,
|
||||||
// Wrap actual action service in a function so that it is lazily created the first time it is called
|
// Wrap actual action service in a function so that it is lazily created the first time it is called
|
||||||
|
|
|
@ -6,10 +6,10 @@ import { migrateDeprecatedSettings } from '../settings/helpers';
|
||||||
import { ShlinkState } from './types';
|
import { ShlinkState } from './types';
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV !== 'production';
|
const isProduction = process.env.NODE_ENV !== 'production';
|
||||||
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
const composeEnhancers: Function = !isProduction ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;
|
||||||
|
|
||||||
const localStorageConfig: RLSOptions = {
|
const localStorageConfig: RLSOptions = {
|
||||||
states: [ 'settings', 'servers' ],
|
states: ['settings', 'servers'],
|
||||||
namespace: 'shlink',
|
namespace: 'shlink',
|
||||||
namespaceSeparator: '.',
|
namespaceSeparator: '.',
|
||||||
debounce: 300,
|
debounce: 300,
|
||||||
|
|
|
@ -39,7 +39,7 @@ const DefaultDomain: FC = () => (
|
||||||
export const DomainRow: FC<DomainRowProps> = (
|
export const DomainRow: FC<DomainRowProps> = (
|
||||||
{ domain, editDomainRedirects, checkDomainHealth, defaultRedirects, selectedServer },
|
{ domain, editDomainRedirects, checkDomainHealth, defaultRedirects, selectedServer },
|
||||||
) => {
|
) => {
|
||||||
const [ isOpen, toggle ] = useToggle();
|
const [isOpen, toggle] = useToggle();
|
||||||
const { domain: authority, isDefault, redirects, status } = domain;
|
const { domain: authority, isDefault, redirects, status } = domain;
|
||||||
const canEditDomain = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
|
const canEditDomain = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ interface DomainSelectorConnectProps extends DomainSelectorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DomainSelector = ({ listDomains, value, domainsList, onChange }: DomainSelectorConnectProps) => {
|
export const DomainSelector = ({ listDomains, value, domainsList, onChange }: DomainSelectorConnectProps) => {
|
||||||
const [ inputDisplayed,, showInput, hideInput ] = useToggle();
|
const [inputDisplayed,, showInput, hideInput] = useToggle();
|
||||||
const { domains } = domainsList;
|
const { domains } = domainsList;
|
||||||
const valueIsEmpty = isEmpty(value);
|
const valueIsEmpty = isEmpty(value);
|
||||||
const unselectDomain = () => onChange('');
|
const unselectDomain = () => onChange('');
|
||||||
|
@ -56,7 +56,7 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
|
||||||
{domains.map(({ domain, isDefault }) => (
|
{domains.map(({ domain, isDefault }) => (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key={domain}
|
key={domain}
|
||||||
active={value === domain || isDefault && valueIsEmpty}
|
active={(value === domain || isDefault) && valueIsEmpty}
|
||||||
onClick={() => onChange(domain)}
|
onClick={() => onChange(domain)}
|
||||||
>
|
>
|
||||||
{domain}
|
{domain}
|
||||||
|
|
|
@ -18,7 +18,7 @@ interface ManageDomainsProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = [ '', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '', '' ];
|
const headers = ['', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '', ''];
|
||||||
|
|
||||||
export const ManageDomains: FC<ManageDomainsProps> = (
|
export const ManageDomains: FC<ManageDomainsProps> = (
|
||||||
{ listDomains, domainsList, filterDomains, editDomainRedirects, checkDomainHealth, selectedServer },
|
{ listDomains, domainsList, filterDomains, editDomainRedirects, checkDomainHealth, selectedServer },
|
||||||
|
|
|
@ -18,7 +18,7 @@ interface DomainStatusIconProps {
|
||||||
export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia = window.matchMedia }) => {
|
export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia = window.matchMedia }) => {
|
||||||
const ref = useRef<HTMLSpanElement>();
|
const ref = useRef<HTMLSpanElement>();
|
||||||
const matchesMobile = () => matchMedia('(max-width: 991px)').matches;
|
const matchesMobile = () => matchMedia('(max-width: 991px)').matches;
|
||||||
const [ isMobile, setIsMobile ] = useState<boolean>(matchesMobile());
|
const [isMobile, setIsMobile] = useState<boolean>(matchesMobile());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = () => setIsMobile(matchesMobile());
|
const listener = () => setIsMobile(matchesMobile());
|
||||||
|
|
|
@ -25,9 +25,9 @@ const FormGroup: FC<InputFormGroupProps & { isLast?: boolean }> = ({ isLast, ...
|
||||||
export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
|
export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
|
||||||
{ isOpen, toggle, domain, editDomainRedirects },
|
{ isOpen, toggle, domain, editDomainRedirects },
|
||||||
) => {
|
) => {
|
||||||
const [ baseUrlRedirect, setBaseUrlRedirect ] = useState(domain.redirects?.baseUrlRedirect ?? '');
|
const [baseUrlRedirect, setBaseUrlRedirect] = useState(domain.redirects?.baseUrlRedirect ?? '');
|
||||||
const [ regular404Redirect, setRegular404Redirect ] = useState(domain.redirects?.regular404Redirect ?? '');
|
const [regular404Redirect, setRegular404Redirect] = useState(domain.redirects?.regular404Redirect ?? '');
|
||||||
const [ invalidShortUrlRedirect, setInvalidShortUrlRedirect ] = useState(
|
const [invalidShortUrlRedirect, setInvalidShortUrlRedirect] = useState(
|
||||||
domain.redirects?.invalidShortUrlRedirect ?? '',
|
domain.redirects?.invalidShortUrlRedirect ?? '',
|
||||||
);
|
);
|
||||||
const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects(domain.domain, {
|
const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects(domain.domain, {
|
||||||
|
|
|
@ -5,11 +5,9 @@ import { GetState } from '../../container/types';
|
||||||
import { ApiErrorAction } from '../../api/types/actions';
|
import { ApiErrorAction } from '../../api/types/actions';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const EDIT_DOMAIN_REDIRECTS_START = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_START';
|
export const EDIT_DOMAIN_REDIRECTS_START = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_START';
|
||||||
export const EDIT_DOMAIN_REDIRECTS_ERROR = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_ERROR';
|
export const EDIT_DOMAIN_REDIRECTS_ERROR = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_ERROR';
|
||||||
export const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
|
export const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface EditDomainRedirectsAction extends Action<string> {
|
export interface EditDomainRedirectsAction extends Action<string> {
|
||||||
domain: string;
|
domain: string;
|
||||||
|
@ -21,10 +19,10 @@ export const editDomainRedirects = (buildShlinkApiClient: ShlinkApiClientBuilder
|
||||||
domainRedirects: Partial<ShlinkDomainRedirects>,
|
domainRedirects: Partial<ShlinkDomainRedirects>,
|
||||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
dispatch({ type: EDIT_DOMAIN_REDIRECTS_START });
|
dispatch({ type: EDIT_DOMAIN_REDIRECTS_START });
|
||||||
const { editDomainRedirects } = buildShlinkApiClient(getState);
|
const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const redirects = await editDomainRedirects({ domain, ...domainRedirects });
|
const redirects = await shlinkEditDomainRedirects({ domain, ...domainRedirects });
|
||||||
|
|
||||||
dispatch<EditDomainRedirectsAction>({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects });
|
dispatch<EditDomainRedirectsAction>({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
@ -10,13 +10,11 @@ import { hasServerData } from '../../servers/data';
|
||||||
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
|
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
|
||||||
import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
|
import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START';
|
export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START';
|
||||||
export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR';
|
export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR';
|
||||||
export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
|
export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
|
||||||
export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
|
export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
|
||||||
export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN';
|
export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface DomainsList {
|
export interface DomainsList {
|
||||||
domains: Domain[];
|
domains: Domain[];
|
||||||
|
@ -55,10 +53,10 @@ export type DomainsCombinedAction = ListDomainsAction
|
||||||
& ValidateDomain;
|
& ValidateDomain;
|
||||||
|
|
||||||
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
|
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
|
||||||
(d: Domain): Domain => d.domain !== domain ? d : { ...d, redirects };
|
(d: Domain): Domain => (d.domain !== domain ? d : { ...d, redirects });
|
||||||
|
|
||||||
export const replaceStatusOnDomain = (domain: string, status: DomainStatus) =>
|
export const replaceStatusOnDomain = (domain: string, status: DomainStatus) =>
|
||||||
(d: Domain): Domain => d.domain !== domain ? d : { ...d, status };
|
(d: Domain): Domain => (d.domain !== domain ? d : { ...d, status });
|
||||||
|
|
||||||
export default buildReducer<DomainsList, DomainsCombinedAction>({
|
export default buildReducer<DomainsList, DomainsCombinedAction>({
|
||||||
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
|
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
|
||||||
|
@ -86,15 +84,15 @@ export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => ()
|
||||||
getState: GetState,
|
getState: GetState,
|
||||||
) => {
|
) => {
|
||||||
dispatch({ type: LIST_DOMAINS_START });
|
dispatch({ type: LIST_DOMAINS_START });
|
||||||
const { listDomains } = buildShlinkApiClient(getState);
|
const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { domains, defaultRedirects } = await listDomains().then(({ data, defaultRedirects }) => ({
|
const resp = await shlinkListDomains().then(({ data, defaultRedirects }) => ({
|
||||||
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
|
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
|
||||||
defaultRedirects,
|
defaultRedirects,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, domains, defaultRedirects });
|
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, ...resp });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) });
|
dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) });
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,12 @@ import { editDomainRedirects } from '../reducers/domainRedirects';
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('DomainSelector', () => DomainSelector);
|
bottle.serviceFactory('DomainSelector', () => DomainSelector);
|
||||||
bottle.decorator('DomainSelector', connect([ 'domainsList' ], [ 'listDomains' ]));
|
bottle.decorator('DomainSelector', connect(['domainsList'], ['listDomains']));
|
||||||
|
|
||||||
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
||||||
bottle.decorator('ManageDomains', connect(
|
bottle.decorator('ManageDomains', connect(
|
||||||
[ 'domainsList', 'selectedServer' ],
|
['domainsList', 'selectedServer'],
|
||||||
[ 'listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth' ],
|
['listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth'],
|
||||||
));
|
));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
|
@ -23,7 +23,7 @@ export function boundToMercureHub<T = {}>(
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisits([ visit ]);
|
const onMessage = (visit: CreateVisit) => (interval ? pendingUpdates.add(visit) : createNewVisits([visit]));
|
||||||
const topics = getTopicsForProps(props, params);
|
const topics = getTopicsForProps(props, params);
|
||||||
const closeEventSource = bindToMercureTopic(mercureInfo, topics, onMessage, loadMercureInfo);
|
const closeEventSource = bindToMercureTopic(mercureInfo, topics, onMessage, loadMercureInfo);
|
||||||
|
|
||||||
|
@ -32,12 +32,12 @@ export function boundToMercureHub<T = {}>(
|
||||||
}
|
}
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
createNewVisits([ ...pendingUpdates ]);
|
createNewVisits([...pendingUpdates]);
|
||||||
pendingUpdates.clear();
|
pendingUpdates.clear();
|
||||||
}, interval * 1000 * 60);
|
}, interval * 1000 * 60);
|
||||||
|
|
||||||
return pipe(() => clearInterval(timer), () => closeEventSource?.());
|
return pipe(() => clearInterval(timer), () => closeEventSource?.());
|
||||||
}, [ mercureInfo ]);
|
}, [mercureInfo]);
|
||||||
|
|
||||||
return <WrappedComponent {...props} />;
|
return <WrappedComponent {...props} />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,11 +4,9 @@ import { GetState } from '../../container/types';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const GET_MERCURE_INFO_START = 'shlink/mercure/GET_MERCURE_INFO_START';
|
export const GET_MERCURE_INFO_START = 'shlink/mercure/GET_MERCURE_INFO_START';
|
||||||
export const GET_MERCURE_INFO_ERROR = 'shlink/mercure/GET_MERCURE_INFO_ERROR';
|
export const GET_MERCURE_INFO_ERROR = 'shlink/mercure/GET_MERCURE_INFO_ERROR';
|
||||||
export const GET_MERCURE_INFO = 'shlink/mercure/GET_MERCURE_INFO';
|
export const GET_MERCURE_INFO = 'shlink/mercure/GET_MERCURE_INFO';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface MercureInfo {
|
export interface MercureInfo {
|
||||||
token?: string;
|
token?: string;
|
||||||
|
|
|
@ -32,10 +32,10 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
const hasServers = !!Object.keys(servers).length;
|
const hasServers = !!Object.keys(servers).length;
|
||||||
const [ serversImported, setServersImported ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
const [serversImported, setServersImported] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
||||||
const [ errorImporting, setErrorImporting ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
const [errorImporting, setErrorImporting] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
||||||
const [ isConfirmModalOpen, toggleConfirmModal ] = useToggle();
|
const [isConfirmModalOpen, toggleConfirmModal] = useToggle();
|
||||||
const [ serverData, setServerData ] = useState<ServerData | undefined>();
|
const [serverData, setServerData] = useState<ServerData | undefined>();
|
||||||
const save = () => {
|
const save = () => {
|
||||||
if (!serverData) {
|
if (!serverData) {
|
||||||
return;
|
return;
|
||||||
|
@ -53,13 +53,14 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
|
||||||
);
|
);
|
||||||
|
|
||||||
serverExists ? toggleConfirmModal() : save();
|
serverExists ? toggleConfirmModal() : save();
|
||||||
}, [ serverData ]);
|
}, [serverData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<ServerForm title={<h5 className="mb-0">Add new server</h5>} onSubmit={setServerData}>
|
<ServerForm title={<h5 className="mb-0">Add new server</h5>} onSubmit={setServerData}>
|
||||||
{!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="ms-2">Create server</Button>
|
<Button outline color="primary" className="ms-2">Create server</Button>
|
||||||
</ServerForm>
|
</ServerForm>
|
||||||
|
@ -69,7 +70,7 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
|
||||||
|
|
||||||
<DuplicatedServersModal
|
<DuplicatedServersModal
|
||||||
isOpen={isConfirmModalOpen}
|
isOpen={isConfirmModalOpen}
|
||||||
duplicatedServers={serverData ? [ serverData ] : []}
|
duplicatedServers={serverData ? [serverData] : []}
|
||||||
onDiscard={goBack}
|
onDiscard={goBack}
|
||||||
onSave={save}
|
onSave={save}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,7 +14,7 @@ export interface DeleteServerButtonProps {
|
||||||
const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<DeleteServerButtonProps> => (
|
const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<DeleteServerButtonProps> => (
|
||||||
{ server, className, children, textClassName },
|
{ server, className, children, textClassName },
|
||||||
) => {
|
) => {
|
||||||
const [ isModalOpen, , showModal, hideModal ] = useToggle();
|
const [isModalOpen, , showModal, hideModal] = useToggle();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ServerWithId } from './data';
|
import { ServerWithId } from './data';
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@ const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
|
||||||
</p>
|
</p>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
<Button color="link" onClick={toggle}>Cancel</Button>
|
||||||
<button className="btn btn-danger" onClick={() => closeModal()}>Delete</button>
|
<Button color="danger" onClick={() => closeModal()}>Delete</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -26,16 +26,16 @@ export const ManageServers = (
|
||||||
ManageServersRow: FC<ManageServersRowProps>,
|
ManageServersRow: FC<ManageServersRowProps>,
|
||||||
): FC<ManageServersProps> => ({ servers }) => {
|
): FC<ManageServersProps> => ({ servers }) => {
|
||||||
const allServers = Object.values(servers);
|
const allServers = Object.values(servers);
|
||||||
const [ serversList, setServersList ] = useState(allServers);
|
const [serversList, setServersList] = useState(allServers);
|
||||||
const filterServers = (searchTerm: string) => setServersList(
|
const filterServers = (searchTerm: string) => setServersList(
|
||||||
allServers.filter(({ name, url }) => `${name} ${url}`.match(searchTerm)),
|
allServers.filter(({ name, url }) => `${name} ${url}`.match(searchTerm)),
|
||||||
);
|
);
|
||||||
const hasAutoConnect = serversList.some(({ autoConnect }) => !!autoConnect);
|
const hasAutoConnect = serversList.some(({ autoConnect }) => !!autoConnect);
|
||||||
const [ errorImporting, setErrorImporting ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
const [errorImporting, setErrorImporting] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setServersList(Object.values(servers));
|
setServersList(Object.values(servers));
|
||||||
}, [ servers ]);
|
}, [servers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
|
@ -61,17 +61,17 @@ export const ManageServers = (
|
||||||
<table className="table table-hover responsive-table 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 aria-label="Auto-connect" style={{ width: '50px' }} />}
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Base URL</th>
|
<th>Base URL</th>
|
||||||
<th />
|
<th aria-label="Options" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{!serversList.length && <tr className="text-center"><td colSpan={4}>No servers found.</td></tr>}
|
{!serversList.length && <tr className="text-center"><td colSpan={4}>No servers found.</td></tr>}
|
||||||
{serversList.map((server) =>
|
{serversList.map((server) => (
|
||||||
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />)
|
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />
|
||||||
}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
|
|
|
@ -25,8 +25,8 @@ interface ManageServersRowDropdownConnectProps extends ManageServersRowDropdownP
|
||||||
export const ManageServersRowDropdown = (
|
export const ManageServersRowDropdown = (
|
||||||
DeleteServerModal: FC<DeleteServerModalProps>,
|
DeleteServerModal: FC<DeleteServerModalProps>,
|
||||||
): FC<ManageServersRowDropdownConnectProps> => ({ server, setAutoConnect }) => {
|
): FC<ManageServersRowDropdownConnectProps> => ({ server, setAutoConnect }) => {
|
||||||
const [ isMenuOpen, toggleMenu ] = useToggle();
|
const [isMenuOpen, toggleMenu] = useToggle();
|
||||||
const [ isModalOpen,, showModal, hideModal ] = useToggle();
|
const [isModalOpen,, showModal, hideModal] = useToggle();
|
||||||
const serverUrl = `/server/${server.id}`;
|
const serverUrl = `/server/${server.id}`;
|
||||||
const { autoConnect: isAutoConnect } = server;
|
const { autoConnect: isAutoConnect } = server;
|
||||||
const autoConnectIcon = isAutoConnect ? toggleOffIcon : toggleOnIcon;
|
const autoConnectIcon = isAutoConnect ? toggleOffIcon : toggleOnIcon;
|
||||||
|
|
|
@ -109,4 +109,4 @@ export const Overview = (
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => [ Topics.visits, Topics.orphanVisits ]);
|
}, () => [Topics.visits, Topics.orphanVisits]);
|
||||||
|
|
|
@ -35,15 +35,15 @@ export const hasServerData = (server: SelectedServer | ServerData): server is Se
|
||||||
!!(server as ServerData)?.url && !!(server as ServerData)?.apiKey;
|
!!(server as ServerData)?.url && !!(server as ServerData)?.apiKey;
|
||||||
|
|
||||||
export const isServerWithId = (server: SelectedServer | ServerWithId): server is ServerWithId =>
|
export const isServerWithId = (server: SelectedServer | ServerWithId): server is ServerWithId =>
|
||||||
!!server?.hasOwnProperty('id');
|
!!(server as ServerWithId)?.id;
|
||||||
|
|
||||||
export const isReachableServer = (server: SelectedServer): server is ReachableServer =>
|
export const isReachableServer = (server: SelectedServer): server is ReachableServer =>
|
||||||
!!server?.hasOwnProperty('version');
|
!!(server as ReachableServer)?.version;
|
||||||
|
|
||||||
export const isNotFoundServer = (server: SelectedServer): server is NotFoundServer =>
|
export const isNotFoundServer = (server: SelectedServer): server is NotFoundServer =>
|
||||||
!!server?.hasOwnProperty('serverNotFound');
|
!!(server as NotFoundServer)?.serverNotFound;
|
||||||
|
|
||||||
export const getServerId = (server: SelectedServer) => isServerWithId(server) ? server.id : '';
|
export const getServerId = (server: SelectedServer) => (isServerWithId(server) ? server.id : '');
|
||||||
|
|
||||||
export const serverWithIdToServerData = (server: ServerWithId): ServerData =>
|
export const serverWithIdToServerData = (server: ServerWithId): ServerData =>
|
||||||
omit<ServerWithId, 'id' | 'autoConnect'>([ 'id', 'autoConnect' ], server);
|
omit<ServerWithId, 'id' | 'autoConnect'>(['id', 'autoConnect'], server);
|
||||||
|
|
|
@ -20,12 +20,12 @@ export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = (
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p>
|
<p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{duplicatedServers.map(({ url, apiKey }, index) => !hasMultipleServers ? (
|
{duplicatedServers.map(({ url, apiKey }, index) => (!hasMultipleServers ? (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
<li>URL: <b>{url}</b></li>
|
<li>URL: <b>{url}</b></li>
|
||||||
<li>API key: <b>{apiKey}</b></li>
|
<li>API key: <b>{apiKey}</b></li>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : <li key={index}><b>{url}</b> - <b>{apiKey}</b></li>)}
|
) : <li key={index}><b>{url}</b> - <b>{apiKey}</b></li>))}
|
||||||
</ul>
|
</ul>
|
||||||
<span>
|
<span>
|
||||||
{hasMultipleServers ? 'Do you want to ignore duplicated servers' : 'Do you want to save this server anyway'}?
|
{hasMultipleServers ? 'Do you want to ignore duplicated servers' : 'Do you want to save this server anyway'}?
|
||||||
|
|
|
@ -10,7 +10,7 @@ export interface HighlightCardProps {
|
||||||
link?: string | false;
|
link?: string | false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildExtraProps = (link?: string | false) => !link ? {} : { tag: Link, to: link };
|
const buildExtraProps = (link?: string | false) => (!link ? {} : { tag: Link, to: link });
|
||||||
|
|
||||||
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link }) => (
|
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link }) => (
|
||||||
<Card className="highlight-card" body {...buildExtraProps(link)}>
|
<Card className="highlight-card" body {...buildExtraProps(link)}>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useRef, RefObject, ChangeEvent, MutableRefObject, FC, useState, useEffect } from 'react';
|
import { useRef, RefObject, ChangeEvent, MutableRefObject, useState, useEffect, FC } from 'react';
|
||||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||||
import { complement, pipe } from 'ramda';
|
import { complement, pipe } from 'ramda';
|
||||||
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
@ -38,9 +38,9 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<Import
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
const ref = fileRef ?? useRef<HTMLInputElement>();
|
const ref = fileRef ?? useRef<HTMLInputElement>();
|
||||||
const [ serversToCreate, setServersToCreate ] = useState<ServerData[] | undefined>();
|
const [serversToCreate, setServersToCreate] = useState<ServerData[] | undefined>();
|
||||||
const [ duplicatedServers, setDuplicatedServers ] = useState<ServerData[]>([]);
|
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
||||||
const [ isModalOpen,, showModal, hideModal ] = useToggle();
|
const [isModalOpen,, showModal, hideModal] = useToggle();
|
||||||
const create = pipe(createServers, onImport);
|
const create = pipe(createServers, onImport);
|
||||||
const createAllServers = pipe(() => create(serversToCreate ?? []), hideModal);
|
const createAllServers = pipe(() => create(serversToCreate ?? []), hideModal);
|
||||||
const createNonDuplicatedServers = pipe(
|
const createNonDuplicatedServers = pipe(
|
||||||
|
@ -52,7 +52,7 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<Import
|
||||||
.then(setServersToCreate)
|
.then(setServersToCreate)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Reset input after processing file
|
// Reset input after processing file
|
||||||
(target as { value: string | null }).value = null;
|
(target as { value: string | null }).value = null; // eslint-disable-line no-param-reassign
|
||||||
})
|
})
|
||||||
.catch(onImportError);
|
.catch(onImportError);
|
||||||
|
|
||||||
|
@ -62,12 +62,12 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<Import
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingServers = Object.values(servers);
|
const existingServers = Object.values(servers);
|
||||||
const duplicatedServers = serversToCreate.filter(serversFiltering(existingServers));
|
const dupServers = serversToCreate.filter(serversFiltering(existingServers));
|
||||||
const hasDuplicatedServers = !!duplicatedServers.length;
|
const hasDuplicatedServers = !!dupServers.length;
|
||||||
|
|
||||||
!hasDuplicatedServers ? create(serversToCreate) : setDuplicatedServers(duplicatedServers);
|
!hasDuplicatedServers ? create(serversToCreate) : setDuplicatedServers(dupServers);
|
||||||
hasDuplicatedServers && showModal();
|
hasDuplicatedServers && showModal();
|
||||||
}, [ serversToCreate ]);
|
}, [serversToCreate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -11,16 +11,16 @@ interface ServerFormProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
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('');
|
||||||
const [ apiKey, setApiKey ] = useState('');
|
const [apiKey, setApiKey] = useState('');
|
||||||
const handleSubmit = handleEventPreventingDefault(() => onSubmit({ name, url, apiKey }));
|
const handleSubmit = handleEventPreventingDefault(() => onSubmit({ name, url, apiKey }));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initialValues && setName(initialValues.name);
|
initialValues && setName(initialValues.name);
|
||||||
initialValues && setUrl(initialValues.url);
|
initialValues && setUrl(initialValues.url);
|
||||||
initialValues && setApiKey(initialValues.apiKey);
|
initialValues && setApiKey(initialValues.apiKey);
|
||||||
}, [ initialValues ]);
|
}, [initialValues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="server-form" onSubmit={handleSubmit}>
|
<form className="server-form" onSubmit={handleSubmit}>
|
||||||
|
|
|
@ -16,7 +16,7 @@ export function withSelectedServer<T = {}>(WrappedComponent: FC<WithSelectedServ
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
params.serverId && selectServer(params.serverId);
|
params.serverId && selectServer(params.serverId);
|
||||||
}, [ params.serverId ]);
|
}, [params.serverId]);
|
||||||
|
|
||||||
if (!selectedServer) {
|
if (!selectedServer) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -6,8 +6,9 @@ interface WithoutSelectedServerProps {
|
||||||
|
|
||||||
export function withoutSelectedServer<T = {}>(WrappedComponent: FC<WithoutSelectedServerProps & T>) {
|
export function withoutSelectedServer<T = {}>(WrappedComponent: FC<WithoutSelectedServerProps & T>) {
|
||||||
return (props: WithoutSelectedServerProps & T) => {
|
return (props: WithoutSelectedServerProps & T) => {
|
||||||
|
const { resetSelectedServer } = props;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
props.resetSelectedServer();
|
resetSelectedServer();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <WrappedComponent {...props} />;
|
return <WrappedComponent {...props} />;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { createServers } from './servers';
|
||||||
|
|
||||||
const responseToServersList = pipe(
|
const responseToServersList = pipe(
|
||||||
prop<any, any>('data'),
|
prop<any, any>('data'),
|
||||||
(data: any): ServerData[] => Array.isArray(data) ? data.filter(hasServerData) : [],
|
(data: any): ServerData[] => (Array.isArray(data) ? data.filter(hasServerData) : []),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fetchServers = ({ get }: AxiosInstance) => () => async (dispatch: Dispatch) => {
|
export const fetchServers = ({ get }: AxiosInstance) => () => async (dispatch: Dispatch) => {
|
||||||
|
|
|
@ -7,21 +7,19 @@ import { ShlinkHealth } from '../../api/types';
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
|
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
|
||||||
export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER';
|
export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER';
|
||||||
|
|
||||||
export const MIN_FALLBACK_VERSION = '1.0.0';
|
export const MIN_FALLBACK_VERSION = '1.0.0';
|
||||||
export const MAX_FALLBACK_VERSION = '999.999.999';
|
export const MAX_FALLBACK_VERSION = '999.999.999';
|
||||||
export const LATEST_VERSION_CONSTRAINT = 'latest';
|
export const LATEST_VERSION_CONSTRAINT = 'latest';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface SelectServerAction extends Action<string> {
|
export interface SelectServerAction extends Action<string> {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionToSemVer = pipe(
|
const versionToSemVer = pipe(
|
||||||
(version: string) => version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version,
|
(version: string) => (version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version),
|
||||||
toSemVer(MIN_FALLBACK_VERSION),
|
toSemVer(MIN_FALLBACK_VERSION),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,10 @@ import { Action } from 'redux';
|
||||||
import { ServerData, ServersMap, ServerWithId } from '../data';
|
import { ServerData, ServersMap, ServerWithId } from '../data';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const EDIT_SERVER = 'shlink/servers/EDIT_SERVER';
|
export const EDIT_SERVER = 'shlink/servers/EDIT_SERVER';
|
||||||
export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER';
|
export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER';
|
||||||
export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS';
|
export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS';
|
||||||
export const SET_AUTO_CONNECT = 'shlink/servers/SET_AUTO_CONNECT';
|
export const SET_AUTO_CONNECT = 'shlink/servers/SET_AUTO_CONNECT';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface CreateServersAction extends Action<string> {
|
export interface CreateServersAction extends Action<string> {
|
||||||
newServers: ServersMap;
|
newServers: ServersMap;
|
||||||
|
@ -37,9 +35,9 @@ const serverWithId = (server: ServerWithId | ServerData): ServerWithId => {
|
||||||
export default buildReducer<ServersMap, CreateServersAction & DeleteServerAction & SetAutoConnectAction>({
|
export default buildReducer<ServersMap, CreateServersAction & DeleteServerAction & SetAutoConnectAction>({
|
||||||
[CREATE_SERVERS]: (state, { newServers }) => ({ ...state, ...newServers }),
|
[CREATE_SERVERS]: (state, { newServers }) => ({ ...state, ...newServers }),
|
||||||
[DELETE_SERVER]: (state, { serverId }) => dissoc(serverId, state),
|
[DELETE_SERVER]: (state, { serverId }) => dissoc(serverId, state),
|
||||||
[EDIT_SERVER]: (state, { serverId, serverData }: any) => !state[serverId]
|
[EDIT_SERVER]: (state, { serverId, serverData }: any) => (
|
||||||
? state
|
!state[serverId] ? state : assoc(serverId, { ...state[serverId], ...serverData }, state)
|
||||||
: assoc(serverId, { ...state[serverId], ...serverData }, state),
|
),
|
||||||
[SET_AUTO_CONNECT]: (state, { serverId, autoConnect }) => {
|
[SET_AUTO_CONNECT]: (state, { serverId, autoConnect }) => {
|
||||||
if (!state[serverId]) {
|
if (!state[serverId]) {
|
||||||
return state;
|
return state;
|
||||||
|
@ -50,7 +48,7 @@ export default buildReducer<ServersMap, CreateServersAction & DeleteServerAction
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromPairs(
|
return fromPairs(
|
||||||
toPairs(state).map(([ evaluatedServerId, server ]) => [
|
toPairs(state).map(([evaluatedServerId, server]) => [
|
||||||
evaluatedServerId,
|
evaluatedServerId,
|
||||||
{ ...server, autoConnect: evaluatedServerId === serverId },
|
{ ...server, autoConnect: evaluatedServerId === serverId },
|
||||||
]),
|
]),
|
||||||
|
@ -66,7 +64,7 @@ export const createServers = pipe(
|
||||||
(newServers: ServersMap) => ({ type: CREATE_SERVERS, newServers }),
|
(newServers: ServersMap) => ({ type: CREATE_SERVERS, newServers }),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const createServer = (server: ServerWithId) => createServers([ server ]);
|
export const createServer = (server: ServerWithId) => createServers([server]);
|
||||||
|
|
||||||
export const editServer = (serverId: string, serverData: Partial<ServerData>) => ({
|
export const editServer = (serverId: string, serverData: Partial<ServerData>) => ({
|
||||||
type: EDIT_SERVER,
|
type: EDIT_SERVER,
|
||||||
|
|
|
@ -29,8 +29,8 @@ export class ServersImporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(servers);
|
resolve(servers);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
reject(e);
|
reject(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
|
|
|
@ -31,41 +31,41 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
'ManageServersRow',
|
'ManageServersRow',
|
||||||
);
|
);
|
||||||
bottle.decorator('ManageServers', withoutSelectedServer);
|
bottle.decorator('ManageServers', withoutSelectedServer);
|
||||||
bottle.decorator('ManageServers', connect([ 'selectedServer', 'servers' ], [ 'resetSelectedServer' ]));
|
bottle.decorator('ManageServers', connect(['selectedServer', 'servers'], ['resetSelectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('ManageServersRow', ManageServersRow, 'ManageServersRowDropdown');
|
bottle.serviceFactory('ManageServersRow', ManageServersRow, 'ManageServersRowDropdown');
|
||||||
|
|
||||||
bottle.serviceFactory('ManageServersRowDropdown', ManageServersRowDropdown, 'DeleteServerModal');
|
bottle.serviceFactory('ManageServersRowDropdown', ManageServersRowDropdown, 'DeleteServerModal');
|
||||||
bottle.decorator('ManageServersRowDropdown', connect(null, [ 'setAutoConnect' ]));
|
bottle.decorator('ManageServersRowDropdown', connect(null, ['setAutoConnect']));
|
||||||
|
|
||||||
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout');
|
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout');
|
||||||
bottle.decorator('CreateServer', withoutSelectedServer);
|
bottle.decorator('CreateServer', withoutSelectedServer);
|
||||||
bottle.decorator('CreateServer', connect([ 'selectedServer', 'servers' ], [ 'createServer', 'resetSelectedServer' ]));
|
bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServer', 'resetSelectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('EditServer', EditServer, 'ServerError');
|
bottle.serviceFactory('EditServer', EditServer, 'ServerError');
|
||||||
bottle.decorator('EditServer', connect([ 'selectedServer' ], [ 'editServer', 'selectServer', 'resetSelectedServer' ]));
|
bottle.decorator('EditServer', connect(['selectedServer'], ['editServer', 'selectServer', 'resetSelectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('ServersDropdown', () => ServersDropdown);
|
bottle.serviceFactory('ServersDropdown', () => ServersDropdown);
|
||||||
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ]));
|
bottle.decorator('ServersDropdown', connect(['servers', 'selectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
|
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
|
||||||
bottle.decorator('DeleteServerModal', connect(null, [ 'deleteServer' ]));
|
bottle.decorator('DeleteServerModal', connect(null, ['deleteServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal');
|
bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal');
|
||||||
|
|
||||||
bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter');
|
bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter');
|
||||||
bottle.decorator('ImportServersBtn', connect([ 'servers' ], [ 'createServers' ]));
|
bottle.decorator('ImportServersBtn', connect(['servers'], ['createServers']));
|
||||||
|
|
||||||
bottle.serviceFactory('ForServerVersion', () => ForServerVersion);
|
bottle.serviceFactory('ForServerVersion', () => ForServerVersion);
|
||||||
bottle.decorator('ForServerVersion', connect([ 'selectedServer' ]));
|
bottle.decorator('ForServerVersion', connect(['selectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
|
bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
|
||||||
bottle.decorator('ServerError', connect([ 'servers', 'selectedServer' ]));
|
bottle.decorator('ServerError', connect(['servers', 'selectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl', 'ForServerVersion');
|
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl', 'ForServerVersion');
|
||||||
bottle.decorator('Overview', connect(
|
bottle.decorator('Overview', connect(
|
||||||
[ 'shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview' ],
|
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview'],
|
||||||
[ 'listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview' ],
|
['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'],
|
||||||
));
|
));
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
/* eslint-disable no-restricted-globals */
|
|
||||||
|
|
||||||
// This service worker can be customized!
|
// This service worker can be customized!
|
||||||
// See https://developers.google.com/web/tools/workbox/modules
|
// See https://developers.google.com/web/tools/workbox/modules
|
||||||
|
|
|
@ -12,7 +12,7 @@ interface RealTimeUpdatesProps {
|
||||||
setRealTimeUpdatesInterval: (interval: number) => void;
|
setRealTimeUpdatesInterval: (interval: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const intervalValue = (interval?: number) => !interval ? '' : `${interval}`;
|
const intervalValue = (interval?: number) => (!interval ? '' : `${interval}`);
|
||||||
|
|
||||||
const RealTimeUpdatesSettings = (
|
const RealTimeUpdatesSettings = (
|
||||||
{ settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
|
{ settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
|
||||||
|
|
|
@ -25,9 +25,9 @@ const Settings = (
|
||||||
</NavPills>
|
</NavPills>
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="general" element={<SettingsSections items={[ <UserInterface key="one" />, <RealTimeUpdates key="two" /> ]} />} />
|
<Route path="general" element={<SettingsSections items={[<UserInterface />, <RealTimeUpdates />]} />} />
|
||||||
<Route path="short-urls" element={<SettingsSections items={[ <ShortUrlCreation key="one" />, <ShortUrlsList key="two" /> ]} />} />
|
<Route path="short-urls" element={<SettingsSections items={[<ShortUrlCreation />, <ShortUrlsList />]} />} />
|
||||||
<Route path="other-items" element={<SettingsSections items={[ <Tags key="one" />, <Visits key="two" /> ]} />} />
|
<Route path="other-items" element={<SettingsSections items={[<Tags />, <Visits />]} />} />
|
||||||
<Route path="*" element={<Navigate replace to="general" />} />
|
<Route path="*" element={<Navigate replace to="general" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
|
|
|
@ -13,11 +13,12 @@ interface ShortUrlCreationProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagFilteringModeText = (tagFilteringMode: TagFilteringMode | undefined): string =>
|
const tagFilteringModeText = (tagFilteringMode: TagFilteringMode | undefined): string =>
|
||||||
tagFilteringMode === 'includes' ? 'Suggest tags including input' : 'Suggest tags starting with input';
|
(tagFilteringMode === 'includes' ? 'Suggest tags including input' : 'Suggest tags starting with input');
|
||||||
const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): ReactNode =>
|
const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): ReactNode => (
|
||||||
tagFilteringMode === 'includes'
|
tagFilteringMode === 'includes'
|
||||||
? <>The list of suggested tags will contain those <b>including</b> provided input.</>
|
? <>The list of suggested tags will contain those <b>including</b> provided input.</>
|
||||||
: <>The list of suggested tags will contain those <b>starting with</b> provided input.</>;
|
: <>The list of suggested tags will contain those <b>starting with</b> provided input.</>
|
||||||
|
);
|
||||||
|
|
||||||
export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ settings, setShortUrlCreationSettings }) => {
|
export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ settings, setShortUrlCreationSettings }) => {
|
||||||
const shortUrlCreation: ShortUrlsSettings = settings.shortUrlCreation ?? { validateUrls: false };
|
const shortUrlCreation: ShortUrlsSettings = settings.shortUrlCreation ?? { validateUrls: false };
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ShlinkState } from '../../container/types';
|
import { ShlinkState } from '../../container/types';
|
||||||
|
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
export const migrateDeprecatedSettings = (state: Partial<ShlinkState>): Partial<ShlinkState> => {
|
export const migrateDeprecatedSettings = (state: Partial<ShlinkState>): Partial<ShlinkState> => {
|
||||||
if (!state.settings) {
|
if (!state.settings) {
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -31,28 +31,28 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
'TagsSettings',
|
'TagsSettings',
|
||||||
);
|
);
|
||||||
bottle.decorator('Settings', withoutSelectedServer);
|
bottle.decorator('Settings', withoutSelectedServer);
|
||||||
bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ]));
|
bottle.decorator('Settings', connect(null, ['resetSelectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('RealTimeUpdatesSettings', () => RealTimeUpdatesSettings);
|
bottle.serviceFactory('RealTimeUpdatesSettings', () => RealTimeUpdatesSettings);
|
||||||
bottle.decorator(
|
bottle.decorator(
|
||||||
'RealTimeUpdatesSettings',
|
'RealTimeUpdatesSettings',
|
||||||
connect([ 'settings' ], [ 'toggleRealTimeUpdates', 'setRealTimeUpdatesInterval' ]),
|
connect(['settings'], ['toggleRealTimeUpdates', 'setRealTimeUpdatesInterval']),
|
||||||
);
|
);
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlCreationSettings', () => ShortUrlCreationSettings);
|
bottle.serviceFactory('ShortUrlCreationSettings', () => ShortUrlCreationSettings);
|
||||||
bottle.decorator('ShortUrlCreationSettings', connect([ 'settings' ], [ 'setShortUrlCreationSettings' ]));
|
bottle.decorator('ShortUrlCreationSettings', connect(['settings'], ['setShortUrlCreationSettings']));
|
||||||
|
|
||||||
bottle.serviceFactory('UserInterfaceSettings', () => UserInterfaceSettings);
|
bottle.serviceFactory('UserInterfaceSettings', () => UserInterfaceSettings);
|
||||||
bottle.decorator('UserInterfaceSettings', connect([ 'settings' ], [ 'setUiSettings' ]));
|
bottle.decorator('UserInterfaceSettings', connect(['settings'], ['setUiSettings']));
|
||||||
|
|
||||||
bottle.serviceFactory('VisitsSettings', () => VisitsSettings);
|
bottle.serviceFactory('VisitsSettings', () => VisitsSettings);
|
||||||
bottle.decorator('VisitsSettings', connect([ 'settings' ], [ 'setVisitsSettings' ]));
|
bottle.decorator('VisitsSettings', connect(['settings'], ['setVisitsSettings']));
|
||||||
|
|
||||||
bottle.serviceFactory('TagsSettings', () => TagsSettings);
|
bottle.serviceFactory('TagsSettings', () => TagsSettings);
|
||||||
bottle.decorator('TagsSettings', connect([ 'settings' ], [ 'setTagsSettings' ]));
|
bottle.decorator('TagsSettings', connect(['settings'], ['setTagsSettings']));
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlsListSettings', () => ShortUrlsListSettings);
|
bottle.serviceFactory('ShortUrlsListSettings', () => ShortUrlsListSettings);
|
||||||
bottle.decorator('ShortUrlsListSettings', connect([ 'settings' ], [ 'setShortUrlsListSettings' ]));
|
bottle.decorator('ShortUrlsListSettings', connect(['settings'], ['setShortUrlsListSettings']));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
||||||
|
|
|
@ -41,7 +41,7 @@ const CreateShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>, CreateShortUrlResul
|
||||||
basicMode = false,
|
basicMode = false,
|
||||||
settings: { shortUrlCreation: shortUrlCreationSettings },
|
settings: { shortUrlCreation: shortUrlCreationSettings },
|
||||||
}: CreateShortUrlConnectProps) => {
|
}: CreateShortUrlConnectProps) => {
|
||||||
const initialState = useMemo(() => getInitialState(shortUrlCreationSettings), [ shortUrlCreationSettings ]);
|
const initialState = useMemo(() => getInitialState(shortUrlCreationSettings), [shortUrlCreationSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -63,9 +63,9 @@ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
|
||||||
const { domain } = parseQuery<{ domain?: string }>(search);
|
const { domain } = parseQuery<{ domain?: string }>(search);
|
||||||
const initialState = useMemo(
|
const initialState = useMemo(
|
||||||
() => getInitialState(shortUrl, shortUrlCreationSettings),
|
() => getInitialState(shortUrl, shortUrlCreationSettings),
|
||||||
[ shortUrl, shortUrlCreationSettings ],
|
[shortUrl, shortUrlCreationSettings],
|
||||||
);
|
);
|
||||||
const [ savingSucceeded,, isSuccessful, isNotSuccessful ] = useToggle();
|
const [savingSucceeded,, isSuccessful, isNotSuccessful] = useToggle();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
params.shortCode && getShortUrlDetail(params.shortCode, domain);
|
params.shortCode && getShortUrlDetail(params.shortCode, domain);
|
||||||
|
|
|
@ -32,14 +32,14 @@ export interface ShortUrlFormProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
||||||
const toDate = (date?: string | Date): Date | undefined => typeof date === 'string' ? parseISO(date) : date;
|
const toDate = (date?: string | Date): Date | undefined => (typeof date === 'string' ? parseISO(date) : date);
|
||||||
const dynamicColClasses = (flag: boolean) => ({ 'col-sm-6': flag, 'col-sm-12': !flag });
|
const dynamicColClasses = (flag: boolean) => ({ 'col-sm-6': flag, 'col-sm-12': !flag });
|
||||||
|
|
||||||
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, selectedServer }) => {
|
||||||
const [ shortUrlData, setShortUrlData ] = useState(initialState);
|
const [shortUrlData, setShortUrlData] = useState(initialState);
|
||||||
const isEdit = mode === 'edit';
|
const isEdit = mode === 'edit';
|
||||||
const isBasicMode = mode === 'create-basic';
|
const isBasicMode = mode === 'create-basic';
|
||||||
const hadTitleOriginally = hasValue(initialState.title);
|
const hadTitleOriginally = hasValue(initialState.title);
|
||||||
|
@ -48,9 +48,9 @@ export const ShortUrlForm = (
|
||||||
const resolveNewTitle = (): OptionalString => {
|
const resolveNewTitle = (): OptionalString => {
|
||||||
const hasNewTitle = hasValue(shortUrlData.title);
|
const hasNewTitle = hasValue(shortUrlData.title);
|
||||||
const matcher = cond<never, OptionalString>([
|
const matcher = cond<never, OptionalString>([
|
||||||
[ () => !hasNewTitle && !hadTitleOriginally, () => undefined ],
|
[() => !hasNewTitle && !hadTitleOriginally, () => undefined],
|
||||||
[ () => !hasNewTitle && hadTitleOriginally, () => null ],
|
[() => !hasNewTitle && hadTitleOriginally, () => null],
|
||||||
[ T, () => shortUrlData.title ],
|
[T, () => shortUrlData.title],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return matcher();
|
return matcher();
|
||||||
|
@ -65,7 +65,7 @@ export const ShortUrlForm = (
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShortUrlData(initialState);
|
setShortUrlData(initialState);
|
||||||
}, [ initialState ]);
|
}, [initialState]);
|
||||||
|
|
||||||
const renderOptionalInput = (
|
const renderOptionalInput = (
|
||||||
id: NonDateFields,
|
id: NonDateFields,
|
||||||
|
|
|
@ -29,23 +29,23 @@ export interface ShortUrlsFilteringProps {
|
||||||
shortUrlsAmount?: number;
|
shortUrlsAmount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateOrNull = (date?: string) => date ? parseISO(date) : null;
|
const dateOrNull = (date?: string) => (date ? parseISO(date) : null);
|
||||||
|
|
||||||
const ShortUrlsFilteringBar = (
|
const ShortUrlsFilteringBar = (
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator: ColorGenerator,
|
||||||
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
||||||
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy }) => {
|
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy }) => {
|
||||||
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage ] = useShortUrlsQuery();
|
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage] = useShortUrlsQuery();
|
||||||
const setDates = pipe(
|
const setDates = pipe(
|
||||||
({ startDate, endDate }: DateRange) => ({
|
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
|
||||||
startDate: formatIsoDate(startDate) ?? undefined,
|
startDate: formatIsoDate(theStartDate) ?? undefined,
|
||||||
endDate: formatIsoDate(endDate) ?? undefined,
|
endDate: formatIsoDate(theEndDate) ?? undefined,
|
||||||
}),
|
}),
|
||||||
toFirstPage,
|
toFirstPage,
|
||||||
);
|
);
|
||||||
const setSearch = pipe(
|
const setSearch = pipe(
|
||||||
(searchTerm: string) => isEmpty(searchTerm) ? undefined : searchTerm,
|
(searchTerm: string) => (isEmpty(searchTerm) ? undefined : searchTerm),
|
||||||
(search) => toFirstPage({ search }),
|
(searchTerm) => toFirstPage({ search: searchTerm }),
|
||||||
);
|
);
|
||||||
const removeTag = pipe(
|
const removeTag = pipe(
|
||||||
(tag: string) => tags.filter((selectedTag) => selectedTag !== tag),
|
(tag: string) => tags.filter((selectedTag) => selectedTag !== tag),
|
||||||
|
@ -53,8 +53,8 @@ const ShortUrlsFilteringBar = (
|
||||||
);
|
);
|
||||||
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
|
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
|
||||||
const toggleTagsMode = pipe(
|
const toggleTagsMode = pipe(
|
||||||
() => tagsMode === 'any' ? 'all' : 'any',
|
() => (tagsMode === 'any' ? 'all' : 'any'),
|
||||||
(tagsMode) => toFirstPage({ tagsMode }),
|
(mode) => toFirstPage({ tagsMode: mode }),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -30,8 +30,8 @@ const ShortUrlsList = (
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
const { page } = useParams();
|
const { page } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [{ tags, search, startDate, endDate, orderBy, tagsMode }, toFirstPage ] = useShortUrlsQuery();
|
const [{ tags, search, startDate, endDate, orderBy, tagsMode }, toFirstPage] = useShortUrlsQuery();
|
||||||
const [ actualOrderBy, setActualOrderBy ] = useState(
|
const [actualOrderBy, setActualOrderBy] = useState(
|
||||||
// This separated state handling is needed to be able to fall back to settings value, but only once when loaded
|
// This separated state handling is needed to be able to fall back to settings value, but only once when loaded
|
||||||
orderBy ?? settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING,
|
orderBy ?? settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING,
|
||||||
);
|
);
|
||||||
|
@ -45,7 +45,7 @@ const ShortUrlsList = (
|
||||||
const renderOrderIcon = (field: ShortUrlsOrderableFields) =>
|
const renderOrderIcon = (field: ShortUrlsOrderableFields) =>
|
||||||
<TableOrderIcon currentOrder={actualOrderBy} field={field} />;
|
<TableOrderIcon currentOrder={actualOrderBy} field={field} />;
|
||||||
const addTag = pipe(
|
const addTag = pipe(
|
||||||
(newTag: string) => [ ...new Set([ ...tags, newTag ]) ],
|
(newTag: string) => [...new Set([...tags, newTag])],
|
||||||
(updatedTags) => toFirstPage({ tags: updatedTags }),
|
(updatedTags) => toFirstPage({ tags: updatedTags }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ const ShortUrlsList = (
|
||||||
orderBy: actualOrderBy,
|
orderBy: actualOrderBy,
|
||||||
tagsMode,
|
tagsMode,
|
||||||
});
|
});
|
||||||
}, [ page, search, tags, startDate, endDate, actualOrderBy, tagsMode ]);
|
}, [page, search, tags, startDate, endDate, actualOrderBy, tagsMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -82,6 +82,6 @@ const ShortUrlsList = (
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => [ Topics.visits ]);
|
}, () => [Topics.visits]);
|
||||||
|
|
||||||
export default ShortUrlsList;
|
export default ShortUrlsList;
|
||||||
|
|
|
@ -70,11 +70,11 @@ export const ShortUrlsTable = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
|
||||||
<th className={orderableColumnsClasses} onClick={orderByColumn?.('shortCode')}>
|
<th className={orderableColumnsClasses} onClick={orderByColumn?.('shortCode')}>
|
||||||
Short URL {renderOrderIcon?.('shortCode')}
|
Short URL {renderOrderIcon?.('shortCode')}
|
||||||
</th>
|
</th>
|
||||||
{!supportsTitle && (
|
{!supportsTitle ? (
|
||||||
<th className={orderableColumnsClasses} onClick={orderByColumn?.('longUrl')}>
|
<th className={orderableColumnsClasses} onClick={orderByColumn?.('longUrl')}>
|
||||||
Long URL {renderOrderIcon?.('longUrl')}
|
Long URL {renderOrderIcon?.('longUrl')}
|
||||||
</th>
|
</th>
|
||||||
) || (
|
) : (
|
||||||
<th className="short-urls-table__header-cell">
|
<th className="short-urls-table__header-cell">
|
||||||
<span className={actionableFieldClasses} onClick={orderByColumn?.('title')}>
|
<span className={actionableFieldClasses} onClick={orderByColumn?.('title')}>
|
||||||
Title {renderOrderIcon?.('title')}
|
Title {renderOrderIcon?.('title')}
|
||||||
|
|
|
@ -37,7 +37,7 @@ const InfoModal = ({ isOpen, toggle }: { isOpen: boolean; toggle: () => void })
|
||||||
);
|
);
|
||||||
|
|
||||||
const UseExistingIfFoundInfoIcon = () => {
|
const UseExistingIfFoundInfoIcon = () => {
|
||||||
const [ isModalOpen, toggleModal ] = useToggle();
|
const [isModalOpen, toggleModal] = useToggle();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -19,7 +19,7 @@ export interface CreateShortUrlResultProps extends ShortUrlCreation {
|
||||||
const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
||||||
{ error, errorData, result, resetCreateShortUrl, canBeClosed = false }: CreateShortUrlResultProps,
|
{ error, errorData, result, resetCreateShortUrl, canBeClosed = false }: CreateShortUrlResultProps,
|
||||||
) => {
|
) => {
|
||||||
const [ showCopyTooltip, setShowCopyTooltip ] = useStateFlagTimeout();
|
const [showCopyTooltip, setShowCopyTooltip] = useStateFlagTimeout();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetCreateShortUrl();
|
resetCreateShortUrl();
|
||||||
|
|
|
@ -17,7 +17,7 @@ interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps {
|
||||||
const DeleteShortUrlModal = (
|
const DeleteShortUrlModal = (
|
||||||
{ shortUrl, toggle, isOpen, shortUrlDeletion, resetDeleteShortUrl, deleteShortUrl }: DeleteShortUrlModalConnectProps,
|
{ shortUrl, toggle, isOpen, shortUrlDeletion, resetDeleteShortUrl, deleteShortUrl }: DeleteShortUrlModalConnectProps,
|
||||||
) => {
|
) => {
|
||||||
const [ inputValue, setInputValue ] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
useEffect(() => resetDeleteShortUrl, []);
|
useEffect(() => resetDeleteShortUrl, []);
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const ExportShortUrlsBtn = (
|
||||||
{ exportShortUrls }: ReportExporter,
|
{ exportShortUrls }: ReportExporter,
|
||||||
): FC<ExportShortUrlsBtnConnectProps> => ({ amount = 0, selectedServer }) => {
|
): FC<ExportShortUrlsBtnConnectProps> => ({ amount = 0, selectedServer }) => {
|
||||||
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
|
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
|
||||||
const [ loading,, startLoading, stopLoading ] = useToggle();
|
const [loading,, startLoading, stopLoading] = useToggle();
|
||||||
const exportAllUrls = async () => {
|
const exportAllUrls = async () => {
|
||||||
if (!isServerWithId(selectedServer)) {
|
if (!isServerWithId(selectedServer)) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -25,28 +25,28 @@ interface QrCodeModalConnectProps extends ShortUrlModalProps {
|
||||||
const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Versions>) => ( // eslint-disable-line
|
const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Versions>) => ( // eslint-disable-line
|
||||||
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps,
|
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps,
|
||||||
) => {
|
) => {
|
||||||
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 capabilities: QrCodeCapabilities = useMemo(() => ({
|
const capabilities: QrCodeCapabilities = useMemo(() => ({
|
||||||
useSizeInPath: !supportsQrCodeSizeInQuery(selectedServer),
|
useSizeInPath: !supportsQrCodeSizeInQuery(selectedServer),
|
||||||
marginIsSupported: supportsQrCodeMargin(selectedServer),
|
marginIsSupported: supportsQrCodeMargin(selectedServer),
|
||||||
errorCorrectionIsSupported: supportsQrErrorCorrection(selectedServer),
|
errorCorrectionIsSupported: supportsQrErrorCorrection(selectedServer),
|
||||||
}), [ selectedServer ]);
|
}), [selectedServer]);
|
||||||
const willRenderThreeControls = capabilities.marginIsSupported !== capabilities.errorCorrectionIsSupported;
|
const willRenderThreeControls = capabilities.marginIsSupported !== capabilities.errorCorrectionIsSupported;
|
||||||
const qrCodeUrl = useMemo(
|
const qrCodeUrl = useMemo(
|
||||||
() => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }, capabilities),
|
() => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }, capabilities),
|
||||||
[ shortUrl, size, format, margin, errorCorrection, capabilities ],
|
[shortUrl, size, format, margin, errorCorrection, capabilities],
|
||||||
);
|
);
|
||||||
const totalSize = useMemo(() => size + margin, [ size, margin ]);
|
const totalSize = useMemo(() => size + margin, [size, margin]);
|
||||||
const modalSize = useMemo(() => {
|
const modalSize = useMemo(() => {
|
||||||
if (totalSize < 500) {
|
if (totalSize < 500) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalSize < 800 ? 'lg' : 'xl';
|
return totalSize < 800 ? 'lg' : 'xl';
|
||||||
}, [ totalSize ]);
|
}, [totalSize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered size={modalSize}>
|
<Modal isOpen={isOpen} toggle={toggle} centered size={modalSize}>
|
||||||
|
@ -69,8 +69,9 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
{capabilities.marginIsSupported && (
|
{capabilities.marginIsSupported && (
|
||||||
<FormGroup className={`d-grid ${willRenderThreeControls ? 'col-md-4' : 'col-md-6'}`}>
|
<FormGroup className={`d-grid ${willRenderThreeControls ? 'col-md-4' : 'col-md-6'}`}>
|
||||||
<label>Margin: {margin}px</label>
|
<label htmlFor="marginControl">Margin: {margin}px</label>
|
||||||
<input
|
<input
|
||||||
|
id="marginControl"
|
||||||
type="range"
|
type="range"
|
||||||
className="form-control-range"
|
className="form-control-range"
|
||||||
value={margin}
|
value={margin}
|
||||||
|
@ -101,7 +102,9 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={async () => imageDownloader.saveImage(qrCodeUrl, `${shortCode}-qr-code.${format}`)}
|
onClick={() => {
|
||||||
|
imageDownloader.saveImage(qrCodeUrl, `${shortCode}-qr-code.${format}`).catch(() => {});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Download <FontAwesomeIcon icon={downloadIcon} className="ms-1" />
|
Download <FontAwesomeIcon icon={downloadIcon} className="ms-1" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -23,8 +23,8 @@ const ShortUrlsRow = (
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator: ColorGenerator,
|
||||||
useStateFlagTimeout: StateFlagTimeout,
|
useStateFlagTimeout: StateFlagTimeout,
|
||||||
) => ({ shortUrl, selectedServer, onTagClick }: ShortUrlsRowProps) => {
|
) => ({ shortUrl, selectedServer, onTagClick }: ShortUrlsRowProps) => {
|
||||||
const [ copiedToClipboard, setCopiedToClipboard ] = useStateFlagTimeout();
|
const [copiedToClipboard, setCopiedToClipboard] = useStateFlagTimeout();
|
||||||
const [ active, setActive ] = useStateFlagTimeout(false, 500);
|
const [active, setActive] = useStateFlagTimeout(false, 500);
|
||||||
const isFirstRun = useRef(true);
|
const isFirstRun = useRef(true);
|
||||||
|
|
||||||
const renderTags = (tags: string[]) => {
|
const renderTags = (tags: string[]) => {
|
||||||
|
@ -48,7 +48,7 @@ const ShortUrlsRow = (
|
||||||
} else {
|
} else {
|
||||||
setActive();
|
setActive();
|
||||||
}
|
}
|
||||||
}, [ shortUrl.visitsCount ]);
|
}, [shortUrl.visitsCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="responsive-table__row">
|
<tr className="responsive-table__row">
|
||||||
|
|
|
@ -23,9 +23,9 @@ const ShortUrlsRowMenu = (
|
||||||
DeleteShortUrlModal: ShortUrlModal,
|
DeleteShortUrlModal: ShortUrlModal,
|
||||||
QrCodeModal: ShortUrlModal,
|
QrCodeModal: ShortUrlModal,
|
||||||
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
|
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
|
||||||
const [ isOpen, toggle ] = useToggle();
|
const [isOpen, toggle] = useToggle();
|
||||||
const [ isQrModalOpen, toggleQrCode ] = useToggle();
|
const [isQrModalOpen, toggleQrCode] = useToggle();
|
||||||
const [ isDeleteModalOpen, toggleDelete ] = useToggle();
|
const [isDeleteModalOpen, toggleDelete] = useToggle();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownBtnMenu toggle={toggle} isOpen={isOpen}>
|
<DropdownBtnMenu toggle={toggle} isOpen={isOpen}>
|
||||||
|
|
|
@ -8,11 +8,6 @@ import { TagsFilteringMode } from '../../api/types';
|
||||||
|
|
||||||
type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
|
type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
|
||||||
|
|
||||||
export interface ShortUrlListRouteParams {
|
|
||||||
page: string;
|
|
||||||
serverId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ShortUrlsQueryCommon {
|
interface ShortUrlsQueryCommon {
|
||||||
search?: string;
|
search?: string;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
|
@ -45,7 +40,7 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
||||||
return { ...rest, orderBy: parsedOrderBy, tags: parsedTags };
|
return { ...rest, orderBy: parsedOrderBy, tags: parsedTags };
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
[ location.search ],
|
[location.search],
|
||||||
);
|
);
|
||||||
const toFirstPageWithExtra = (extra: Partial<ShortUrlsFiltering>) => {
|
const toFirstPageWithExtra = (extra: Partial<ShortUrlsFiltering>) => {
|
||||||
const { orderBy, tags, ...mergedQuery } = { ...query, ...extra };
|
const { orderBy, tags, ...mergedQuery } = { ...query, ...extra };
|
||||||
|
@ -57,8 +52,8 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
||||||
const evolvedQuery = stringifyQuery(normalizedQuery);
|
const evolvedQuery = stringifyQuery(normalizedQuery);
|
||||||
const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`;
|
const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`;
|
||||||
|
|
||||||
navigate(`/server/${params.serverId}/list-short-urls/1${queryString}`);
|
navigate(`/server/${params.serverId ?? ''}/list-short-urls/1${queryString}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return [ query, toFirstPageWithExtra ];
|
return [query, toFirstPageWithExtra];
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,12 +7,10 @@ import { ProblemDetailsError } from '../../api/types';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ApiErrorAction } from '../../api/types/actions';
|
import { ApiErrorAction } from '../../api/types/actions';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START';
|
export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START';
|
||||||
export const CREATE_SHORT_URL_ERROR = 'shlink/createShortUrl/CREATE_SHORT_URL_ERROR';
|
export const CREATE_SHORT_URL_ERROR = 'shlink/createShortUrl/CREATE_SHORT_URL_ERROR';
|
||||||
export const CREATE_SHORT_URL = 'shlink/createShortUrl/CREATE_SHORT_URL';
|
export const CREATE_SHORT_URL = 'shlink/createShortUrl/CREATE_SHORT_URL';
|
||||||
export const RESET_CREATE_SHORT_URL = 'shlink/createShortUrl/RESET_CREATE_SHORT_URL';
|
export const RESET_CREATE_SHORT_URL = 'shlink/createShortUrl/RESET_CREATE_SHORT_URL';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface ShortUrlCreation {
|
export interface ShortUrlCreation {
|
||||||
result: ShortUrl | null;
|
result: ShortUrl | null;
|
||||||
|
@ -43,10 +41,10 @@ export const createShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
|
||||||
getState: GetState,
|
getState: GetState,
|
||||||
) => {
|
) => {
|
||||||
dispatch({ type: CREATE_SHORT_URL_START });
|
dispatch({ type: CREATE_SHORT_URL_START });
|
||||||
const { createShortUrl } = buildShlinkApiClient(getState);
|
const { createShortUrl: shlinkCreateShortUrl } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await createShortUrl(data);
|
const result = await shlinkCreateShortUrl(data);
|
||||||
|
|
||||||
dispatch<CreateShortUrlAction>({ type: CREATE_SHORT_URL, result });
|
dispatch<CreateShortUrlAction>({ type: CREATE_SHORT_URL, result });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
@ -6,12 +6,10 @@ import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilde
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ApiErrorAction } from '../../api/types/actions';
|
import { ApiErrorAction } from '../../api/types/actions';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
|
export const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
|
||||||
export const DELETE_SHORT_URL_ERROR = 'shlink/deleteShortUrl/DELETE_SHORT_URL_ERROR';
|
export const DELETE_SHORT_URL_ERROR = 'shlink/deleteShortUrl/DELETE_SHORT_URL_ERROR';
|
||||||
export const SHORT_URL_DELETED = 'shlink/deleteShortUrl/SHORT_URL_DELETED';
|
export const SHORT_URL_DELETED = 'shlink/deleteShortUrl/SHORT_URL_DELETED';
|
||||||
export const RESET_DELETE_SHORT_URL = 'shlink/deleteShortUrl/RESET_DELETE_SHORT_URL';
|
export const RESET_DELETE_SHORT_URL = 'shlink/deleteShortUrl/RESET_DELETE_SHORT_URL';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface ShortUrlDeletion {
|
export interface ShortUrlDeletion {
|
||||||
shortCode: string;
|
shortCode: string;
|
||||||
|
@ -43,10 +41,10 @@ export const deleteShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
|
||||||
domain?: string | null,
|
domain?: string | null,
|
||||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
dispatch({ type: DELETE_SHORT_URL_START });
|
dispatch({ type: DELETE_SHORT_URL_START });
|
||||||
const { deleteShortUrl } = buildShlinkApiClient(getState);
|
const { deleteShortUrl: shlinkDeleteShortUrl } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteShortUrl(shortCode, domain);
|
await shlinkDeleteShortUrl(shortCode, domain);
|
||||||
dispatch<DeleteShortUrlAction>({ type: SHORT_URL_DELETED, shortCode, domain });
|
dispatch<DeleteShortUrlAction>({ type: SHORT_URL_DELETED, shortCode, domain });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
dispatch<ApiErrorAction>({ type: DELETE_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
dispatch<ApiErrorAction>({ type: DELETE_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
||||||
|
|
|
@ -9,11 +9,9 @@ import { ProblemDetailsError } from '../../api/types';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ApiErrorAction } from '../../api/types/actions';
|
import { ApiErrorAction } from '../../api/types/actions';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const GET_SHORT_URL_DETAIL_START = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_START';
|
export const GET_SHORT_URL_DETAIL_START = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_START';
|
||||||
export const GET_SHORT_URL_DETAIL_ERROR = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_ERROR';
|
export const GET_SHORT_URL_DETAIL_ERROR = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_ERROR';
|
||||||
export const GET_SHORT_URL_DETAIL = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL';
|
export const GET_SHORT_URL_DETAIL = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface ShortUrlDetail {
|
export interface ShortUrlDetail {
|
||||||
shortUrl?: ShortUrl;
|
shortUrl?: ShortUrl;
|
||||||
|
@ -46,7 +44,7 @@ export const getShortUrlDetail = (buildShlinkApiClient: ShlinkApiClientBuilder)
|
||||||
try {
|
try {
|
||||||
const { shortUrlsList } = getState();
|
const { shortUrlsList } = getState();
|
||||||
const shortUrl = shortUrlsList?.shortUrls?.data.find(
|
const shortUrl = shortUrlsList?.shortUrls?.data.find(
|
||||||
(shortUrl) => shortUrlMatches(shortUrl, shortCode, domain),
|
(url) => shortUrlMatches(url, shortCode, domain),
|
||||||
) ?? await buildShlinkApiClient(getState).getShortUrl(shortCode, domain);
|
) ?? await buildShlinkApiClient(getState).getShortUrl(shortCode, domain);
|
||||||
|
|
||||||
dispatch<ShortUrlDetailAction>({ shortUrl, type: GET_SHORT_URL_DETAIL });
|
dispatch<ShortUrlDetailAction>({ shortUrl, type: GET_SHORT_URL_DETAIL });
|
||||||
|
|
|
@ -9,11 +9,9 @@ import { parseApiError } from '../../api/utils';
|
||||||
import { supportsTagsInPatch } from '../../utils/helpers/features';
|
import { supportsTagsInPatch } from '../../utils/helpers/features';
|
||||||
import { ApiErrorAction } from '../../api/types/actions';
|
import { ApiErrorAction } from '../../api/types/actions';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const EDIT_SHORT_URL_START = 'shlink/shortUrlEdition/EDIT_SHORT_URL_START';
|
export const EDIT_SHORT_URL_START = 'shlink/shortUrlEdition/EDIT_SHORT_URL_START';
|
||||||
export const EDIT_SHORT_URL_ERROR = 'shlink/shortUrlEdition/EDIT_SHORT_URL_ERROR';
|
export const EDIT_SHORT_URL_ERROR = 'shlink/shortUrlEdition/EDIT_SHORT_URL_ERROR';
|
||||||
export const SHORT_URL_EDITED = 'shlink/shortUrlEdition/SHORT_URL_EDITED';
|
export const SHORT_URL_EDITED = 'shlink/shortUrlEdition/SHORT_URL_EDITED';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface ShortUrlEdition {
|
export interface ShortUrlEdition {
|
||||||
shortUrl?: ShortUrl;
|
shortUrl?: ShortUrl;
|
||||||
|
@ -49,7 +47,7 @@ export const editShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||||
const { updateShortUrl, updateShortUrlTags } = buildShlinkApiClient(getState);
|
const { updateShortUrl, updateShortUrlTags } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [ shortUrl ] = await Promise.all([
|
const [shortUrl] = await Promise.all([
|
||||||
updateShortUrl(shortCode, domain, data as any), // FIXME Parse dates
|
updateShortUrl(shortCode, domain, data as any), // FIXME Parse dates
|
||||||
sendTagsSeparately && data.tags ? updateShortUrlTags(shortCode, domain, data.tags) : undefined,
|
sendTagsSeparately && data.tags ? updateShortUrlTags(shortCode, domain, data.tags) : undefined,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -10,11 +10,9 @@ import { DeleteShortUrlAction, SHORT_URL_DELETED } from './shortUrlDeletion';
|
||||||
import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation';
|
import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation';
|
||||||
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
|
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START';
|
export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START';
|
||||||
export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR';
|
export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR';
|
||||||
export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
|
export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export const ITEMS_IN_OVERVIEW_PAGE = 5;
|
export const ITEMS_IN_OVERVIEW_PAGE = 5;
|
||||||
|
|
||||||
|
@ -46,19 +44,19 @@ export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
|
||||||
[LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true }),
|
[LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true }),
|
||||||
[LIST_SHORT_URLS]: (_, { shortUrls }) => ({ loading: false, error: false, shortUrls }),
|
[LIST_SHORT_URLS]: (_, { shortUrls }) => ({ loading: false, error: false, shortUrls }),
|
||||||
[SHORT_URL_DELETED]: pipe(
|
[SHORT_URL_DELETED]: pipe(
|
||||||
(state: ShortUrlsList, { shortCode, domain }: DeleteShortUrlAction) => !state.shortUrls ? state : assocPath(
|
(state: ShortUrlsList, { shortCode, domain }: DeleteShortUrlAction) => (!state.shortUrls ? state : assocPath(
|
||||||
[ 'shortUrls', 'data' ],
|
['shortUrls', 'data'],
|
||||||
reject((shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), state.shortUrls.data),
|
reject((shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), state.shortUrls.data),
|
||||||
state,
|
state,
|
||||||
),
|
)),
|
||||||
(state) => !state.shortUrls ? state : assocPath(
|
(state) => (!state.shortUrls ? state : assocPath(
|
||||||
[ 'shortUrls', 'pagination', 'totalItems' ],
|
['shortUrls', 'pagination', 'totalItems'],
|
||||||
state.shortUrls.pagination.totalItems - 1,
|
state.shortUrls.pagination.totalItems - 1,
|
||||||
state,
|
state,
|
||||||
),
|
)),
|
||||||
),
|
),
|
||||||
[CREATE_VISITS]: (state, { createdVisits }) => assocPath(
|
[CREATE_VISITS]: (state, { createdVisits }) => assocPath(
|
||||||
[ 'shortUrls', 'data' ],
|
['shortUrls', 'data'],
|
||||||
state.shortUrls?.data?.map(
|
state.shortUrls?.data?.map(
|
||||||
(currentShortUrl) => {
|
(currentShortUrl) => {
|
||||||
// Find the last of the new visit for this short URL, and pick the amount of visits from it
|
// Find the last of the new visit for this short URL, and pick the amount of visits from it
|
||||||
|
@ -79,36 +77,36 @@ export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
|
||||||
// The only place where the list and the creation form coexist is the overview page.
|
// The only place where the list and the creation form coexist is the overview page.
|
||||||
// There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL.
|
// There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL.
|
||||||
// We can also remove the items above the amount that is displayed there.
|
// We can also remove the items above the amount that is displayed there.
|
||||||
(state: ShortUrlsList, { result }: CreateShortUrlAction) => !state.shortUrls ? state : assocPath(
|
(state: ShortUrlsList, { result }: CreateShortUrlAction) => (!state.shortUrls ? state : assocPath(
|
||||||
[ 'shortUrls', 'data' ],
|
['shortUrls', 'data'],
|
||||||
[ result, ...state.shortUrls.data.slice(0, ITEMS_IN_OVERVIEW_PAGE - 1) ],
|
[result, ...state.shortUrls.data.slice(0, ITEMS_IN_OVERVIEW_PAGE - 1)],
|
||||||
state,
|
state,
|
||||||
),
|
)),
|
||||||
(state: ShortUrlsList) => !state.shortUrls ? state : assocPath(
|
(state: ShortUrlsList) => (!state.shortUrls ? state : assocPath(
|
||||||
[ 'shortUrls', 'pagination', 'totalItems' ],
|
['shortUrls', 'pagination', 'totalItems'],
|
||||||
state.shortUrls.pagination.totalItems + 1,
|
state.shortUrls.pagination.totalItems + 1,
|
||||||
state,
|
state,
|
||||||
),
|
)),
|
||||||
),
|
),
|
||||||
[SHORT_URL_EDITED]: (state, { shortUrl: editedShortUrl }) => !state.shortUrls ? state : assocPath(
|
[SHORT_URL_EDITED]: (state, { shortUrl: editedShortUrl }) => (!state.shortUrls ? state : assocPath(
|
||||||
[ 'shortUrls', 'data' ],
|
['shortUrls', 'data'],
|
||||||
state.shortUrls.data.map((shortUrl) => {
|
state.shortUrls.data.map((shortUrl) => {
|
||||||
const { shortCode, domain } = editedShortUrl;
|
const { shortCode, domain } = editedShortUrl;
|
||||||
|
|
||||||
return shortUrlMatches(shortUrl, shortCode, domain) ? editedShortUrl : shortUrl;
|
return shortUrlMatches(shortUrl, shortCode, domain) ? editedShortUrl : shortUrl;
|
||||||
}),
|
}),
|
||||||
state,
|
state,
|
||||||
),
|
)),
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||||
params: ShlinkShortUrlsListParams = {},
|
params: ShlinkShortUrlsListParams = {},
|
||||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
dispatch({ type: LIST_SHORT_URLS_START });
|
dispatch({ type: LIST_SHORT_URLS_START });
|
||||||
const { listShortUrls } = buildShlinkApiClient(getState);
|
const { listShortUrls: shlinkListShortUrls } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const shortUrls = await listShortUrls(params);
|
const shortUrls = await shlinkListShortUrls(params);
|
||||||
|
|
||||||
dispatch<ListShortUrlsAction>({ type: LIST_SHORT_URLS, shortUrls });
|
dispatch<ListShortUrlsAction>({ type: LIST_SHORT_URLS, shortUrls });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -22,8 +22,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'ShortUrlsFilteringBar');
|
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'ShortUrlsFilteringBar');
|
||||||
bottle.decorator('ShortUrlsList', connect(
|
bottle.decorator('ShortUrlsList', connect(
|
||||||
[ 'selectedServer', 'mercureInfo', 'shortUrlsList', 'settings' ],
|
['selectedServer', 'mercureInfo', 'shortUrlsList', 'settings'],
|
||||||
[ 'listShortUrls', 'createNewVisits', 'loadMercureInfo' ],
|
['listShortUrls', 'createNewVisits', 'loadMercureInfo'],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow');
|
bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow');
|
||||||
|
@ -35,25 +35,25 @@ 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([ 'shortUrlCreationResult', 'selectedServer', 'settings' ], [ 'createShortUrl', 'resetCreateShortUrl' ]),
|
connect(['shortUrlCreationResult', 'selectedServer', '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', 'selectedServer', 'settings'],
|
||||||
[ 'getShortUrlDetail', 'editShortUrl' ],
|
['getShortUrlDetail', 'editShortUrl'],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal);
|
bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal);
|
||||||
bottle.decorator('DeleteShortUrlModal', connect([ 'shortUrlDeletion' ], [ 'deleteShortUrl', 'resetDeleteShortUrl' ]));
|
bottle.decorator('DeleteShortUrlModal', connect(['shortUrlDeletion'], ['deleteShortUrl', 'resetDeleteShortUrl']));
|
||||||
|
|
||||||
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader', 'ForServerVersion');
|
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader', 'ForServerVersion');
|
||||||
bottle.decorator('QrCodeModal', connect([ 'selectedServer' ]));
|
bottle.decorator('QrCodeModal', connect(['selectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator', 'ExportShortUrlsBtn');
|
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator', 'ExportShortUrlsBtn');
|
||||||
|
|
||||||
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'buildShlinkApiClient', 'ReportExporter');
|
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'buildShlinkApiClient', 'ReportExporter');
|
||||||
bottle.decorator('ExportShortUrlsBtn', connect([ 'selectedServer' ]));
|
bottle.decorator('ExportShortUrlsBtn', connect(['selectedServer']));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
|
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
|
||||||
|
|
|
@ -25,9 +25,9 @@ const TagCard = (
|
||||||
EditTagModal: FC<TagModalProps>,
|
EditTagModal: FC<TagModalProps>,
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator: ColorGenerator,
|
||||||
) => ({ tag, selectedServer, displayed, toggle }: TagCardProps) => {
|
) => ({ tag, selectedServer, displayed, toggle }: TagCardProps) => {
|
||||||
const [ isDeleteModalOpen, toggleDelete ] = useToggle();
|
const [isDeleteModalOpen, toggleDelete] = useToggle();
|
||||||
const [ isEditModalOpen, toggleEdit ] = useToggle();
|
const [isEditModalOpen, toggleEdit] = useToggle();
|
||||||
const [ hasTitle,, displayTitle ] = useToggle();
|
const [hasTitle,, displayTitle] = useToggle();
|
||||||
const titleRef = useRef<HTMLElement>();
|
const titleRef = useRef<HTMLElement>();
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ const TagCard = (
|
||||||
if (isTruncated(titleRef.current)) {
|
if (isTruncated(titleRef.current)) {
|
||||||
displayTitle();
|
displayTitle();
|
||||||
}
|
}
|
||||||
}, [ titleRef.current ]);
|
}, [titleRef.current]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="tag-card">
|
<Card className="tag-card">
|
||||||
|
|
|
@ -8,7 +8,7 @@ const { ceil } = Math;
|
||||||
const TAGS_GROUPS_AMOUNT = 4;
|
const TAGS_GROUPS_AMOUNT = 4;
|
||||||
|
|
||||||
export const TagsCards = (TagCard: FC<TagCardProps>): FC<TagsListChildrenProps> => ({ sortedTags, selectedServer }) => {
|
export const TagsCards = (TagCard: FC<TagCardProps>): FC<TagsListChildrenProps> => ({ sortedTags, selectedServer }) => {
|
||||||
const [ displayedTag, setDisplayedTag ] = useState<string | undefined>();
|
const [displayedTag, setDisplayedTag] = useState<string | undefined>();
|
||||||
const tagsCount = sortedTags.length;
|
const tagsCount = sortedTags.length;
|
||||||
const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), sortedTags);
|
const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), sortedTags);
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,8 @@ export interface TagsListProps {
|
||||||
const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
||||||
{ filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps,
|
{ filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps,
|
||||||
) => {
|
) => {
|
||||||
const [ mode, setMode ] = useState<TagsMode>(settings.tags?.defaultMode ?? 'cards');
|
const [mode, setMode] = useState<TagsMode>(settings.tags?.defaultMode ?? 'cards');
|
||||||
const [ order, setOrder ] = useState<TagsOrder>(settings.tags?.defaultOrdering ?? {});
|
const [order, setOrder] = useState<TagsOrder>(settings.tags?.defaultOrdering ?? {});
|
||||||
const resolveSortedTags = pipe(
|
const resolveSortedTags = pipe(
|
||||||
() => tagsList.filteredTags.map((tag): NormalizedTag => ({
|
() => tagsList.filteredTags.map((tag): NormalizedTag => ({
|
||||||
tag,
|
tag,
|
||||||
|
@ -103,6 +103,6 @@ const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableP
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => [ Topics.visits ]);
|
}, () => [Topics.visits]);
|
||||||
|
|
||||||
export default TagsList;
|
export default TagsList;
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
|
||||||
const isFirstLoad = useRef(true);
|
const isFirstLoad = useRef(true);
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const { page: pageFromQuery = 1 } = parseQuery<{ page?: number | string }>(search);
|
const { page: pageFromQuery = 1 } = parseQuery<{ page?: number | string }>(search);
|
||||||
const [ page, setPage ] = useQueryState<number>('page', Number(pageFromQuery));
|
const [page, setPage] = useQueryState<number>('page', Number(pageFromQuery));
|
||||||
const pages = splitEvery(TAGS_PER_PAGE, sortedTags);
|
const pages = splitEvery(TAGS_PER_PAGE, sortedTags);
|
||||||
const showPaginator = pages.length > 1;
|
const showPaginator = pages.length > 1;
|
||||||
const currentPage = pages[page - 1] ?? [];
|
const currentPage = pages[page - 1] ?? [];
|
||||||
|
@ -31,10 +31,10 @@ export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!isFirstLoad.current && setPage(1);
|
!isFirstLoad.current && setPage(1);
|
||||||
isFirstLoad.current = false;
|
isFirstLoad.current = false;
|
||||||
}, [ sortedTags ]);
|
}, [sortedTags]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollTo(0, 0);
|
scrollTo(0, 0);
|
||||||
}, [ page ]);
|
}, [page]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleCard key={page} bodyClassName={showPaginator ? 'pb-1' : ''}>
|
<SimpleCard key={page} bodyClassName={showPaginator ? 'pb-1' : ''}>
|
||||||
|
@ -50,9 +50,9 @@ export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
|
||||||
<th className="tags-table__header-cell text-lg-end" 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 aria-label="Options" className="tags-table__header-cell" />
|
||||||
</tr>
|
</tr>
|
||||||
<tr><th colSpan={4} className="p-0 border-top-0" /></tr>
|
<tr><th aria-label="Separator" colSpan={4} className="p-0 border-top-0" /></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{currentPage.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>}
|
{currentPage.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>}
|
||||||
|
|
|
@ -21,9 +21,9 @@ export const TagsTableRow = (
|
||||||
EditTagModal: FC<TagModalProps>,
|
EditTagModal: FC<TagModalProps>,
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator: ColorGenerator,
|
||||||
) => ({ tag, selectedServer }: TagsTableRowProps) => {
|
) => ({ tag, selectedServer }: TagsTableRowProps) => {
|
||||||
const [ isDeleteModalOpen, toggleDelete ] = useToggle();
|
const [isDeleteModalOpen, toggleDelete] = useToggle();
|
||||||
const [ isEditModalOpen, toggleEdit ] = useToggle();
|
const [isEditModalOpen, toggleEdit] = useToggle();
|
||||||
const [ isDropdownOpen, toggleDropdown ] = useToggle();
|
const [isDropdownOpen, toggleDropdown] = useToggle();
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import { TagDeletion } from '../reducers/tagDelete';
|
import { TagDeletion } from '../reducers/tagDelete';
|
||||||
import { TagModalProps } from '../data';
|
import { TagModalProps } from '../data';
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
|
@ -34,10 +34,10 @@ const DeleteTagConfirmModal = (
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
<Button color="link" onClick={toggle}>Cancel</Button>
|
||||||
<button className="btn btn-danger" disabled={deleting} onClick={doDelete}>
|
<Button color="danger" disabled={deleting} onClick={doDelete}>
|
||||||
{deleting ? 'Deleting tag...' : 'Delete tag'}
|
{deleting ? 'Deleting tag...' : 'Delete tag'}
|
||||||
</button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,9 +21,9 @@ interface EditTagModalProps extends TagModalProps {
|
||||||
const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
|
const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
|
||||||
{ tag, editTag, toggle, tagEdited, isOpen, tagEdit }: EditTagModalProps,
|
{ tag, editTag, toggle, tagEdited, isOpen, tagEdit }: EditTagModalProps,
|
||||||
) => {
|
) => {
|
||||||
const [ newTagName, setNewTagName ] = useState(tag);
|
const [newTagName, setNewTagName] = useState(tag);
|
||||||
const [ color, setColor ] = useState(getColorForKey(tag));
|
const [color, setColor] = useState(getColorForKey(tag));
|
||||||
const [ showColorPicker, toggleColorPicker, , hideColorPicker ] = useToggle();
|
const [showColorPicker, toggleColorPicker, , hideColorPicker] = useToggle();
|
||||||
const { editing, error, errorData } = tagEdit;
|
const { editing, error, errorData } = tagEdit;
|
||||||
const saveTag = handleEventPreventingDefault(
|
const saveTag = handleEventPreventingDefault(
|
||||||
async () => editTag(tag, newTagName, color)
|
async () => editTag(tag, newTagName, color)
|
||||||
|
|
|
@ -13,7 +13,7 @@ export interface TagsSelectorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TagsSelectorConnectProps extends TagsSelectorProps {
|
interface TagsSelectorConnectProps extends TagsSelectorProps {
|
||||||
listTags: Function;
|
listTags: () => void;
|
||||||
tagsList: TagsList;
|
tagsList: TagsList;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
@ -47,14 +47,14 @@ const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||||
addOnBlur
|
addOnBlur
|
||||||
placeholderText={placeholder ?? 'Add tags to the URL'}
|
placeholderText={placeholder ?? 'Add tags to the URL'}
|
||||||
minQueryLength={1}
|
minQueryLength={1}
|
||||||
delimiters={[ 'Enter', 'Tab', ',' ]}
|
delimiters={['Enter', 'Tab', ',']}
|
||||||
suggestionsTransform={
|
suggestionsTransform={
|
||||||
searchMode === 'includes'
|
searchMode === 'includes'
|
||||||
? (query, suggestions) => suggestions.filter(({ name }) => name.includes(query))
|
? (query, suggestions) => suggestions.filter(({ name }) => name.includes(query))
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onDelete={(removedTagIndex) => {
|
onDelete={(removedTagIndex) => {
|
||||||
const tagsCopy = [ ...selectedTags ];
|
const tagsCopy = [...selectedTags];
|
||||||
|
|
||||||
tagsCopy.splice(removedTagIndex, 1);
|
tagsCopy.splice(removedTagIndex, 1);
|
||||||
onChange(tagsCopy);
|
onChange(tagsCopy);
|
||||||
|
@ -62,7 +62,7 @@ const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||||
onAddition={({ name: newTag }) => onChange(
|
onAddition={({ name: newTag }) => onChange(
|
||||||
// * Avoid duplicated tags (thanks to the Set),
|
// * Avoid duplicated tags (thanks to the Set),
|
||||||
// * Split any of the new tags by comma, allowing to paste multiple comma-separated tags at once.
|
// * Split any of the new tags by comma, allowing to paste multiple comma-separated tags at once.
|
||||||
[ ...new Set([ ...selectedTags, ...newTag.toLowerCase().split(',') ]) ],
|
[...new Set([...selectedTags, ...newTag.toLowerCase().split(',')])],
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,12 +6,10 @@ import { ProblemDetailsError } from '../../api/types';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ApiErrorAction } from '../../api/types/actions';
|
import { ApiErrorAction } from '../../api/types/actions';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
|
export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
|
||||||
export const DELETE_TAG_ERROR = 'shlink/deleteTag/DELETE_TAG_ERROR';
|
export const DELETE_TAG_ERROR = 'shlink/deleteTag/DELETE_TAG_ERROR';
|
||||||
export const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG';
|
export const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG';
|
||||||
export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED';
|
export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export interface TagDeletion {
|
export interface TagDeletion {
|
||||||
deleting: boolean;
|
deleting: boolean;
|
||||||
|
@ -42,7 +40,7 @@ export const deleteTag = (buildShlinkApiClient: ShlinkApiClientBuilder) => (tag:
|
||||||
const { deleteTags } = buildShlinkApiClient(getState);
|
const { deleteTags } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteTags([ tag ]);
|
await deleteTags([tag]);
|
||||||
dispatch({ type: DELETE_TAG });
|
dispatch({ type: DELETE_TAG });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
dispatch<ApiErrorAction>({ type: DELETE_TAG_ERROR, errorData: parseApiError(e) });
|
dispatch<ApiErrorAction>({ type: DELETE_TAG_ERROR, errorData: parseApiError(e) });
|
||||||
|
|
|
@ -8,11 +8,9 @@ import { ProblemDetailsError } from '../../api/types';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ApiErrorAction } from '../../api/types/actions';
|
import { ApiErrorAction } from '../../api/types/actions';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
||||||
export const EDIT_TAG_ERROR = 'shlink/editTag/EDIT_TAG_ERROR';
|
export const EDIT_TAG_ERROR = 'shlink/editTag/EDIT_TAG_ERROR';
|
||||||
export const EDIT_TAG = 'shlink/editTag/EDIT_TAG';
|
export const EDIT_TAG = 'shlink/editTag/EDIT_TAG';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
export const TAG_EDITED = 'shlink/editTag/TAG_EDITED';
|
export const TAG_EDITED = 'shlink/editTag/TAG_EDITED';
|
||||||
|
|
||||||
|
@ -41,7 +39,7 @@ export default buildReducer<TagEdition, EditTagAction & ApiErrorAction>({
|
||||||
[EDIT_TAG_START]: (state) => ({ ...state, editing: true, error: false }),
|
[EDIT_TAG_START]: (state) => ({ ...state, editing: true, error: false }),
|
||||||
[EDIT_TAG_ERROR]: (state, { errorData }) => ({ ...state, editing: false, error: true, errorData }),
|
[EDIT_TAG_ERROR]: (state, { errorData }) => ({ ...state, editing: false, error: true, errorData }),
|
||||||
[EDIT_TAG]: (_, action) => ({
|
[EDIT_TAG]: (_, action) => ({
|
||||||
...pick([ 'oldName', 'newName' ], action),
|
...pick(['oldName', 'newName'], action),
|
||||||
editing: false,
|
editing: false,
|
||||||
error: false,
|
error: false,
|
||||||
}),
|
}),
|
||||||
|
@ -53,10 +51,10 @@ export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGener
|
||||||
color: string,
|
color: string,
|
||||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
dispatch({ type: EDIT_TAG_START });
|
dispatch({ type: EDIT_TAG_START });
|
||||||
const { editTag } = buildShlinkApiClient(getState);
|
const { editTag: shlinkEditTag } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await editTag(oldName, newName);
|
await shlinkEditTag(oldName, newName);
|
||||||
colorGenerator.setColorForKey(newName, color);
|
colorGenerator.setColorForKey(newName, color);
|
||||||
dispatch({ type: EDIT_TAG, oldName, newName });
|
dispatch({ type: EDIT_TAG, oldName, newName });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
@ -13,12 +13,10 @@ import { CREATE_SHORT_URL, CreateShortUrlAction } from '../../short-urls/reducer
|
||||||
import { DeleteTagAction, TAG_DELETED } from './tagDelete';
|
import { DeleteTagAction, TAG_DELETED } from './tagDelete';
|
||||||
import { EditTagAction, TAG_EDITED } from './tagEdit';
|
import { EditTagAction, TAG_EDITED } from './tagEdit';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
|
export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
|
||||||
export const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR';
|
export const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR';
|
||||||
export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
|
export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
|
||||||
export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
|
export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
type TagsStatsMap = Record<string, TagStats>;
|
type TagsStatsMap = Record<string, TagStats>;
|
||||||
|
|
||||||
|
@ -58,19 +56,19 @@ const initialState = {
|
||||||
|
|
||||||
type TagIncrease = [string, number];
|
type TagIncrease = [string, number];
|
||||||
|
|
||||||
const renameTag = (oldName: string, newName: string) => (tag: string) => tag === oldName ? newName : tag;
|
const renameTag = (oldName: string, newName: string) => (tag: string) => (tag === oldName ? newName : tag);
|
||||||
const rejectTag = (tags: string[], tagToReject: string) => reject((tag) => tag === tagToReject, tags);
|
const rejectTag = (tags: string[], tagToReject: string) => reject((tag) => tag === tagToReject, tags);
|
||||||
const increaseVisitsForTags = (tags: TagIncrease[], stats: TagsStatsMap) => tags.reduce((stats, [ tag, increase ]) => {
|
const increaseVisitsForTags = (tags: TagIncrease[], stats: TagsStatsMap) => tags.reduce((theStats, [tag, increase]) => {
|
||||||
if (!stats[tag]) {
|
if (!theStats[tag]) {
|
||||||
return stats;
|
return theStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagStats = stats[tag];
|
const tagStats = theStats[tag];
|
||||||
|
|
||||||
tagStats.visitsCount = tagStats.visitsCount + increase;
|
tagStats.visitsCount += increase;
|
||||||
stats[tag] = tagStats;
|
theStats[tag] = tagStats; // eslint-disable-line no-param-reassign
|
||||||
|
|
||||||
return stats;
|
return theStats;
|
||||||
}, { ...stats });
|
}, { ...stats });
|
||||||
const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => Object.entries(
|
const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => Object.entries(
|
||||||
createdVisits.reduce<Stats>((acc, { shortUrl }) => {
|
createdVisits.reduce<Stats>((acc, { shortUrl }) => {
|
||||||
|
@ -123,8 +121,8 @@ export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = t
|
||||||
dispatch({ type: LIST_TAGS_START });
|
dispatch({ type: LIST_TAGS_START });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { listTags } = buildShlinkApiClient(getState);
|
const { listTags: shlinkListTags } = buildShlinkApiClient(getState);
|
||||||
const { tags, stats = [] }: ShlinkTags = await listTags();
|
const { tags, stats = [] }: ShlinkTags = await shlinkListTags();
|
||||||
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, shortUrlsCount, visitsCount }) => {
|
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, shortUrlsCount, visitsCount }) => {
|
||||||
acc[tag] = { shortUrlsCount, visitsCount };
|
acc[tag] = { shortUrlsCount, visitsCount };
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,15 @@ import { TagsTableRow } from '../TagsTableRow';
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator');
|
bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator');
|
||||||
bottle.decorator('TagsSelector', connect([ 'tagsList', 'settings' ], [ 'listTags' ]));
|
bottle.decorator('TagsSelector', connect(['tagsList', 'settings'], ['listTags']));
|
||||||
|
|
||||||
bottle.serviceFactory('TagCard', TagCard, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator');
|
bottle.serviceFactory('TagCard', TagCard, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator');
|
||||||
|
|
||||||
bottle.serviceFactory('DeleteTagConfirmModal', () => DeleteTagConfirmModal);
|
bottle.serviceFactory('DeleteTagConfirmModal', () => DeleteTagConfirmModal);
|
||||||
bottle.decorator('DeleteTagConfirmModal', connect([ 'tagDelete' ], [ 'deleteTag', 'tagDeleted' ]));
|
bottle.decorator('DeleteTagConfirmModal', connect(['tagDelete'], ['deleteTag', 'tagDeleted']));
|
||||||
|
|
||||||
bottle.serviceFactory('EditTagModal', EditTagModal, 'ColorGenerator');
|
bottle.serviceFactory('EditTagModal', EditTagModal, 'ColorGenerator');
|
||||||
bottle.decorator('EditTagModal', connect([ 'tagEdit' ], [ 'editTag', 'tagEdited' ]));
|
bottle.decorator('EditTagModal', connect(['tagEdit'], ['editTag', 'tagEdited']));
|
||||||
|
|
||||||
bottle.serviceFactory('TagsCards', TagsCards, 'TagCard');
|
bottle.serviceFactory('TagsCards', TagsCards, 'TagCard');
|
||||||
bottle.serviceFactory('TagsTableRow', TagsTableRow, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator');
|
bottle.serviceFactory('TagsTableRow', TagsTableRow, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator');
|
||||||
|
@ -32,8 +32,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
|
|
||||||
bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable');
|
bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable');
|
||||||
bottle.decorator('TagsList', connect(
|
bottle.decorator('TagsList', connect(
|
||||||
[ 'tagsList', 'selectedServer', 'mercureInfo', 'settings' ],
|
['tagsList', 'selectedServer', 'mercureInfo', 'settings'],
|
||||||
[ 'forceListTags', 'filterTags', 'createNewVisits', 'loadMercureInfo' ],
|
['forceListTags', 'filterTags', 'createNewVisits', 'loadMercureInfo'],
|
||||||
));
|
));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
|
@ -15,7 +15,7 @@ export interface DropdownBtnProps {
|
||||||
export const DropdownBtn: FC<DropdownBtnProps> = (
|
export const DropdownBtn: FC<DropdownBtnProps> = (
|
||||||
{ text, disabled = false, className = '', children, dropdownClassName, right = false, minWidth },
|
{ text, disabled = false, className = '', children, dropdownClassName, right = false, minWidth },
|
||||||
) => {
|
) => {
|
||||||
const [ isOpen, toggle ] = useToggle();
|
const [isOpen, toggle] = useToggle();
|
||||||
const toggleClasses = `dropdown-btn__toggle btn-block ${className}`;
|
const toggleClasses = `dropdown-btn__toggle btn-block ${className}`;
|
||||||
const style = { minWidth: minWidth && `${minWidth}px` };
|
const style = { minWidth: minWidth && `${minWidth}px` };
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ export function OrderingDropdown<T extends string = string>(
|
||||||
end={right}
|
end={right}
|
||||||
className={classNames('w-100', { 'ordering-dropdown__menu--link': !isButton })}
|
className={classNames('w-100', { 'ordering-dropdown__menu--link': !isButton })}
|
||||||
>
|
>
|
||||||
{toPairs(items).map(([ fieldKey, fieldValue ]) => (
|
{toPairs(items).map(([fieldKey, fieldValue]) => (
|
||||||
<DropdownItem key={fieldKey} active={order.field === fieldKey} onClick={handleItemClick(fieldKey as T)}>
|
<DropdownItem key={fieldKey} active={order.field === fieldKey} onClick={handleItemClick(fieldKey as T)}>
|
||||||
{fieldValue}
|
{fieldValue}
|
||||||
{order.field === fieldKey && (
|
{order.field === fieldKey && (
|
||||||
|
|
|
@ -16,7 +16,7 @@ interface SearchFieldProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchField = ({ onChange, className, large = true, noBorder = false, initialValue = '' }: SearchFieldProps) => {
|
const SearchField = ({ onChange, className, large = true, noBorder = false, initialValue = '' }: SearchFieldProps) => {
|
||||||
const [ searchTerm, setSearchTerm ] = useState(initialValue);
|
const [searchTerm, setSearchTerm] = useState(initialValue);
|
||||||
|
|
||||||
const resetTimer = () => {
|
const resetTimer = () => {
|
||||||
timer && clearTimeout(timer);
|
timer && clearTimeout(timer);
|
||||||
|
|
|
@ -25,8 +25,8 @@ export const DateRangeSelector = (
|
||||||
{ onDatesChange, initialDateRange, defaultText, disabled, updatable = false }: DateRangeSelectorProps,
|
{ onDatesChange, initialDateRange, defaultText, disabled, updatable = false }: DateRangeSelectorProps,
|
||||||
) => {
|
) => {
|
||||||
const initialIntervalIsRange = rangeIsInterval(initialDateRange);
|
const initialIntervalIsRange = rangeIsInterval(initialDateRange);
|
||||||
const [ activeInterval, setActiveInterval ] = useState(initialIntervalIsRange ? initialDateRange : undefined);
|
const [activeInterval, setActiveInterval] = useState(initialIntervalIsRange ? initialDateRange : undefined);
|
||||||
const [ activeDateRange, setActiveDateRange ] = useState(initialIntervalIsRange ? undefined : initialDateRange);
|
const [activeDateRange, setActiveDateRange] = useState(initialIntervalIsRange ? undefined : initialDateRange);
|
||||||
|
|
||||||
const updateDateRange = (dateRange: DateRange) => {
|
const updateDateRange = (dateRange: DateRange) => {
|
||||||
setActiveInterval(dateRangeIsEmpty(dateRange) ? 'all' : undefined);
|
setActiveInterval(dateRangeIsEmpty(dateRange) ? 'all' : undefined);
|
||||||
|
@ -44,7 +44,7 @@ export const DateRangeSelector = (
|
||||||
|
|
||||||
isDateInterval && updateInterval(initialDateRange);
|
isDateInterval && updateInterval(initialDateRange);
|
||||||
initialDateRange && !isDateInterval && updateDateRange(initialDateRange);
|
initialDateRange && !isDateInterval && updateDateRange(initialDateRange);
|
||||||
}, [ initialDateRange ]);
|
}, [initialDateRange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownBtn disabled={disabled} text={rangeOrIntervalToString(activeInterval ?? activeDateRange) ?? defaultText}>
|
<DropdownBtn disabled={disabled} text={rangeOrIntervalToString(activeInterval ?? activeDateRange) ?? defaultText}>
|
||||||
|
|
|
@ -88,13 +88,13 @@ export const dateToMatchingInterval = (date: DateOrString): DateInterval => {
|
||||||
const theDate: Date = parseISO(date);
|
const theDate: Date = parseISO(date);
|
||||||
|
|
||||||
return cond<never, DateInterval>([
|
return cond<never, DateInterval>([
|
||||||
[ () => isBeforeOrEqual(startOfDay(new Date()), theDate), () => 'today' ],
|
[() => isBeforeOrEqual(startOfDay(new Date()), theDate), () => 'today'],
|
||||||
[ () => isBeforeOrEqual(startOfDaysAgo(1), theDate), () => 'yesterday' ],
|
[() => isBeforeOrEqual(startOfDaysAgo(1), theDate), () => 'yesterday'],
|
||||||
[ () => isBeforeOrEqual(startOfDaysAgo(7), theDate), () => 'last7Days' ],
|
[() => isBeforeOrEqual(startOfDaysAgo(7), theDate), () => 'last7Days'],
|
||||||
[ () => isBeforeOrEqual(startOfDaysAgo(30), theDate), () => 'last30Days' ],
|
[() => isBeforeOrEqual(startOfDaysAgo(30), theDate), () => 'last30Days'],
|
||||||
[ () => isBeforeOrEqual(startOfDaysAgo(90), theDate), () => 'last90Days' ],
|
[() => isBeforeOrEqual(startOfDaysAgo(90), theDate), () => 'last90Days'],
|
||||||
[ () => isBeforeOrEqual(startOfDaysAgo(180), theDate), () => 'last180Days' ],
|
[() => isBeforeOrEqual(startOfDaysAgo(180), theDate), () => 'last180Days'],
|
||||||
[ () => isBeforeOrEqual(startOfDaysAgo(365), theDate), () => 'last365Days' ],
|
[() => isBeforeOrEqual(startOfDaysAgo(365), theDate), () => 'last365Days'],
|
||||||
[ T, () => 'all' ],
|
[T, () => 'all'],
|
||||||
])();
|
])();
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { LabeledFormGroup } from './LabeledFormGroup';
|
||||||
export interface InputFormGroupProps {
|
export interface InputFormGroupProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (newValue: string) => void;
|
onChange: (newValue: string) => void;
|
||||||
id?: string;
|
|
||||||
type?: InputType;
|
type?: InputType;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
|
|
@ -7,6 +7,7 @@ interface LabeledFormGroupProps {
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||||
export const LabeledFormGroup: FC<LabeledFormGroupProps> = (
|
export const LabeledFormGroup: FC<LabeledFormGroupProps> = (
|
||||||
{ children, label, className = '', labelClassName = '', noMargin = false },
|
{ children, label, className = '', labelClassName = '', noMargin = false },
|
||||||
) => (
|
) => (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ActiveElement, ChartEvent, ChartType, TooltipItem } from 'chart.js';
|
import { ActiveElement, ChartEvent, ChartType, TooltipItem } from 'chart.js';
|
||||||
import { prettify } from './numbers';
|
import { prettify } from './numbers';
|
||||||
|
|
||||||
export const pointerOnHover = ({ native }: ChartEvent, [ firstElement ]: ActiveElement[]) => {
|
export const pointerOnHover = ({ native }: ChartEvent, [firstElement]: ActiveElement[]) => {
|
||||||
if (!native?.target) {
|
if (!native?.target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,15 @@ const formatDateFromFormat = (date?: NullableDate, theFormat?: string): Optional
|
||||||
return theFormat ? format(date, theFormat) : formatISO(date);
|
return theFormat ? format(date, theFormat) : formatISO(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatDate = (format = 'yyyy-MM-dd') => (date?: NullableDate) => formatDateFromFormat(date, format);
|
export const formatDate = (theFormat = 'yyyy-MM-dd') => (date?: NullableDate) => formatDateFromFormat(date, theFormat);
|
||||||
|
|
||||||
export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date, undefined);
|
export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date, undefined);
|
||||||
|
|
||||||
export const formatInternational = formatDate();
|
export const formatInternational = formatDate();
|
||||||
|
|
||||||
export const parseDate = (date: string, format: string) => parse(date, format, new Date());
|
export const parseDate = (date: string, theFormat: string) => parse(date, theFormat, new Date());
|
||||||
|
|
||||||
export const parseISO = (date: DateOrString): Date => isDateObject(date) ? date : stdParseISO(date);
|
export const parseISO = (date: DateOrString): Date => (isDateObject(date) ? date : stdParseISO(date));
|
||||||
|
|
||||||
export const isBetween = (date: DateOrString, start?: DateOrString, end?: DateOrString): boolean => {
|
export const isBetween = (date: DateOrString, start?: DateOrString, end?: DateOrString): boolean => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -5,27 +5,15 @@ const serverMatchesVersions = (versions: Versions) => (selectedServer: SelectedS
|
||||||
isReachableServer(selectedServer) && versionMatch(selectedServer.version, versions);
|
isReachableServer(selectedServer) && versionMatch(selectedServer.version, versions);
|
||||||
|
|
||||||
export const supportsQrCodeSizeInQuery = serverMatchesVersions({ minVersion: '2.5.0' });
|
export const supportsQrCodeSizeInQuery = serverMatchesVersions({ minVersion: '2.5.0' });
|
||||||
|
|
||||||
export const supportsShortUrlTitle = serverMatchesVersions({ minVersion: '2.6.0' });
|
export const supportsShortUrlTitle = serverMatchesVersions({ minVersion: '2.6.0' });
|
||||||
|
|
||||||
export const supportsOrphanVisits = supportsShortUrlTitle;
|
export const supportsOrphanVisits = supportsShortUrlTitle;
|
||||||
|
|
||||||
export const supportsQrCodeMargin = supportsShortUrlTitle;
|
export const supportsQrCodeMargin = supportsShortUrlTitle;
|
||||||
|
|
||||||
export const supportsTagsInPatch = supportsShortUrlTitle;
|
export const supportsTagsInPatch = supportsShortUrlTitle;
|
||||||
|
|
||||||
export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' });
|
export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' });
|
||||||
|
|
||||||
export const supportsCrawlableVisits = supportsBotVisits;
|
export const supportsCrawlableVisits = supportsBotVisits;
|
||||||
|
|
||||||
export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2.8.0' });
|
export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2.8.0' });
|
||||||
|
|
||||||
export const supportsDomainRedirects = supportsQrErrorCorrection;
|
export const supportsDomainRedirects = supportsQrErrorCorrection;
|
||||||
|
|
||||||
export const supportsForwardQuery = serverMatchesVersions({ minVersion: '2.9.0' });
|
export const supportsForwardQuery = serverMatchesVersions({ minVersion: '2.9.0' });
|
||||||
|
|
||||||
export const supportsDefaultDomainRedirectsEdition = serverMatchesVersions({ minVersion: '2.10.0' });
|
export const supportsDefaultDomainRedirectsEdition = serverMatchesVersions({ minVersion: '2.10.0' });
|
||||||
|
|
||||||
export const supportsNonOrphanVisits = serverMatchesVersions({ minVersion: '3.0.0' });
|
export const supportsNonOrphanVisits = serverMatchesVersions({ minVersion: '3.0.0' });
|
||||||
|
|
||||||
export const supportsAllTagsFiltering = supportsNonOrphanVisits;
|
export const supportsAllTagsFiltering = supportsNonOrphanVisits;
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const saveUrl = ({ document }: Window, url: string, filename: string) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveCsv = (window: Window, csv: string, filename: string) => {
|
export const saveCsv = (window: Window, csv: string, filename: string) => {
|
||||||
const blob = new Blob([ csv ], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
saveUrl(window, url, filename);
|
saveUrl(window, url, filename);
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const useStateFlagTimeout = (
|
||||||
setTimeout: (callback: Function, timeout: number) => number,
|
setTimeout: (callback: Function, timeout: number) => number,
|
||||||
clearTimeout: (timer: number) => void,
|
clearTimeout: (timer: number) => void,
|
||||||
): StateFlagTimeout => (initialValue = false, delay = DEFAULT_DELAY) => {
|
): StateFlagTimeout => (initialValue = false, delay = DEFAULT_DELAY) => {
|
||||||
const [ flag, setFlag ] = useState<boolean>(initialValue);
|
const [flag, setFlag] = useState<boolean>(initialValue);
|
||||||
const timeout = useRef<number | undefined>(undefined);
|
const timeout = useRef<number | undefined>(undefined);
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
setFlag(!initialValue);
|
setFlag(!initialValue);
|
||||||
|
@ -23,15 +23,15 @@ export const useStateFlagTimeout = (
|
||||||
timeout.current = setTimeout(() => setFlag(initialValue), delay);
|
timeout.current = setTimeout(() => setFlag(initialValue), delay);
|
||||||
};
|
};
|
||||||
|
|
||||||
return [ flag, callback ];
|
return [flag, callback];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ToggleResult = [ boolean, () => void, () => void, () => void ];
|
type ToggleResult = [ boolean, () => void, () => void, () => void ];
|
||||||
|
|
||||||
export const useToggle = (initialValue = false): ToggleResult => {
|
export const useToggle = (initialValue = false): ToggleResult => {
|
||||||
const [ flag, setFlag ] = useState<boolean>(initialValue);
|
const [flag, setFlag] = useState<boolean>(initialValue);
|
||||||
|
|
||||||
return [ flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false) ];
|
return [flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false)];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSwipeable = (showSidebar: () => void, hideSidebar: () => void) => {
|
export const useSwipeable = (showSidebar: () => void, hideSidebar: () => void) => {
|
||||||
|
@ -55,17 +55,17 @@ export const useSwipeable = (showSidebar: () => void, hideSidebar: () => void) =
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useQueryState = <T>(paramName: string, initialState: T): [ T, (newValue: T) => void ] => {
|
export const useQueryState = <T>(paramName: string, initialState: T): [ T, (newValue: T) => void ] => {
|
||||||
const [ value, setValue ] = useState(initialState);
|
const [value, setValue] = useState(initialState);
|
||||||
const setValueWithLocation = (value: T) => {
|
const setValueWithLocation = (valueToSet: T) => {
|
||||||
const { location, history } = window;
|
const { location, history } = window;
|
||||||
const query = parseQuery<any>(location.search);
|
const query = parseQuery<any>(location.search);
|
||||||
|
|
||||||
query[paramName] = value;
|
query[paramName] = valueToSet;
|
||||||
history.pushState(null, '', `${location.pathname}?${stringifyQuery(query)}`);
|
history.pushState(null, '', `${location.pathname}?${stringifyQuery(query)}`);
|
||||||
setValue(value);
|
setValue(valueToSet);
|
||||||
};
|
};
|
||||||
|
|
||||||
return [ value, setValueWithLocation ];
|
return [value, setValueWithLocation];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useEffectExceptFirstTime = (callback: EffectCallback, deps: DependencyList): void => {
|
export const useEffectExceptFirstTime = (callback: EffectCallback, deps: DependencyList): void => {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import marker from 'leaflet/dist/images/marker-icon.png';
|
||||||
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
||||||
|
|
||||||
export const fixLeafletIcons = () => {
|
export const fixLeafletIcons = () => {
|
||||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
delete (L.Icon.Default.prototype as any)._getIconUrl; // eslint-disable-line no-underscore-dangle
|
||||||
|
|
||||||
L.Icon.Default.mergeOptions({
|
L.Icon.Default.mergeOptions({
|
||||||
iconRetinaUrl: marker2x,
|
iconRetinaUrl: marker2x,
|
||||||
|
|
|
@ -22,20 +22,20 @@ export const determineOrderDir = <T extends string = string>(
|
||||||
return currentOrderDir ? newOrderMap[currentOrderDir] : 'ASC';
|
return currentOrderDir ? newOrderMap[currentOrderDir] : 'ASC';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sortList = <List>(list: List[], { field, dir }: Order<Partial<keyof List>>) => !field || !dir
|
export const sortList = <List>(list: List[], { field, dir }: Order<Partial<keyof List>>) => (
|
||||||
? list
|
!field || !dir ? list : list.sort((a, b) => {
|
||||||
: list.sort((a, b) => {
|
|
||||||
const greaterThan = dir === 'ASC' ? 1 : -1;
|
const greaterThan = dir === 'ASC' ? 1 : -1;
|
||||||
const smallerThan = dir === 'ASC' ? -1 : 1;
|
const smallerThan = dir === 'ASC' ? -1 : 1;
|
||||||
|
|
||||||
return a[field] > b[field] ? greaterThan : smallerThan;
|
return a[field] > b[field] ? greaterThan : smallerThan;
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const orderToString = <T>(order: Order<T>): string | undefined =>
|
export const orderToString = <T>(order: Order<T>): string | undefined => (
|
||||||
order.dir ? `${order.field}-${order.dir}` : undefined;
|
order.dir ? `${order.field}-${order.dir}` : undefined
|
||||||
|
);
|
||||||
|
|
||||||
export const stringToOrder = <T>(order: string): Order<T> => {
|
export const stringToOrder = <T>(order: string): Order<T> => {
|
||||||
const [ field, dir ] = order.split('-') as [ T | undefined, OrderDir | undefined ];
|
const [field, dir] = order.split('-') as [ T | undefined, OrderDir | undefined ];
|
||||||
|
|
||||||
return { field, dir };
|
return { field, dir };
|
||||||
};
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue