diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f23a750..1023daa8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,6 @@ jobs: ci: uses: shlinkio/github-actions/.github/workflows/web-app-ci.yml@main with: - node-version: 18.12 + node-version: 20.2 publish-coverage: true force-install: true diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 43cc780a..02d402ae 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -9,14 +9,14 @@ jobs: continue-on-error: true steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} - name: Use node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: - node-version: 18.12 + node-version: 20.2 - name: Build run: | npm ci --force && \ diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 554783a1..6241ec6c 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -10,11 +10,11 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Use node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: - node-version: 18.12 + node-version: 20.2 - name: Generate release assets run: npm ci --force && VERSION=${GITHUB_REF#refs/tags/v} npm run build:dist - name: Publish release with assets diff --git a/Dockerfile b/Dockerfile index 61f29c6a..214c2292 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.12-alpine as node +FROM node:20.2-alpine as node COPY . /shlink-web-client ARG VERSION="latest" ENV VERSION ${VERSION} diff --git a/docker-compose.yml b/docker-compose.yml index dbe6884c..b9bbf511 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '3' services: shlink_web_client_node: container_name: shlink_web_client_node - image: node:18.12-alpine + image: node:20.2-alpine command: /bin/sh -c "cd /home/shlink/www && npm install --force && npm run start" volumes: - ./:/home/shlink/www diff --git a/src/domains/helpers/DomainDropdown.tsx b/src/domains/helpers/DomainDropdown.tsx index 6d7ba670..8c99e2ac 100644 --- a/src/domains/helpers/DomainDropdown.tsx +++ b/src/domains/helpers/DomainDropdown.tsx @@ -5,9 +5,9 @@ import { Link } from 'react-router-dom'; import { DropdownItem } from 'reactstrap'; import type { SelectedServer } from '../../servers/data'; import { getServerId } from '../../servers/data'; -import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu'; import { useFeature } from '../../utils/helpers/features'; import { useToggle } from '../../utils/helpers/hooks'; +import { RowDropdownBtn } from '../../utils/RowDropdownBtn'; import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits'; import type { Domain } from '../data'; import type { EditDomainRedirects } from '../reducers/domainRedirects'; @@ -20,7 +20,6 @@ interface DomainDropdownProps { } export const DomainDropdown: FC = ({ domain, editDomainRedirects, selectedServer }) => { - const [isOpen, toggle] = useToggle(); const [isModalOpen, toggleModal] = useToggle(); const { isDefault } = domain; const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition', selectedServer); @@ -28,7 +27,7 @@ export const DomainDropdown: FC = ({ domain, editDomainRedi const serverId = getServerId(selectedServer); return ( - + {withVisits && ( = ({ domain, editDomainRedi toggle={toggleModal} editDomainRedirects={editDomainRedirects} /> - + ); }; diff --git a/src/servers/ManageServersRowDropdown.tsx b/src/servers/ManageServersRowDropdown.tsx index 357c0126..9e058df0 100644 --- a/src/servers/ManageServersRowDropdown.tsx +++ b/src/servers/ManageServersRowDropdown.tsx @@ -9,8 +9,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import type { FC } from 'react'; import { Link } from 'react-router-dom'; import { DropdownItem } from 'reactstrap'; -import { DropdownBtnMenu } from '../utils/DropdownBtnMenu'; import { useToggle } from '../utils/helpers/hooks'; +import { RowDropdownBtn } from '../utils/RowDropdownBtn'; import type { ServerWithId } from './data'; import type { DeleteServerModalProps } from './DeleteServerModal'; @@ -25,14 +25,13 @@ interface ManageServersRowDropdownConnectProps extends ManageServersRowDropdownP export const ManageServersRowDropdown = ( DeleteServerModal: FC, ): FC => ({ server, setAutoConnect }) => { - const [isMenuOpen, toggleMenu] = useToggle(); const [isModalOpen,, showModal, hideModal] = useToggle(); const serverUrl = `/server/${server.id}`; const { autoConnect: isAutoConnect } = server; const autoConnectIcon = isAutoConnect ? toggleOffIcon : toggleOnIcon; return ( - + Connect @@ -48,6 +47,6 @@ export const ManageServersRowDropdown = ( - + ); }; diff --git a/src/short-urls/helpers/ShortUrlsFilterDropdown.tsx b/src/short-urls/helpers/ShortUrlsFilterDropdown.tsx index 3e1366a6..16e47395 100644 --- a/src/short-urls/helpers/ShortUrlsFilterDropdown.tsx +++ b/src/short-urls/helpers/ShortUrlsFilterDropdown.tsx @@ -17,7 +17,7 @@ export const ShortUrlsFilterDropdown = ( const onFilterClick = (key: keyof ShortUrlsFilter) => () => onChange({ ...selected, [key]: !selected?.[key] }); return ( - + Visits: Ignore visits from bots diff --git a/src/short-urls/helpers/ShortUrlsRow.tsx b/src/short-urls/helpers/ShortUrlsRow.tsx index 5e3d4748..ebc5a968 100644 --- a/src/short-urls/helpers/ShortUrlsRow.tsx +++ b/src/short-urls/helpers/ShortUrlsRow.tsx @@ -87,7 +87,7 @@ export const ShortUrlsRow = ( - + diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.tsx b/src/short-urls/helpers/ShortUrlsRowMenu.tsx index 7b08fe89..c7a20b35 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.tsx +++ b/src/short-urls/helpers/ShortUrlsRowMenu.tsx @@ -8,8 +8,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import type { FC } from 'react'; import { DropdownItem } from 'reactstrap'; import type { SelectedServer } from '../../servers/data'; -import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu'; import { useToggle } from '../../utils/helpers/hooks'; +import { RowDropdownBtn } from '../../utils/RowDropdownBtn'; import type { ShortUrl, ShortUrlModalProps } from '../data'; import { ShortUrlDetailLink } from './ShortUrlDetailLink'; @@ -23,12 +23,11 @@ export const ShortUrlsRowMenu = ( DeleteShortUrlModal: ShortUrlModal, QrCodeModal: ShortUrlModal, ) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => { - const [isOpen, toggle] = useToggle(); const [isQrModalOpen,, openQrCodeModal, closeQrCodeModal] = useToggle(); const [isDeleteModalOpen,, openDeleteModal, closeDeleteModal] = useToggle(); return ( - + Visit stats @@ -48,7 +47,7 @@ export const ShortUrlsRowMenu = ( Delete short URL - + ); }; diff --git a/src/tags/TagsTableRow.tsx b/src/tags/TagsTableRow.tsx index d3257bae..b643a388 100644 --- a/src/tags/TagsTableRow.tsx +++ b/src/tags/TagsTableRow.tsx @@ -5,9 +5,9 @@ import { Link } from 'react-router-dom'; import { DropdownItem } from 'reactstrap'; import type { SelectedServer } from '../servers/data'; import { getServerId } from '../servers/data'; -import { DropdownBtnMenu } from '../utils/DropdownBtnMenu'; import { useToggle } from '../utils/helpers/hooks'; import { prettify } from '../utils/helpers/numbers'; +import { RowDropdownBtn } from '../utils/RowDropdownBtn'; import type { ColorGenerator } from '../utils/services/ColorGenerator'; import type { SimplifiedTag, TagModalProps } from './data'; import { TagBullet } from './helpers/TagBullet'; @@ -24,7 +24,6 @@ export const TagsTableRow = ( ) => ({ tag, selectedServer }: TagsTableRowProps) => { const [isDeleteModalOpen, toggleDelete] = useToggle(); const [isEditModalOpen, toggleEdit] = useToggle(); - const [isDropdownOpen, toggleDropdown] = useToggle(); const serverId = getServerId(selectedServer); return ( @@ -43,14 +42,14 @@ export const TagsTableRow = ( - + Edit Delete - + diff --git a/src/utils/DropdownBtn.scss b/src/utils/DropdownBtn.scss index f5c40c27..86395a84 100644 --- a/src/utils/DropdownBtn.scss +++ b/src/utils/DropdownBtn.scss @@ -2,13 +2,20 @@ @import '../utils/mixins/vertical-align'; +.dropdown-btn__toggle.dropdown-btn__toggle { + text-align: left; +} + +.dropdown-btn__toggle.dropdown-btn__toggle--with-caret { + padding-right: 1.75rem; +} + .dropdown-btn__toggle.dropdown-btn__toggle, .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled).active, .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):active, .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):focus, .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):hover, .show > .dropdown-btn__toggle.dropdown-btn__toggle.dropdown-toggle { - text-align: left; color: var(--input-text-color); background-color: var(--primary-color); border-color: var(--input-border-color); diff --git a/src/utils/DropdownBtn.tsx b/src/utils/DropdownBtn.tsx index 08e0ef4d..c6076a86 100644 --- a/src/utils/DropdownBtn.tsx +++ b/src/utils/DropdownBtn.tsx @@ -1,28 +1,45 @@ -import type { FC, PropsWithChildren } from 'react'; +import classNames from 'classnames'; +import type { FC, PropsWithChildren, ReactNode } from 'react'; import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap'; +import type { DropdownToggleProps } from 'reactstrap/types/lib/DropdownToggle'; import { useToggle } from './helpers/hooks'; import './DropdownBtn.scss'; -export type DropdownBtnProps = PropsWithChildren<{ - text: string; - disabled?: boolean; +export type DropdownBtnProps = PropsWithChildren & { + text: ReactNode; + noCaret?: boolean; className?: string; dropdownClassName?: string; - right?: boolean; + inline?: boolean; minWidth?: number; + size?: 'sm' | 'md' | 'lg'; }>; -export const DropdownBtn: FC = ( - { text, disabled = false, className = '', children, dropdownClassName, right = false, minWidth }, -) => { +export const DropdownBtn: FC = ({ + text, + disabled = false, + className, + children, + dropdownClassName, + noCaret, + end = false, + minWidth, + inline, + size, +}) => { const [isOpen, toggle] = useToggle(); - const toggleClasses = `dropdown-btn__toggle btn-block ${className}`; - const style = { minWidth: minWidth && `${minWidth}px` }; + const toggleClasses = classNames('dropdown-btn__toggle', className, { + 'btn-block': !inline, + 'dropdown-btn__toggle--with-caret': !noCaret, + }); + const menuStyle = { minWidth: minWidth && `${minWidth}px` }; return ( - {text} - {children} + + {text} + + {children} ); }; diff --git a/src/utils/DropdownBtnMenu.scss b/src/utils/DropdownBtnMenu.scss deleted file mode 100644 index 45b54a28..00000000 --- a/src/utils/DropdownBtnMenu.scss +++ /dev/null @@ -1,3 +0,0 @@ -.dropdown-btn-menu__dropdown-toggle:after { - display: none !important; -} diff --git a/src/utils/DropdownBtnMenu.tsx b/src/utils/DropdownBtnMenu.tsx deleted file mode 100644 index 330c5434..00000000 --- a/src/utils/DropdownBtnMenu.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { faEllipsisV as menuIcon } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import type { FC, PropsWithChildren } from 'react'; -import { ButtonDropdown, DropdownMenu, DropdownToggle } from 'reactstrap'; -import './DropdownBtnMenu.scss'; - -export type DropdownBtnMenuProps = PropsWithChildren<{ - isOpen: boolean; - toggle: () => void; - right?: boolean; -}>; - -export const DropdownBtnMenu: FC = ({ isOpen, toggle, children, right = true }) => ( - - -    - - {children} - -); diff --git a/src/utils/ExportBtn.tsx b/src/utils/ExportBtn.tsx index 351083c1..feee65d9 100644 --- a/src/utils/ExportBtn.tsx +++ b/src/utils/ExportBtn.tsx @@ -5,10 +5,10 @@ import type { ButtonProps } from 'reactstrap'; import { Button } from 'reactstrap'; import { prettify } from './helpers/numbers'; -interface ExportBtnProps extends Omit { +type ExportBtnProps = Omit & { amount?: number; loading?: boolean; -} +}; export const ExportBtn: FC = ({ amount = 0, loading = false, ...rest }) => (