diff --git a/CHANGELOG.md b/CHANGELOG.md index a1dea376..8ccdb0c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * *Nothing* ### Fixed -* *Nothing* +* [#589](https://github.com/shlinkio/shlink-web-client/pull/589) Fixed alignment of shlink versions footer, by basing the logic on the presence of the sidebar instead of selected server. ## [3.5.1] - 2022-01-08 diff --git a/src/common/MenuLayout.tsx b/src/common/MenuLayout.tsx index 567587b4..8e9ab6bd 100644 --- a/src/common/MenuLayout.tsx +++ b/src/common/MenuLayout.tsx @@ -11,6 +11,11 @@ import NotFound from './NotFound'; import { AsideMenuProps } from './AsideMenu'; import './MenuLayout.scss'; +interface MenuLayoutProps { + sidebarPresent: Function; + sidebarNotPresent: Function; +} + const MenuLayout = ( TagsList: FC, ShortUrlsList: FC, @@ -24,13 +29,19 @@ const MenuLayout = ( Overview: FC, EditShortUrl: FC, ManageDomains: FC, -) => withSelectedServer(({ selectedServer }) => { +) => withSelectedServer(({ selectedServer, sidebarNotPresent, sidebarPresent }) => { const location = useLocation(); const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle(); + const showContent = isReachableServer(selectedServer); useEffect(() => hideSidebar(), [ location ]); + useEffect(() => { + showContent && sidebarPresent(); - if (!isReachableServer(selectedServer)) { + return () => sidebarNotPresent(); + }, []); + + if (!showContent) { return ; } diff --git a/src/common/ShlinkVersions.tsx b/src/common/ShlinkVersions.tsx index 188f8f8d..50277ab0 100644 --- a/src/common/ShlinkVersions.tsx +++ b/src/common/ShlinkVersions.tsx @@ -1,13 +1,13 @@ import { pipe } from 'ramda'; import { ExternalLink } from 'react-external-link'; import { versionToPrintable, versionToSemVer } from '../utils/helpers/version'; -import { isReachableServer } from '../servers/data'; -import { ShlinkVersionsContainerProps } from './ShlinkVersionsContainer'; +import { isReachableServer, SelectedServer } from '../servers/data'; const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%'; const normalizeVersion = pipe(versionToSemVer(), versionToPrintable); -export interface ShlinkVersionsProps extends ShlinkVersionsContainerProps { +export interface ShlinkVersionsProps { + selectedServer: SelectedServer; clientVersion?: string; } diff --git a/src/common/ShlinkVersionsContainer.scss b/src/common/ShlinkVersionsContainer.scss index 296731cf..0ad96872 100644 --- a/src/common/ShlinkVersionsContainer.scss +++ b/src/common/ShlinkVersionsContainer.scss @@ -1,6 +1,6 @@ @import '../utils/base'; -.shlink-versions-container--with-server { +.shlink-versions-container--with-sidebar { margin-left: 0; @media (min-width: $mdMin) { diff --git a/src/common/ShlinkVersionsContainer.tsx b/src/common/ShlinkVersionsContainer.tsx index be3b9d74..378283e8 100644 --- a/src/common/ShlinkVersionsContainer.tsx +++ b/src/common/ShlinkVersionsContainer.tsx @@ -1,15 +1,17 @@ import classNames from 'classnames'; -import { isReachableServer, SelectedServer } from '../servers/data'; +import { SelectedServer } from '../servers/data'; import ShlinkVersions from './ShlinkVersions'; +import { Sidebar } from './reducers/sidebar'; import './ShlinkVersionsContainer.scss'; export interface ShlinkVersionsContainerProps { selectedServer: SelectedServer; + sidebar: Sidebar; } -const ShlinkVersionsContainer = ({ selectedServer }: ShlinkVersionsContainerProps) => { +const ShlinkVersionsContainer = ({ selectedServer, sidebar }: ShlinkVersionsContainerProps) => { const classes = classNames('text-center', { - 'shlink-versions-container--with-server': isReachableServer(selectedServer), + 'shlink-versions-container--with-sidebar': sidebar.sidebarPresent, }); return ( diff --git a/src/common/reducers/sidebar.ts b/src/common/reducers/sidebar.ts new file mode 100644 index 00000000..409ebb1e --- /dev/null +++ b/src/common/reducers/sidebar.ts @@ -0,0 +1,27 @@ +import { Action } from '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_NOT_PRESENT = 'shlink/common/SIDEBAR_NOT_PRESENT'; +/* eslint-enable padding-line-between-statements */ + +export interface Sidebar { + sidebarPresent: boolean; +} + +type SidebarRenderedAction = Action; +type SidebarNotRenderedAction = Action; + +const initialState: Sidebar = { + sidebarPresent: false, +}; + +export default buildReducer({ + [SIDEBAR_PRESENT]: () => ({ sidebarPresent: true }), + [SIDEBAR_NOT_PRESENT]: () => ({ sidebarPresent: false }), +}, initialState); + +export const sidebarPresent = buildActionCreator(SIDEBAR_PRESENT); + +export const sidebarNotPresent = buildActionCreator(SIDEBAR_NOT_PRESENT); diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index 8c7cc2af..c085aeaf 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -9,6 +9,7 @@ import ErrorHandler from '../ErrorHandler'; import ShlinkVersionsContainer from '../ShlinkVersionsContainer'; import { ConnectDecorator } from '../../container/types'; import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; +import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar'; import { ImageDownloader } from './ImageDownloader'; const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { @@ -44,14 +45,18 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { 'EditShortUrl', 'ManageDomains', ); - bottle.decorator('MenuLayout', connect([ 'selectedServer' ], [ 'selectServer' ])); + bottle.decorator('MenuLayout', connect([ 'selectedServer' ], [ 'selectServer', 'sidebarPresent', 'sidebarNotPresent' ])); bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer); - bottle.decorator('ShlinkVersionsContainer', connect([ 'selectedServer' ])); + bottle.decorator('ShlinkVersionsContainer', connect([ 'selectedServer', 'sidebar' ])); bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console'); + + // Actions + bottle.serviceFactory('sidebarPresent', () => sidebarPresent); + bottle.serviceFactory('sidebarNotPresent', () => sidebarNotPresent); }; export default provideServices; diff --git a/src/container/types.ts b/src/container/types.ts index 284ab00b..aeb4a8b5 100644 --- a/src/container/types.ts +++ b/src/container/types.ts @@ -14,6 +14,7 @@ import { TagVisits } from '../visits/reducers/tagVisits'; import { DomainsList } from '../domains/reducers/domainsList'; import { VisitsOverview } from '../visits/reducers/visitsOverview'; import { VisitsInfo } from '../visits/types'; +import { Sidebar } from '../common/reducers/sidebar'; export interface ShlinkState { servers: ServersMap; @@ -35,6 +36,7 @@ export interface ShlinkState { domainsList: DomainsList; visitsOverview: VisitsOverview; appUpdated: boolean; + sidebar: Sidebar; } export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any; diff --git a/src/reducers/index.ts b/src/reducers/index.ts index a42e8814..c6573632 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -18,6 +18,7 @@ import settingsReducer from '../settings/reducers/settings'; import domainsListReducer from '../domains/reducers/domainsList'; import visitsOverviewReducer from '../visits/reducers/visitsOverview'; import appUpdatesReducer from '../app/reducers/appUpdates'; +import sidebarReducer from '../common/reducers/sidebar'; import { ShlinkState } from '../container/types'; export default combineReducers({ @@ -40,4 +41,5 @@ export default combineReducers({ domainsList: domainsListReducer, visitsOverview: visitsOverviewReducer, appUpdated: appUpdatesReducer, + sidebar: sidebarReducer, }); diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index d8d6b2a0..6d25c6fd 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -30,7 +30,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { 'useStateFlagTimeout', 'ManageServersRow', ); - bottle.decorator('ManageServers', connect([ 'servers' ])); + bottle.decorator('ManageServers', withoutSelectedServer); + bottle.decorator('ManageServers', connect([ 'selectedServer', 'servers' ], [ 'resetSelectedServer' ])); bottle.serviceFactory('ManageServersRow', ManageServersRow, 'ManageServersRowDropdown'); @@ -42,7 +43,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.decorator('CreateServer', connect([ 'selectedServer', 'servers' ], [ 'createServer', 'resetSelectedServer' ])); bottle.serviceFactory('EditServer', EditServer, 'ServerError'); - bottle.decorator('EditServer', connect([ 'selectedServer' ], [ 'editServer', 'selectServer' ])); + bottle.decorator('EditServer', connect([ 'selectedServer' ], [ 'editServer', 'selectServer', 'resetSelectedServer' ])); bottle.serviceFactory('ServersDropdown', () => ServersDropdown); bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ])); diff --git a/test/common/MenuLayout.test.tsx b/test/common/MenuLayout.test.tsx index 98306335..25c4e920 100644 --- a/test/common/MenuLayout.test.tsx +++ b/test/common/MenuLayout.test.tsx @@ -20,7 +20,14 @@ describe('', () => { const createWrapper = (selectedServer: SelectedServer) => { (useParams as any).mockReturnValue({ serverId: 'abc123' }); - wrapper = shallow(); + wrapper = shallow( + , + ); return wrapper; }; diff --git a/test/common/ShlinkVersionsContainer.test.tsx b/test/common/ShlinkVersionsContainer.test.tsx index 5d86bdc7..cec87fed 100644 --- a/test/common/ShlinkVersionsContainer.test.tsx +++ b/test/common/ShlinkVersionsContainer.test.tsx @@ -1,13 +1,14 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Mock } from 'ts-mockery'; import ShlinkVersionsContainer from '../../src/common/ShlinkVersionsContainer'; -import { NonReachableServer, NotFoundServer, ReachableServer, SelectedServer } from '../../src/servers/data'; +import { SelectedServer } from '../../src/servers/data'; +import { Sidebar } from '../../src/common/reducers/sidebar'; describe('', () => { let wrapper: ShallowWrapper; - const createWrapper = (selectedServer: SelectedServer) => { - wrapper = shallow(); + const createWrapper = (sidebar: Sidebar) => { + wrapper = shallow(()} sidebar={sidebar} />); return wrapper; }; @@ -15,12 +16,10 @@ describe('', () => { afterEach(() => wrapper?.unmount()); it.each([ - [ null, 'text-center' ], - [ Mock.of({ serverNotFound: true }), 'text-center' ], - [ Mock.of({ serverNotReachable: true }), 'text-center' ], - [ Mock.of({ version: '1.0.0' }), 'text-center shlink-versions-container--with-server' ], - ])('renders proper col classes based on type of selected server', (selectedServer, expectedClasses) => { - const wrapper = createWrapper(selectedServer); + [{ sidebarPresent: false }, 'text-center' ], + [{ sidebarPresent: true }, 'text-center shlink-versions-container--with-sidebar' ], + ])('renders proper col classes based on sidebar status', (sidebar, expectedClasses) => { + const wrapper = createWrapper(sidebar); expect(wrapper.find('div').prop('className')).toEqual(`${expectedClasses}`); }); diff --git a/test/common/reducer/sidebar.test.ts b/test/common/reducer/sidebar.test.ts new file mode 100644 index 00000000..ecd31075 --- /dev/null +++ b/test/common/reducer/sidebar.test.ts @@ -0,0 +1,31 @@ +import { Mock } from 'ts-mockery'; +import reducer, { + Sidebar, + SIDEBAR_NOT_PRESENT, + SIDEBAR_PRESENT, + sidebarNotPresent, + sidebarPresent, +} from '../../../src/common/reducers/sidebar'; + +describe('sidebarReducer', () => { + describe('reducer', () => { + it.each([ + [ SIDEBAR_PRESENT, { sidebarPresent: true }], + [ SIDEBAR_NOT_PRESENT, { sidebarPresent: false }], + ])('returns expected on %s', (type, expected) => { + expect(reducer(Mock.all(), { type })).toEqual(expected); + }); + }); + + describe('sidebarPresent', () => { + it('returns expected action', () => { + expect(sidebarPresent()).toEqual({ type: SIDEBAR_PRESENT }); + }); + }); + + describe('sidebarNotPresent', () => { + it('returns expected action', () => { + expect(sidebarNotPresent()).toEqual({ type: SIDEBAR_NOT_PRESENT }); + }); + }); +});