From 5598fe0f530e37ddae556f90ffd4e8b4f8a87476 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 23 Dec 2021 17:53:06 +0100 Subject: [PATCH 1/9] Created new settings card for tags-related options --- CHANGELOG.md | 3 -- src/container/index.ts | 5 ++- src/container/store.ts | 7 ++-- src/index.tsx | 4 +-- src/settings/Settings.tsx | 7 ++-- src/settings/Tags.tsx | 25 ++++++++++++++ src/settings/UserInterface.tsx | 11 ------ src/settings/helpers/index.ts | 17 +++++++++ src/settings/reducers/settings.ts | 13 ++++++- src/settings/services/provideServices.ts | 8 ++++- src/tags/TagsList.tsx | 2 +- test/settings/Settings.test.tsx | 4 +-- test/settings/Tags.test.tsx | 44 ++++++++++++++++++++++++ test/settings/UserInterface.test.tsx | 29 +--------------- 14 files changed, 121 insertions(+), 58 deletions(-) create mode 100644 src/settings/Tags.tsx create mode 100644 src/settings/helpers/index.ts create mode 100644 test/settings/Tags.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index e29a5871..ce34677b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed * [#534](https://github.com/shlinkio/shlink-web-client/pull/534) Updated axios. * [#538](https://github.com/shlinkio/shlink-web-client/pull/538) Switched to the `-` notation in `orderBy` param for short URLs list, in preparation for Shlink v3.0.0 -* Fixed typo in identifier for "Last 180 days" interval. - - If that was your default interval, you will see now "All visits" is selected instead. You will need to go to settings page and change it again to "Last 180 days". ### Deprecated * *Nothing* diff --git a/src/container/index.ts b/src/container/index.ts index 1fbf7bd3..aedf0ece 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -18,7 +18,8 @@ import { ConnectDecorator } from './types'; type LazyActionMap = Record; const bottle = new Bottle(); -const { container } = bottle; + +export const { container } = bottle; const lazyService = (container: IContainer, serviceName: string) => (...args: any[]) => (container[serviceName] as T)(...args) as K; @@ -44,5 +45,3 @@ provideUtilsServices(bottle); provideMercureServices(bottle); provideSettingsServices(bottle, connect); provideDomainsServices(bottle, connect); - -export default container; diff --git a/src/container/store.ts b/src/container/store.ts index 3196d6c2..e99dbd8f 100644 --- a/src/container/store.ts +++ b/src/container/store.ts @@ -2,6 +2,8 @@ import ReduxThunk from 'redux-thunk'; import { applyMiddleware, compose, createStore } from 'redux'; import { save, load, RLSOptions } from 'redux-localstorage-simple'; import reducers from '../reducers'; +import { migrateDeprecatedSettings } from '../settings/helpers'; +import { ShlinkState } from './types'; const isProduction = process.env.NODE_ENV !== 'production'; const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; @@ -12,9 +14,8 @@ const localStorageConfig: RLSOptions = { namespaceSeparator: '.', debounce: 300, }; +const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState); -const store = createStore(reducers, load(localStorageConfig), composeEnhancers( +export const store = createStore(reducers, preloadedState, composeEnhancers( applyMiddleware(save(localStorageConfig), ReduxThunk), )); - -export default store; diff --git a/src/index.tsx b/src/index.tsx index cc47e20c..eb2e31dd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,8 +2,8 @@ import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import { homepage } from '../package.json'; -import container from './container'; -import store from './container/store'; +import { container } from './container'; +import { store } from './container/store'; import { fixLeafletIcons } from './utils/helpers/leaflet'; import { register as registerServiceWorker } from './serviceWorkerRegistration'; import 'react-datepicker/dist/react-datepicker.css'; diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index fc8ea340..e9d3369f 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -7,7 +7,7 @@ const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => ( {items.map((child, index) => ( {child.map((subChild, subIndex) => ( -
+
{subChild}
))} @@ -16,12 +16,13 @@ const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => ( ); -const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC, Visits: FC) => () => ( +const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC, Visits: FC, Tags: FC) => () => ( , ], // eslint-disable-line react/jsx-key + [ ], // eslint-disable-line react/jsx-key [ , ], // eslint-disable-line react/jsx-key + [ , ], // eslint-disable-line react/jsx-key ]} /> diff --git a/src/settings/Tags.tsx b/src/settings/Tags.tsx new file mode 100644 index 00000000..47d52260 --- /dev/null +++ b/src/settings/Tags.tsx @@ -0,0 +1,25 @@ +import { FC } from 'react'; +import { FormGroup } from 'reactstrap'; +import { SimpleCard } from '../utils/SimpleCard'; +import { TagsModeDropdown } from '../tags/TagsModeDropdown'; +import { capitalize } from '../utils/utils'; +import { Settings, TagsSettings } from './reducers/settings'; + +interface TagsProps { + settings: Settings; + setTagsSettings: (settings: TagsSettings) => void; +} + +export const Tags: FC = ({ settings: { tags }, setTagsSettings }) => ( + + + + capitalize(tagsMode)} + onChange={(defaultMode) => setTagsSettings({ ...tags, defaultMode })} + /> + Tags will be displayed as {tags?.defaultMode ?? 'cards'}. + + +); diff --git a/src/settings/UserInterface.tsx b/src/settings/UserInterface.tsx index fbd41a8c..e2d74a5d 100644 --- a/src/settings/UserInterface.tsx +++ b/src/settings/UserInterface.tsx @@ -5,8 +5,6 @@ import { FormGroup } from 'reactstrap'; import { SimpleCard } from '../utils/SimpleCard'; import ToggleSwitch from '../utils/ToggleSwitch'; import { changeThemeInMarkup, Theme } from '../utils/theme'; -import { TagsModeDropdown } from '../tags/TagsModeDropdown'; -import { capitalize } from '../utils/utils'; import { Settings, UiSettings } from './reducers/settings'; import './UserInterface.scss'; @@ -31,14 +29,5 @@ export const UserInterface: FC = ({ settings: { ui }, setUiS Use dark theme. - - - capitalize(tagsMode)} - onChange={(tagsMode) => setUiSettings({ ...ui ?? { theme: 'light' }, tagsMode })} - /> - Tags will be displayed as {ui?.tagsMode ?? 'cards'}. - ); diff --git a/src/settings/helpers/index.ts b/src/settings/helpers/index.ts new file mode 100644 index 00000000..a405e827 --- /dev/null +++ b/src/settings/helpers/index.ts @@ -0,0 +1,17 @@ +import { ShlinkState } from '../../container/types'; + +export const migrateDeprecatedSettings = (state: ShlinkState): ShlinkState => { + // The "last180Days" interval had a typo, with a lowercase d + if ((state.settings.visits?.defaultInterval as any) === 'last180days') { + state.settings.visits && (state.settings.visits.defaultInterval = 'last180Days'); + } + + // The "tags display mode" option has been moved from "ui" to "tags" + state.settings.tags = { + ...state.settings.tags, + defaultMode: state.settings.tags?.defaultMode ?? (state.settings.ui as any)?.tagsMode, + }; + state.settings.ui && delete (state.settings.ui as any).tagsMode; + + return state; +}; diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index 6b0079d2..ee88cdc4 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -4,6 +4,7 @@ import { buildReducer } from '../../utils/helpers/redux'; import { RecursivePartial } from '../../utils/utils'; import { Theme } from '../../utils/theme'; import { DateInterval } from '../../utils/dates/types'; +import { TagsOrder } from '../../tags/data/TagsListChildrenProps'; export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS'; @@ -29,18 +30,23 @@ export type TagsMode = 'cards' | 'list'; export interface UiSettings { theme: Theme; - tagsMode?: TagsMode; } export interface VisitsSettings { defaultInterval: DateInterval; } +export interface TagsSettings { + defaultOrdering?: TagsOrder; + defaultMode?: TagsMode; +} + export interface Settings { realTimeUpdates: RealTimeUpdatesSettings; shortUrlCreation?: ShortUrlCreationSettings; ui?: UiSettings; visits?: VisitsSettings; + tags?: TagsSettings; } const initialState: Settings = { @@ -90,3 +96,8 @@ export const setVisitsSettings = (settings: VisitsSettings): PartialSettingsActi type: SET_SETTINGS, visits: settings, }); + +export const setTagsSettings = (settings: TagsSettings): PartialSettingsAction => ({ + type: SET_SETTINGS, + tags: settings, +}); diff --git a/src/settings/services/provideServices.ts b/src/settings/services/provideServices.ts index 52652154..93d82584 100644 --- a/src/settings/services/provideServices.ts +++ b/src/settings/services/provideServices.ts @@ -4,6 +4,7 @@ import Settings from '../Settings'; import { setRealTimeUpdatesInterval, setShortUrlCreationSettings, + setTagsSettings, setUiSettings, setVisitsSettings, toggleRealTimeUpdates, @@ -13,10 +14,11 @@ import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServ import { ShortUrlCreation } from '../ShortUrlCreation'; import { UserInterface } from '../UserInterface'; import { Visits } from '../Visits'; +import { Tags } from '../Tags'; const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components - bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation', 'UserInterface', 'Visits'); + bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation', 'UserInterface', 'Visits', 'Tags'); bottle.decorator('Settings', withoutSelectedServer); bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ])); @@ -35,12 +37,16 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('Visits', () => Visits); bottle.decorator('Visits', connect([ 'settings' ], [ 'setVisitsSettings' ])); + bottle.serviceFactory('Tags', () => Tags); + bottle.decorator('Tags', connect([ 'settings' ], [ 'setTagsSettings' ])); + // Actions bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates); bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval); bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings); bottle.serviceFactory('setUiSettings', () => setUiSettings); bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings); + bottle.serviceFactory('setTagsSettings', () => setTagsSettings); }; export default provideServices; diff --git a/src/tags/TagsList.tsx b/src/tags/TagsList.tsx index 5254dd2c..7aff73af 100644 --- a/src/tags/TagsList.tsx +++ b/src/tags/TagsList.tsx @@ -28,7 +28,7 @@ export interface TagsListProps { const TagsList = (TagsCards: FC, TagsTable: FC) => boundToMercureHub(( { filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps, ) => { - const [ mode, setMode ] = useState(settings.ui?.tagsMode ?? 'cards'); + const [ mode, setMode ] = useState(settings.tags?.defaultMode ?? 'cards'); const [ order, setOrder ] = useState({}); const resolveSortedTags = pipe( () => tagsList.filteredTags.map((tag): NormalizedTag => ({ diff --git a/test/settings/Settings.test.tsx b/test/settings/Settings.test.tsx index 1f34137d..e8f3560c 100644 --- a/test/settings/Settings.test.tsx +++ b/test/settings/Settings.test.tsx @@ -4,7 +4,7 @@ import NoMenuLayout from '../../src/common/NoMenuLayout'; describe('', () => { const Component = () => null; - const Settings = createSettings(Component, Component, Component, Component); + const Settings = createSettings(Component, Component, Component, Component, Component); it('renders a no-menu layout with the expected settings sections', () => { const wrapper = shallow(); @@ -13,6 +13,6 @@ describe('', () => { expect(layout).toHaveLength(1); expect(sections).toHaveLength(1); - expect((sections.prop('items') as any[]).flat()).toHaveLength(4); + expect((sections.prop('items') as any[]).flat()).toHaveLength(5); }); }); diff --git a/test/settings/Tags.test.tsx b/test/settings/Tags.test.tsx new file mode 100644 index 00000000..b9084fc9 --- /dev/null +++ b/test/settings/Tags.test.tsx @@ -0,0 +1,44 @@ +import { shallow, ShallowWrapper } from 'enzyme'; +import { Mock } from 'ts-mockery'; +import { Settings, TagsMode, TagsSettings } from '../../src/settings/reducers/settings'; +import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown'; +import { Tags } from '../../src/settings/Tags'; + +describe('', () => { + let wrapper: ShallowWrapper; + const setTagsSettings = jest.fn(); + const createWrapper = (tags?: TagsSettings) => { + wrapper = shallow(({ tags })} setTagsSettings={setTagsSettings} />); + + return wrapper; + }; + + afterEach(() => wrapper?.unmount()); + afterEach(jest.clearAllMocks); + + it.each([ + [ undefined, 'cards' ], + [{}, 'cards' ], + [{ defaultMode: 'cards' as TagsMode }, 'cards' ], + [{ defaultMode: 'list' as TagsMode }, 'list' ], + ])('shows expected tags displaying mode', (tags, expectedMode) => { + const wrapper = createWrapper(tags); + const dropdown = wrapper.find(TagsModeDropdown); + const small = wrapper.find('small'); + + expect(dropdown.prop('mode')).toEqual(expectedMode); + expect(small.html()).toContain(`Tags will be displayed as ${expectedMode}.`); + }); + + it.each([ + [ 'cards' as TagsMode ], + [ 'list' as TagsMode ], + ])('invokes setTagsSettings when tags mode changes', (defaultMode) => { + const wrapper = createWrapper(); + const dropdown = wrapper.find(TagsModeDropdown); + + expect(setTagsSettings).not.toHaveBeenCalled(); + dropdown.simulate('change', defaultMode); + expect(setTagsSettings).toHaveBeenCalledWith({ defaultMode }); + }); +}); diff --git a/test/settings/UserInterface.test.tsx b/test/settings/UserInterface.test.tsx index 3b205c0e..55abf685 100644 --- a/test/settings/UserInterface.test.tsx +++ b/test/settings/UserInterface.test.tsx @@ -2,11 +2,10 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Mock } from 'ts-mockery'; import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Settings, TagsMode, UiSettings } from '../../src/settings/reducers/settings'; +import { Settings, UiSettings } from '../../src/settings/reducers/settings'; import { UserInterface } from '../../src/settings/UserInterface'; import ToggleSwitch from '../../src/utils/ToggleSwitch'; import { Theme } from '../../src/utils/theme'; -import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown'; describe('', () => { let wrapper: ShallowWrapper; @@ -53,30 +52,4 @@ describe('', () => { toggle.simulate('change', checked); expect(setUiSettings).toHaveBeenCalledWith({ theme }); }); - - it.each([ - [ undefined, 'cards' ], - [{ theme: 'light' as Theme }, 'cards' ], - [{ theme: 'light' as Theme, tagsMode: 'cards' as TagsMode }, 'cards' ], - [{ theme: 'light' as Theme, tagsMode: 'list' as TagsMode }, 'list' ], - ])('shows expected tags displaying mode', (ui, expectedMode) => { - const wrapper = createWrapper(ui); - const dropdown = wrapper.find(TagsModeDropdown); - const small = wrapper.find('small'); - - expect(dropdown.prop('mode')).toEqual(expectedMode); - expect(small.html()).toContain(`Tags will be displayed as ${expectedMode}.`); - }); - - it.each([ - [ 'cards' as TagsMode ], - [ 'list' as TagsMode ], - ])('invokes setUiSettings when tags mode changes', (tagsMode) => { - const wrapper = createWrapper(); - const dropdown = wrapper.find(TagsModeDropdown); - - expect(setUiSettings).not.toHaveBeenCalled(); - dropdown.simulate('change', tagsMode); - expect(setUiSettings).toHaveBeenCalledWith({ theme: 'light', tagsMode }); - }); }); From e954a860bf650c2d6d626b0c327431239ef1e140 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 23 Dec 2021 17:59:18 +0100 Subject: [PATCH 2/9] Added test for migrateDeprecatedSettings function --- test/settings/helpers/index.test.ts | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/settings/helpers/index.test.ts diff --git a/test/settings/helpers/index.test.ts b/test/settings/helpers/index.test.ts new file mode 100644 index 00000000..0a3b349e --- /dev/null +++ b/test/settings/helpers/index.test.ts @@ -0,0 +1,31 @@ +import { Mock } from 'ts-mockery'; +import { migrateDeprecatedSettings } from '../../../src/settings/helpers'; +import { ShlinkState } from '../../../src/container/types'; + +describe('settings-helpers', () => { + describe('migrateDeprecatedSettings', () => { + it('updates settings as expected', () => { + const state = Mock.of({ + settings: { + visits: { + defaultInterval: 'last180days' as any, + }, + ui: { + tagsMode: 'list', + } as any, + }, + }); + + expect(migrateDeprecatedSettings(state)).toEqual(expect.objectContaining({ + settings: expect.objectContaining({ + visits: { + defaultInterval: 'last180Days', + }, + tags: { + defaultMode: 'list', + }, + }), + })); + }); + }); +}); From d8442e435d48c9d7e076191b9811113b021cc89e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 24 Dec 2021 11:05:22 +0100 Subject: [PATCH 3/9] Added option to customize ordering in tags list --- src/settings/Tags.tsx | 12 +++++- src/settings/reducers/settings.ts | 6 +++ src/short-urls/ShortUrlsList.tsx | 6 +-- .../reducers/shortUrlsListParams.ts | 4 +- src/tags/TagsList.tsx | 2 +- test/settings/Tags.test.tsx | 37 +++++++++++++++++++ 6 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/settings/Tags.tsx b/src/settings/Tags.tsx index 47d52260..63bc4aab 100644 --- a/src/settings/Tags.tsx +++ b/src/settings/Tags.tsx @@ -3,6 +3,8 @@ import { FormGroup } from 'reactstrap'; import { SimpleCard } from '../utils/SimpleCard'; import { TagsModeDropdown } from '../tags/TagsModeDropdown'; import { capitalize } from '../utils/utils'; +import SortingDropdown from '../utils/SortingDropdown'; +import { SORTABLE_FIELDS } from '../tags/data/TagsListChildrenProps'; import { Settings, TagsSettings } from './reducers/settings'; interface TagsProps { @@ -12,7 +14,7 @@ interface TagsProps { export const Tags: FC = ({ settings: { tags }, setTagsSettings }) => ( - + = ({ settings: { tags }, setTagsSettings }) => /> Tags will be displayed as {tags?.defaultMode ?? 'cards'}. + + + setTagsSettings({ ...tags, defaultOrdering: { field, dir } })} + /> + ); diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index ee88cdc4..2a7e0fcc 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -5,6 +5,7 @@ import { RecursivePartial } from '../../utils/utils'; import { Theme } from '../../utils/theme'; import { DateInterval } from '../../utils/dates/types'; import { TagsOrder } from '../../tags/data/TagsListChildrenProps'; +import { ShortUrlsOrder } from '../../short-urls/reducers/shortUrlsListParams'; export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS'; @@ -41,9 +42,14 @@ export interface TagsSettings { defaultMode?: TagsMode; } +export interface ShortUrlListSettings { + defaultOrdering?: ShortUrlsOrder; +} + export interface Settings { realTimeUpdates: RealTimeUpdatesSettings; shortUrlCreation?: ShortUrlCreationSettings; + shortUrlList?: ShortUrlListSettings; ui?: UiSettings; visits?: VisitsSettings; tags?: TagsSettings; diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index b5fd5250..f7198a44 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -3,14 +3,14 @@ import { FC, useEffect, useMemo, useState } from 'react'; import { RouteComponentProps } from 'react-router'; import { Card } from 'reactstrap'; import SortingDropdown from '../utils/SortingDropdown'; -import { determineOrderDir, Order, OrderDir } from '../utils/helpers/ordering'; +import { determineOrderDir, OrderDir } from '../utils/helpers/ordering'; import { getServerId, SelectedServer } from '../servers/data'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Topics } from '../mercure/helpers/Topics'; import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { ShlinkShortUrlsListParams } from '../api/types'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; -import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams'; +import { OrderableFields, ShortUrlsListParams, ShortUrlsOrder, SORTABLE_FIELDS } from './reducers/shortUrlsListParams'; import { ShortUrlsTableProps } from './ShortUrlsTable'; import Paginator from './Paginator'; import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; @@ -23,8 +23,6 @@ interface ShortUrlsListProps extends RouteComponentProps void; } -type ShortUrlsOrder = Order; - const ShortUrlsList = (ShortUrlsTable: FC, SearchBar: FC) => boundToMercureHub(({ listShortUrls, resetShortUrlParams, diff --git a/src/short-urls/reducers/shortUrlsListParams.ts b/src/short-urls/reducers/shortUrlsListParams.ts index c05d0ccc..cefc4814 100644 --- a/src/short-urls/reducers/shortUrlsListParams.ts +++ b/src/short-urls/reducers/shortUrlsListParams.ts @@ -1,5 +1,5 @@ import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; -import { OrderDir } from '../../utils/helpers/ordering'; +import { Order, OrderDir } from '../../utils/helpers/ordering'; import { LIST_SHORT_URLS, ListShortUrlsAction } from './shortUrlsList'; export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS'; @@ -14,6 +14,8 @@ export const SORTABLE_FIELDS = { export type OrderableFields = keyof typeof SORTABLE_FIELDS; +export type ShortUrlsOrder = Order; + export type OrderBy = Partial>; export interface ShortUrlsListParams { diff --git a/src/tags/TagsList.tsx b/src/tags/TagsList.tsx index 7aff73af..63832b53 100644 --- a/src/tags/TagsList.tsx +++ b/src/tags/TagsList.tsx @@ -29,7 +29,7 @@ const TagsList = (TagsCards: FC, TagsTable: FC { const [ mode, setMode ] = useState(settings.tags?.defaultMode ?? 'cards'); - const [ order, setOrder ] = useState({}); + const [ order, setOrder ] = useState(settings.tags?.defaultOrdering ?? {}); const resolveSortedTags = pipe( () => tagsList.filteredTags.map((tag): NormalizedTag => ({ tag, diff --git a/test/settings/Tags.test.tsx b/test/settings/Tags.test.tsx index b9084fc9..8e5440a4 100644 --- a/test/settings/Tags.test.tsx +++ b/test/settings/Tags.test.tsx @@ -1,8 +1,11 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Mock } from 'ts-mockery'; +import { FormGroup } from 'reactstrap'; import { Settings, TagsMode, TagsSettings } from '../../src/settings/reducers/settings'; import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown'; import { Tags } from '../../src/settings/Tags'; +import SortingDropdown from '../../src/utils/SortingDropdown'; +import { TagsOrder } from '../../src/tags/data/TagsListChildrenProps'; describe('', () => { let wrapper: ShallowWrapper; @@ -16,6 +19,13 @@ describe('', () => { afterEach(() => wrapper?.unmount()); afterEach(jest.clearAllMocks); + it('renders expected amount of groups', () => { + const wrapper = createWrapper(); + const groups = wrapper.find(FormGroup); + + expect(groups).toHaveLength(2); + }); + it.each([ [ undefined, 'cards' ], [{}, 'cards' ], @@ -41,4 +51,31 @@ describe('', () => { dropdown.simulate('change', defaultMode); expect(setTagsSettings).toHaveBeenCalledWith({ defaultMode }); }); + + it.each([ + [ undefined, {}], + [{}, {}], + [{ defaultOrdering: {} }, {}], + [{ defaultOrdering: { field: 'tag', dir: 'DESC' } as TagsOrder }, { field: 'tag', dir: 'DESC' }], + [{ defaultOrdering: { field: 'visits', dir: 'ASC' } as TagsOrder }, { field: 'visits', dir: 'ASC' }], + ])('shows expected ordering', (tags, expectedOrder) => { + const wrapper = createWrapper(tags); + const dropdown = wrapper.find(SortingDropdown); + + expect(dropdown.prop('order')).toEqual(expectedOrder); + }); + + it.each([ + [ undefined, undefined ], + [ 'tag', 'ASC' ], + [ 'visits', undefined ], + [ 'shortUrls', 'DESC' ], + ])('invokes setTagsSettings when ordering changes', (field, dir) => { + const wrapper = createWrapper(); + const dropdown = wrapper.find(SortingDropdown); + + expect(setTagsSettings).not.toHaveBeenCalled(); + dropdown.simulate('change', field, dir); + expect(setTagsSettings).toHaveBeenCalledWith({ defaultOrdering: { field, dir } }); + }); }); From 57075c581d8befe1fc19ac0a00283e566ac2b729 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 24 Dec 2021 13:14:13 +0100 Subject: [PATCH 4/9] Updated Short URLs list so that it allows setting default orderBy from settings --- src/api/services/ShlinkApiClient.ts | 7 ++- src/api/types/index.ts | 4 +- src/servers/Overview.tsx | 2 +- src/settings/reducers/settings.ts | 8 ++++ src/short-urls/ShortUrlsList.tsx | 35 +++++++-------- src/short-urls/reducers/shortUrlsList.ts | 3 +- .../reducers/shortUrlsListParams.ts | 7 +-- src/short-urls/services/provideServices.ts | 2 +- test/api/services/ShlinkApiClient.test.ts | 8 ++-- test/settings/reducers/settings.test.ts | 13 +++++- test/short-urls/ShortUrlsList.test.tsx | 43 +++++++------------ .../reducers/shortUrlsListParams.test.ts | 6 +-- 12 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/api/services/ShlinkApiClient.ts b/src/api/services/ShlinkApiClient.ts index 192bad0f..330bfb55 100644 --- a/src/api/services/ShlinkApiClient.ts +++ b/src/api/services/ShlinkApiClient.ts @@ -24,12 +24,11 @@ const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/re const rejectNilProps = reject(isNil); const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => { const { orderBy = {}, ...rest } = params; - const [ firstKey ] = Object.keys(orderBy); - const [ firstValue ] = Object.values(orderBy); + const { field, dir } = orderBy; - return !firstValue ? rest : { + return !dir ? rest : { ...rest, - orderBy: `${firstKey}-${firstValue}`, + orderBy: `${field}-${dir}`, }; }; diff --git a/src/api/types/index.ts b/src/api/types/index.ts index cce4751c..af833bb2 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -1,7 +1,7 @@ import { Visit } from '../../visits/types'; import { OptionalString } from '../../utils/utils'; import { ShortUrl, ShortUrlMeta } from '../../short-urls/data'; -import { OrderBy } from '../../short-urls/reducers/shortUrlsListParams'; +import { ShortUrlsOrder } from '../../short-urls/reducers/shortUrlsListParams'; export interface ShlinkShortUrlsResponse { data: ShortUrl[]; @@ -94,7 +94,7 @@ export interface ShlinkShortUrlsListParams { searchTerm?: string; startDate?: string; endDate?: string; - orderBy?: OrderBy; + orderBy?: ShortUrlsOrder; } export interface ShlinkShortUrlsListNormalizedParams extends Omit { diff --git a/src/servers/Overview.tsx b/src/servers/Overview.tsx index 9b99cc6f..a68d3e17 100644 --- a/src/servers/Overview.tsx +++ b/src/servers/Overview.tsx @@ -44,7 +44,7 @@ export const Overview = ( const history = useHistory(); useEffect(() => { - listShortUrls({ itemsPerPage: 5, orderBy: { dateCreated: 'DESC' } }); + listShortUrls({ itemsPerPage: 5, orderBy: { field: 'dateCreated', dir: 'DESC' } }); listTags(); loadVisitsOverview(); }, []); diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index 2a7e0fcc..fb2932d6 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -9,6 +9,11 @@ import { ShortUrlsOrder } from '../../short-urls/reducers/shortUrlsListParams'; export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS'; +export const DEFAULT_SHORT_URLS_ORDERING: ShortUrlsOrder = { + field: 'dateCreated', + dir: 'DESC', +}; + /** * Important! When adding new props in the main Settings interface or any of the nested props, they have to be set as * optional, as old instances of the app will load partial objects from local storage until it is saved again. @@ -68,6 +73,9 @@ const initialState: Settings = { visits: { defaultInterval: 'last30Days', }, + shortUrlList: { + defaultOrdering: DEFAULT_SHORT_URLS_ORDERING, + }, }; type SettingsAction = Action & Settings; diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index f7198a44..fe0623e7 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -1,4 +1,4 @@ -import { head, keys, pipe, values } from 'ramda'; +import { pipe } from 'ramda'; import { FC, useEffect, useMemo, useState } from 'react'; import { RouteComponentProps } from 'react-router'; import { Card } from 'reactstrap'; @@ -9,6 +9,7 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Topics } from '../mercure/helpers/Topics'; import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { ShlinkShortUrlsListParams } from '../api/types'; +import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; import { OrderableFields, ShortUrlsListParams, ShortUrlsOrder, SORTABLE_FIELDS } from './reducers/shortUrlsListParams'; import { ShortUrlsTableProps } from './ShortUrlsTable'; @@ -18,9 +19,10 @@ import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; interface ShortUrlsListProps extends RouteComponentProps { selectedServer: SelectedServer; shortUrlsList: ShortUrlsListState; - listShortUrls: (params: ShortUrlsListParams) => void; + listShortUrls: (params: ShlinkShortUrlsListParams) => void; shortUrlsListParams: ShortUrlsListParams; resetShortUrlParams: () => void; + settings: Settings; } const ShortUrlsList = (ShortUrlsTable: FC, SearchBar: FC) => boundToMercureHub(({ @@ -32,24 +34,17 @@ const ShortUrlsList = (ShortUrlsTable: FC, SearchBar: FC) = history, shortUrlsList, selectedServer, + settings, }: ShortUrlsListProps) => { const serverId = getServerId(selectedServer); const { orderBy } = shortUrlsListParams; - const [ order, setOrder ] = useState({ - field: orderBy && (head(keys(orderBy)) as OrderableFields), - dir: orderBy && head(values(orderBy)), - }); + const initialOrderBy = orderBy ?? settings.shortUrlList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING; + const [ order, setOrder ] = useState(initialOrderBy); const [{ tags, search, startDate, endDate }, toFirstPage ] = useShortUrlsQuery({ history, match, location }); const selectedTags = useMemo(() => tags?.split(',') ?? [], [ tags ]); const { pagination } = shortUrlsList?.shortUrls ?? {}; - const refreshList = (extraParams: ShlinkShortUrlsListParams) => listShortUrls( - { ...shortUrlsListParams, ...extraParams }, - ); - const handleOrderBy = (field?: OrderableFields, dir?: OrderDir) => { - setOrder({ field, dir }); - refreshList({ orderBy: field ? { [field]: dir } : undefined }); - }; + const handleOrderBy = (field?: OrderableFields, dir?: OrderDir) => setOrder({ field, dir }); const orderByColumn = (field: OrderableFields) => () => handleOrderBy(field, determineOrderDir(field, order.field, order.dir)); const renderOrderIcon = (field: OrderableFields) => ; @@ -60,10 +55,16 @@ const ShortUrlsList = (ShortUrlsTable: FC, SearchBar: FC) = useEffect(() => resetShortUrlParams, []); useEffect(() => { - refreshList( - { page: match.params.page, searchTerm: search, tags: selectedTags, itemsPerPage: undefined, startDate, endDate }, - ); - }, [ match.params.page, search, selectedTags, startDate, endDate ]); + listShortUrls({ + page: match.params.page, + searchTerm: search, + tags: selectedTags, + itemsPerPage: undefined, + startDate, + endDate, + orderBy: order, + }); + }, [ match.params.page, search, selectedTags, startDate, endDate, order ]); return ( <> diff --git a/src/short-urls/reducers/shortUrlsList.ts b/src/short-urls/reducers/shortUrlsList.ts index 72290bea..e50d931c 100644 --- a/src/short-urls/reducers/shortUrlsList.ts +++ b/src/short-urls/reducers/shortUrlsList.ts @@ -7,7 +7,6 @@ import { GetState } from '../../container/types'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api/types'; import { DeleteShortUrlAction, SHORT_URL_DELETED } from './shortUrlDeletion'; -import { ShortUrlsListParams } from './shortUrlsListParams'; import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation'; import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition'; @@ -25,7 +24,7 @@ export interface ShortUrlsList { export interface ListShortUrlsAction extends Action { shortUrls: ShlinkShortUrlsResponse; - params: ShortUrlsListParams; + params: ShlinkShortUrlsListParams; } export type ListShortUrlsCombinedAction = ( diff --git a/src/short-urls/reducers/shortUrlsListParams.ts b/src/short-urls/reducers/shortUrlsListParams.ts index cefc4814..f2c2b0bc 100644 --- a/src/short-urls/reducers/shortUrlsListParams.ts +++ b/src/short-urls/reducers/shortUrlsListParams.ts @@ -1,5 +1,5 @@ import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; -import { Order, OrderDir } from '../../utils/helpers/ordering'; +import { Order } from '../../utils/helpers/ordering'; import { LIST_SHORT_URLS, ListShortUrlsAction } from './shortUrlsList'; export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS'; @@ -16,17 +16,14 @@ export type OrderableFields = keyof typeof SORTABLE_FIELDS; export type ShortUrlsOrder = Order; -export type OrderBy = Partial>; - export interface ShortUrlsListParams { page?: string; itemsPerPage?: number; - orderBy?: OrderBy; + orderBy?: ShortUrlsOrder; } const initialState: ShortUrlsListParams = { page: '1', - orderBy: { dateCreated: 'DESC' }, }; export default buildReducer({ diff --git a/src/short-urls/services/provideServices.ts b/src/short-urls/services/provideServices.ts index bd7c7daf..2394ddd5 100644 --- a/src/short-urls/services/provideServices.ts +++ b/src/short-urls/services/provideServices.ts @@ -22,7 +22,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: // Components bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'SearchBar'); bottle.decorator('ShortUrlsList', connect( - [ 'selectedServer', 'shortUrlsListParams', 'mercureInfo', 'shortUrlsList' ], + [ 'selectedServer', 'shortUrlsListParams', 'mercureInfo', 'shortUrlsList', 'settings' ], [ 'listShortUrls', 'resetShortUrlParams', 'createNewVisits', 'loadMercureInfo' ], )); diff --git a/test/api/services/ShlinkApiClient.test.ts b/test/api/services/ShlinkApiClient.test.ts index 10090847..11131f97 100644 --- a/test/api/services/ShlinkApiClient.test.ts +++ b/test/api/services/ShlinkApiClient.test.ts @@ -5,7 +5,7 @@ import { OptionalString } from '../../../src/utils/utils'; import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/api/types'; import { ShortUrl } from '../../../src/short-urls/data'; import { Visit } from '../../../src/visits/types'; -import { OrderDir } from '../../../src/utils/helpers/ordering'; +import { ShortUrlsOrder } from '../../../src/short-urls/reducers/shortUrlsListParams'; describe('ShlinkApiClient', () => { const createAxios = (data: AxiosRequestConfig) => (async () => Promise.resolve(data)) as unknown as AxiosInstance; @@ -33,9 +33,9 @@ describe('ShlinkApiClient', () => { }); it.each([ - [{ visits: 'DESC' as OrderDir }, 'visits-DESC' ], - [{ longUrl: 'ASC' as OrderDir }, 'longUrl-ASC' ], - [{ longUrl: undefined as OrderDir }, undefined ], + [ { field: 'visits', dir: 'DESC' } as ShortUrlsOrder, 'visits-DESC' ], + [ { field: 'longUrl', dir: 'ASC' } as ShortUrlsOrder, 'longUrl-ASC' ], + [ { field: 'longUrl', dir: undefined } as ShortUrlsOrder, undefined ], ])('parses orderBy in params', async (orderBy, expectedOrderBy) => { const axiosSpy = createAxiosMock({ data: expectedList, diff --git a/test/settings/reducers/settings.test.ts b/test/settings/reducers/settings.test.ts index 70c5d4b7..272e8905 100644 --- a/test/settings/reducers/settings.test.ts +++ b/test/settings/reducers/settings.test.ts @@ -1,10 +1,12 @@ import reducer, { SET_SETTINGS, + DEFAULT_SHORT_URLS_ORDERING, toggleRealTimeUpdates, setRealTimeUpdatesInterval, setShortUrlCreationSettings, setUiSettings, setVisitsSettings, + setTagsSettings, } from '../../../src/settings/reducers/settings'; describe('settingsReducer', () => { @@ -12,7 +14,8 @@ describe('settingsReducer', () => { const shortUrlCreation = { validateUrls: false }; const ui = { theme: 'light' }; const visits = { defaultInterval: 'last30Days' }; - const settings = { realTimeUpdates, shortUrlCreation, ui, visits }; + const shortUrlList = { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING }; + const settings = { realTimeUpdates, shortUrlCreation, ui, visits, shortUrlList }; describe('reducer', () => { it('returns realTimeUpdates when action is SET_SETTINGS', () => { @@ -59,4 +62,12 @@ describe('settingsReducer', () => { expect(result).toEqual({ type: SET_SETTINGS, visits: { defaultInterval: 'last180Days' } }); }); }); + + describe('setTagsSettings', () => { + it('creates action to set tags settings', () => { + const result = setTagsSettings({ defaultMode: 'list' }); + + expect(result).toEqual({ type: SET_SETTINGS, tags: { defaultMode: 'list' } }); + }); + }); }); diff --git a/test/short-urls/ShortUrlsList.test.tsx b/test/short-urls/ShortUrlsList.test.tsx index ebb09941..71a1dd5f 100644 --- a/test/short-urls/ShortUrlsList.test.tsx +++ b/test/short-urls/ShortUrlsList.test.tsx @@ -8,10 +8,11 @@ import { ShortUrl } from '../../src/short-urls/data'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList'; import SortingDropdown from '../../src/utils/SortingDropdown'; -import { OrderableFields, OrderBy } from '../../src/short-urls/reducers/shortUrlsListParams'; +import { OrderableFields, ShortUrlsOrder } from '../../src/short-urls/reducers/shortUrlsListParams'; import Paginator from '../../src/short-urls/Paginator'; import { ReachableServer } from '../../src/servers/data'; import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks'; +import { Settings } from '../../src/settings/reducers/settings'; describe('', () => { let wrapper: ShallowWrapper; @@ -32,7 +33,7 @@ describe('', () => { }, }); const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, SearchBar); - const createWrapper = (orderBy: OrderBy = {}) => shallow( + const createWrapper = (orderBy: ShortUrlsOrder = {}) => shallow( ({ mercureInfo: { loading: true } })} listShortUrls={listShortUrlsMock} @@ -43,6 +44,7 @@ describe('', () => { shortUrlsList={shortUrlsList} history={Mock.of({ push })} selectedServer={Mock.of({ id: '1' })} + settings={Mock.all()} />, ).dive(); // Dive is needed as this component is wrapped in a HOC @@ -91,20 +93,16 @@ describe('', () => { it('handles order through table', () => { const orderByColumn: (field: OrderableFields) => Function = wrapper.find(ShortUrlsTable).prop('orderByColumn'); - orderByColumn('visits')(); - orderByColumn('title')(); - orderByColumn('shortCode')(); + expect(wrapper.find(SortingDropdown).prop('order')).toEqual({}); - expect(listShortUrlsMock).toHaveBeenCalledTimes(3); - expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ - orderBy: { visits: 'ASC' }, - })); - expect(listShortUrlsMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ - orderBy: { title: 'ASC' }, - })); - expect(listShortUrlsMock).toHaveBeenNthCalledWith(3, expect.objectContaining({ - orderBy: { shortCode: 'ASC' }, - })); + orderByColumn('visits')(); + expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'visits', dir: 'ASC' }); + + orderByColumn('title')(); + expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'title', dir: 'ASC' }); + + orderByColumn('shortCode')(); + expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'shortCode', dir: 'ASC' }); }); it('handles order through dropdown', () => { @@ -118,21 +116,12 @@ describe('', () => { wrapper.find(SortingDropdown).simulate('change', undefined, undefined); expect(wrapper.find(SortingDropdown).prop('order')).toEqual({}); - - expect(listShortUrlsMock).toHaveBeenCalledTimes(3); - expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ - orderBy: { visits: 'ASC' }, - })); - expect(listShortUrlsMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ - orderBy: { shortCode: 'DESC' }, - })); - expect(listShortUrlsMock).toHaveBeenNthCalledWith(3, expect.objectContaining({ orderBy: undefined })); }); it.each([ - [ Mock.of({ visits: 'ASC' }), 'visits', 'ASC' ], - [ Mock.of({ title: 'DESC' }), 'title', 'DESC' ], - [ Mock.of(), undefined, undefined ], + [ Mock.of({ field: 'visits', dir: 'ASC' }), 'visits', 'ASC' ], + [ Mock.of({ field: 'title', dir: 'DESC' }), 'title', 'DESC' ], + [ Mock.of(), undefined, undefined ], ])('has expected initial ordering', (initialOrderBy, field, dir) => { const wrapper = createWrapper(initialOrderBy); diff --git a/test/short-urls/reducers/shortUrlsListParams.test.ts b/test/short-urls/reducers/shortUrlsListParams.test.ts index 871ac7ff..6acace25 100644 --- a/test/short-urls/reducers/shortUrlsListParams.test.ts +++ b/test/short-urls/reducers/shortUrlsListParams.test.ts @@ -10,14 +10,10 @@ describe('shortUrlsListParamsReducer', () => { expect(reducer(undefined, { type: LIST_SHORT_URLS, params: { searchTerm: 'foo', page: '2' } } as any)).toEqual({ page: '2', searchTerm: 'foo', - orderBy: { dateCreated: 'DESC' }, })); it('returns default value when action is RESET_SHORT_URL_PARAMS', () => - expect(reducer(undefined, { type: RESET_SHORT_URL_PARAMS } as any)).toEqual({ - page: '1', - orderBy: { dateCreated: 'DESC' }, - })); + expect(reducer(undefined, { type: RESET_SHORT_URL_PARAMS } as any)).toEqual({ page: '1' })); }); describe('resetShortUrlParams', () => { From 275aee4de26f3a84a0e57280b5fb911398ba67ca Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 24 Dec 2021 13:39:51 +0100 Subject: [PATCH 5/9] Removed shortUrlsListParams reducer, as the state is now handled internally in the component --- src/container/types.ts | 2 -- src/reducers/index.ts | 2 -- src/servers/reducers/selectedServer.ts | 2 -- src/short-urls/ShortUrlsList.tsx | 10 ++------ .../reducers/shortUrlsListParams.ts | 21 ----------------- src/short-urls/services/provideServices.ts | 6 ++--- test/servers/reducers/selectedServer.test.ts | 10 ++++---- test/short-urls/ShortUrlsList.test.tsx | 6 ++--- .../reducers/shortUrlsListParams.test.ts | 23 ------------------- 9 files changed, 10 insertions(+), 72 deletions(-) delete mode 100644 test/short-urls/reducers/shortUrlsListParams.test.ts diff --git a/src/container/types.ts b/src/container/types.ts index f4d20282..e3289da8 100644 --- a/src/container/types.ts +++ b/src/container/types.ts @@ -4,7 +4,6 @@ import { Settings } from '../settings/reducers/settings'; import { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation'; import { ShortUrlDeletion } from '../short-urls/reducers/shortUrlDeletion'; import { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition'; -import { ShortUrlsListParams } from '../short-urls/reducers/shortUrlsListParams'; import { ShortUrlsList } from '../short-urls/reducers/shortUrlsList'; import { TagDeletion } from '../tags/reducers/tagDelete'; import { TagEdition } from '../tags/reducers/tagEdit'; @@ -20,7 +19,6 @@ export interface ShlinkState { servers: ServersMap; selectedServer: SelectedServer; shortUrlsList: ShortUrlsList; - shortUrlsListParams: ShortUrlsListParams; shortUrlCreationResult: ShortUrlCreation; shortUrlDeletion: ShortUrlDeletion; shortUrlEdition: ShortUrlEdition; diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 2257cdda..3c85f168 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -2,7 +2,6 @@ import { combineReducers } from 'redux'; import serversReducer from '../servers/reducers/servers'; import selectedServerReducer from '../servers/reducers/selectedServer'; import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList'; -import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListParams'; import shortUrlCreationReducer from '../short-urls/reducers/shortUrlCreation'; import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion'; import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition'; @@ -24,7 +23,6 @@ export default combineReducers({ servers: serversReducer, selectedServer: selectedServerReducer, shortUrlsList: shortUrlsListReducer, - shortUrlsListParams: shortUrlsListParamsReducer, shortUrlCreationResult: shortUrlCreationReducer, shortUrlDeletion: shortUrlDeletionReducer, shortUrlEdition: shortUrlEditionReducer, diff --git a/src/servers/reducers/selectedServer.ts b/src/servers/reducers/selectedServer.ts index 38c7c133..b531547c 100644 --- a/src/servers/reducers/selectedServer.ts +++ b/src/servers/reducers/selectedServer.ts @@ -1,6 +1,5 @@ import { identity, memoizeWith, pipe } from 'ramda'; import { Action, Dispatch } from 'redux'; -import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'; import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version'; import { SelectedServer } from '../data'; import { GetState } from '../../container/types'; @@ -53,7 +52,6 @@ export const selectServer = ( getState: GetState, ) => { dispatch(resetSelectedServer()); - dispatch(resetShortUrlParams()); const { servers } = getState(); const selectedServer = servers[serverId]; diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index fe0623e7..dc81d6d4 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -11,7 +11,7 @@ import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { ShlinkShortUrlsListParams } from '../api/types'; import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; -import { OrderableFields, ShortUrlsListParams, ShortUrlsOrder, SORTABLE_FIELDS } from './reducers/shortUrlsListParams'; +import { OrderableFields, ShortUrlsOrder, SORTABLE_FIELDS } from './reducers/shortUrlsListParams'; import { ShortUrlsTableProps } from './ShortUrlsTable'; import Paginator from './Paginator'; import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; @@ -20,15 +20,11 @@ interface ShortUrlsListProps extends RouteComponentProps void; - shortUrlsListParams: ShortUrlsListParams; - resetShortUrlParams: () => void; settings: Settings; } const ShortUrlsList = (ShortUrlsTable: FC, SearchBar: FC) => boundToMercureHub(({ listShortUrls, - resetShortUrlParams, - shortUrlsListParams, match, location, history, @@ -37,8 +33,7 @@ const ShortUrlsList = (ShortUrlsTable: FC, SearchBar: FC) = settings, }: ShortUrlsListProps) => { const serverId = getServerId(selectedServer); - const { orderBy } = shortUrlsListParams; - const initialOrderBy = orderBy ?? settings.shortUrlList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING; + const initialOrderBy = settings.shortUrlList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING; const [ order, setOrder ] = useState(initialOrderBy); const [{ tags, search, startDate, endDate }, toFirstPage ] = useShortUrlsQuery({ history, match, location }); const selectedTags = useMemo(() => tags?.split(',') ?? [], [ tags ]); @@ -53,7 +48,6 @@ const ShortUrlsList = (ShortUrlsTable: FC, SearchBar: FC) = (tags) => toFirstPage({ tags }), ); - useEffect(() => resetShortUrlParams, []); useEffect(() => { listShortUrls({ page: match.params.page, diff --git a/src/short-urls/reducers/shortUrlsListParams.ts b/src/short-urls/reducers/shortUrlsListParams.ts index f2c2b0bc..299f9d1a 100644 --- a/src/short-urls/reducers/shortUrlsListParams.ts +++ b/src/short-urls/reducers/shortUrlsListParams.ts @@ -1,8 +1,4 @@ -import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { Order } from '../../utils/helpers/ordering'; -import { LIST_SHORT_URLS, ListShortUrlsAction } from './shortUrlsList'; - -export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS'; export const SORTABLE_FIELDS = { dateCreated: 'Created at', @@ -15,20 +11,3 @@ export const SORTABLE_FIELDS = { export type OrderableFields = keyof typeof SORTABLE_FIELDS; export type ShortUrlsOrder = Order; - -export interface ShortUrlsListParams { - page?: string; - itemsPerPage?: number; - orderBy?: ShortUrlsOrder; -} - -const initialState: ShortUrlsListParams = { - page: '1', -}; - -export default buildReducer({ - [LIST_SHORT_URLS]: (state, { params }) => ({ ...state, ...params }), - [RESET_SHORT_URL_PARAMS]: () => initialState, -}, initialState); - -export const resetShortUrlParams = buildActionCreator(RESET_SHORT_URL_PARAMS); diff --git a/src/short-urls/services/provideServices.ts b/src/short-urls/services/provideServices.ts index 2394ddd5..45b745f2 100644 --- a/src/short-urls/services/provideServices.ts +++ b/src/short-urls/services/provideServices.ts @@ -9,7 +9,6 @@ import CreateShortUrlResult from '../helpers/CreateShortUrlResult'; import { listShortUrls } from '../reducers/shortUrlsList'; import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation'; import { deleteShortUrl, resetDeleteShortUrl } from '../reducers/shortUrlDeletion'; -import { resetShortUrlParams } from '../reducers/shortUrlsListParams'; import { editShortUrl } from '../reducers/shortUrlEdition'; import { ConnectDecorator } from '../../container/types'; import { ShortUrlsTable } from '../ShortUrlsTable'; @@ -22,8 +21,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: // Components bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'SearchBar'); bottle.decorator('ShortUrlsList', connect( - [ 'selectedServer', 'shortUrlsListParams', 'mercureInfo', 'shortUrlsList', 'settings' ], - [ 'listShortUrls', 'resetShortUrlParams', 'createNewVisits', 'loadMercureInfo' ], + [ 'selectedServer', 'mercureInfo', 'shortUrlsList', 'settings' ], + [ 'listShortUrls', 'createNewVisits', 'loadMercureInfo' ], )); bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow'); @@ -56,7 +55,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: // Actions bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient'); - bottle.serviceFactory('resetShortUrlParams', () => resetShortUrlParams); bottle.serviceFactory('createShortUrl', createShortUrl, 'buildShlinkApiClient'); bottle.serviceFactory('resetCreateShortUrl', () => resetCreateShortUrl); diff --git a/test/servers/reducers/selectedServer.test.ts b/test/servers/reducers/selectedServer.test.ts index 030b17f4..acd896ef 100644 --- a/test/servers/reducers/selectedServer.test.ts +++ b/test/servers/reducers/selectedServer.test.ts @@ -8,7 +8,6 @@ import reducer, { MAX_FALLBACK_VERSION, MIN_FALLBACK_VERSION, } from '../../../src/servers/reducers/selectedServer'; -import { RESET_SHORT_URL_PARAMS } from '../../../src/short-urls/reducers/shortUrlsListParams'; import { ShlinkState } from '../../../src/container/types'; import { NonReachableServer, NotFoundServer, RegularServer } from '../../../src/servers/data'; @@ -62,10 +61,9 @@ describe('selectedServerReducer', () => { await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState); - expect(dispatch).toHaveBeenCalledTimes(4); + expect(dispatch).toHaveBeenCalledTimes(3); expect(dispatch).toHaveBeenNthCalledWith(1, { type: RESET_SELECTED_SERVER }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: RESET_SHORT_URL_PARAMS }); - expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer: expectedSelectedServer }); expect(loadMercureInfo).toHaveBeenCalledTimes(1); }); @@ -89,7 +87,7 @@ describe('selectedServerReducer', () => { await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState); expect(apiClientMock.health).toHaveBeenCalled(); - expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer: expectedSelectedServer }); expect(loadMercureInfo).not.toHaveBeenCalled(); }); @@ -102,7 +100,7 @@ describe('selectedServerReducer', () => { expect(getState).toHaveBeenCalled(); expect(apiClientMock.health).not.toHaveBeenCalled(); - expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer: expectedSelectedServer }); expect(loadMercureInfo).not.toHaveBeenCalled(); }); }); diff --git a/test/short-urls/ShortUrlsList.test.tsx b/test/short-urls/ShortUrlsList.test.tsx index 71a1dd5f..8ceadcbc 100644 --- a/test/short-urls/ShortUrlsList.test.tsx +++ b/test/short-urls/ShortUrlsList.test.tsx @@ -33,18 +33,16 @@ describe('', () => { }, }); const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, SearchBar); - const createWrapper = (orderBy: ShortUrlsOrder = {}) => shallow( + const createWrapper = (defaultOrdering: ShortUrlsOrder = {}) => shallow( ({ mercureInfo: { loading: true } })} listShortUrls={listShortUrlsMock} - resetShortUrlParams={jest.fn()} - shortUrlsListParams={{ page: '1', orderBy }} match={Mock.of>({ params: {} })} location={Mock.of({ search: '?tags=test%20tag&search=example.com' })} shortUrlsList={shortUrlsList} history={Mock.of({ push })} selectedServer={Mock.of({ id: '1' })} - settings={Mock.all()} + settings={Mock.of({ shortUrlList: { defaultOrdering } })} />, ).dive(); // Dive is needed as this component is wrapped in a HOC diff --git a/test/short-urls/reducers/shortUrlsListParams.test.ts b/test/short-urls/reducers/shortUrlsListParams.test.ts deleted file mode 100644 index 6acace25..00000000 --- a/test/short-urls/reducers/shortUrlsListParams.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import reducer, { - RESET_SHORT_URL_PARAMS, - resetShortUrlParams, -} from '../../../src/short-urls/reducers/shortUrlsListParams'; -import { LIST_SHORT_URLS } from '../../../src/short-urls/reducers/shortUrlsList'; - -describe('shortUrlsListParamsReducer', () => { - describe('reducer', () => { - it('returns params when action is LIST_SHORT_URLS', () => - expect(reducer(undefined, { type: LIST_SHORT_URLS, params: { searchTerm: 'foo', page: '2' } } as any)).toEqual({ - page: '2', - searchTerm: 'foo', - })); - - it('returns default value when action is RESET_SHORT_URL_PARAMS', () => - expect(reducer(undefined, { type: RESET_SHORT_URL_PARAMS } as any)).toEqual({ page: '1' })); - }); - - describe('resetShortUrlParams', () => { - it('returns proper action', () => - expect(resetShortUrlParams()).toEqual({ type: RESET_SHORT_URL_PARAMS })); - }); -}); From d4356ba6e64725b34813facc61d68877f13833b5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 24 Dec 2021 13:47:27 +0100 Subject: [PATCH 6/9] Moved types from old shortUrlsListParams reducer, to the data index file --- src/api/types/index.ts | 3 +-- src/settings/reducers/settings.ts | 2 +- src/short-urls/ShortUrlsList.tsx | 2 +- src/short-urls/ShortUrlsTable.tsx | 2 +- src/short-urls/data/index.ts | 13 +++++++++++++ src/short-urls/reducers/shortUrlsListParams.ts | 13 ------------- test/api/services/ShlinkApiClient.test.ts | 3 +-- test/short-urls/ShortUrlsList.test.tsx | 3 +-- test/short-urls/ShortUrlsTable.test.tsx | 2 +- 9 files changed, 20 insertions(+), 23 deletions(-) delete mode 100644 src/short-urls/reducers/shortUrlsListParams.ts diff --git a/src/api/types/index.ts b/src/api/types/index.ts index af833bb2..4c656819 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -1,7 +1,6 @@ import { Visit } from '../../visits/types'; import { OptionalString } from '../../utils/utils'; -import { ShortUrl, ShortUrlMeta } from '../../short-urls/data'; -import { ShortUrlsOrder } from '../../short-urls/reducers/shortUrlsListParams'; +import { ShortUrl, ShortUrlMeta, ShortUrlsOrder } from '../../short-urls/data'; export interface ShlinkShortUrlsResponse { data: ShortUrl[]; diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index fb2932d6..c2f0a8da 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -5,7 +5,7 @@ import { RecursivePartial } from '../../utils/utils'; import { Theme } from '../../utils/theme'; import { DateInterval } from '../../utils/dates/types'; import { TagsOrder } from '../../tags/data/TagsListChildrenProps'; -import { ShortUrlsOrder } from '../../short-urls/reducers/shortUrlsListParams'; +import { ShortUrlsOrder } from '../../short-urls/data'; export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS'; diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index dc81d6d4..b3e63943 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -11,10 +11,10 @@ import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { ShlinkShortUrlsListParams } from '../api/types'; import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; -import { OrderableFields, ShortUrlsOrder, SORTABLE_FIELDS } from './reducers/shortUrlsListParams'; import { ShortUrlsTableProps } from './ShortUrlsTable'; import Paginator from './Paginator'; import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; +import { OrderableFields, ShortUrlsOrder, SORTABLE_FIELDS } from './data'; interface ShortUrlsListProps extends RouteComponentProps { selectedServer: SelectedServer; diff --git a/src/short-urls/ShortUrlsTable.tsx b/src/short-urls/ShortUrlsTable.tsx index ae73ad9e..df386f88 100644 --- a/src/short-urls/ShortUrlsTable.tsx +++ b/src/short-urls/ShortUrlsTable.tsx @@ -5,7 +5,7 @@ import { SelectedServer } from '../servers/data'; import { supportsShortUrlTitle } from '../utils/helpers/features'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; import { ShortUrlsRowProps } from './helpers/ShortUrlsRow'; -import { OrderableFields } from './reducers/shortUrlsListParams'; +import { OrderableFields } from './data'; import './ShortUrlsTable.scss'; export interface ShortUrlsTableProps { diff --git a/src/short-urls/data/index.ts b/src/short-urls/data/index.ts index 5b436bdf..f5a001cd 100644 --- a/src/short-urls/data/index.ts +++ b/src/short-urls/data/index.ts @@ -1,4 +1,5 @@ import { Nullable, OptionalString } from '../../utils/utils'; +import { Order } from '../../utils/helpers/ordering'; export interface EditShortUrlData { longUrl?: string; @@ -50,3 +51,15 @@ export interface ShortUrlIdentifier { shortCode: string; domain: OptionalString; } + +export const SORTABLE_FIELDS = { + dateCreated: 'Created at', + shortCode: 'Short URL', + longUrl: 'Long URL', + title: 'Title', + visits: 'Visits', +}; + +export type OrderableFields = keyof typeof SORTABLE_FIELDS; + +export type ShortUrlsOrder = Order; diff --git a/src/short-urls/reducers/shortUrlsListParams.ts b/src/short-urls/reducers/shortUrlsListParams.ts deleted file mode 100644 index 299f9d1a..00000000 --- a/src/short-urls/reducers/shortUrlsListParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Order } from '../../utils/helpers/ordering'; - -export const SORTABLE_FIELDS = { - dateCreated: 'Created at', - shortCode: 'Short URL', - longUrl: 'Long URL', - title: 'Title', - visits: 'Visits', -}; - -export type OrderableFields = keyof typeof SORTABLE_FIELDS; - -export type ShortUrlsOrder = Order; diff --git a/test/api/services/ShlinkApiClient.test.ts b/test/api/services/ShlinkApiClient.test.ts index 11131f97..9ce5af41 100644 --- a/test/api/services/ShlinkApiClient.test.ts +++ b/test/api/services/ShlinkApiClient.test.ts @@ -3,9 +3,8 @@ import { Mock } from 'ts-mockery'; import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; import { OptionalString } from '../../../src/utils/utils'; import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/api/types'; -import { ShortUrl } from '../../../src/short-urls/data'; +import { ShortUrl, ShortUrlsOrder } from '../../../src/short-urls/data'; import { Visit } from '../../../src/visits/types'; -import { ShortUrlsOrder } from '../../../src/short-urls/reducers/shortUrlsListParams'; describe('ShlinkApiClient', () => { const createAxios = (data: AxiosRequestConfig) => (async () => Promise.resolve(data)) as unknown as AxiosInstance; diff --git a/test/short-urls/ShortUrlsList.test.tsx b/test/short-urls/ShortUrlsList.test.tsx index 8ceadcbc..e6379e14 100644 --- a/test/short-urls/ShortUrlsList.test.tsx +++ b/test/short-urls/ShortUrlsList.test.tsx @@ -4,11 +4,10 @@ import { Mock } from 'ts-mockery'; import { History, Location } from 'history'; import { match } from 'react-router'; import shortUrlsListCreator from '../../src/short-urls/ShortUrlsList'; -import { ShortUrl } from '../../src/short-urls/data'; +import { OrderableFields, ShortUrl, ShortUrlsOrder } from '../../src/short-urls/data'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList'; import SortingDropdown from '../../src/utils/SortingDropdown'; -import { OrderableFields, ShortUrlsOrder } from '../../src/short-urls/reducers/shortUrlsListParams'; import Paginator from '../../src/short-urls/Paginator'; import { ReachableServer } from '../../src/servers/data'; import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks'; diff --git a/test/short-urls/ShortUrlsTable.test.tsx b/test/short-urls/ShortUrlsTable.test.tsx index 7164e40e..bcbccd23 100644 --- a/test/short-urls/ShortUrlsTable.test.tsx +++ b/test/short-urls/ShortUrlsTable.test.tsx @@ -2,10 +2,10 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Mock } from 'ts-mockery'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ShortUrlsTable as shortUrlsTableCreator } from '../../src/short-urls/ShortUrlsTable'; -import { OrderableFields, SORTABLE_FIELDS } from '../../src/short-urls/reducers/shortUrlsListParams'; import { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList'; import { ReachableServer, SelectedServer } from '../../src/servers/data'; import { SemVer } from '../../src/utils/helpers/version'; +import { OrderableFields, SORTABLE_FIELDS } from '../../src/short-urls/data'; describe('', () => { let wrapper: ShallowWrapper; From de32d899bcc21d88698714592d1dcd0eeccd72c0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 24 Dec 2021 14:15:28 +0100 Subject: [PATCH 7/9] Added new settings card to customize short URLs lists --- src/settings/Settings.tsx | 15 ++++++-- src/settings/ShortUrlsList.tsx | 24 ++++++++++++ src/settings/Tags.tsx | 2 +- src/settings/reducers/settings.ts | 11 ++++-- src/settings/services/provideServices.ts | 17 ++++++++- src/short-urls/ShortUrlsList.tsx | 2 +- test/settings/Settings.test.tsx | 4 +- test/settings/ShortUrlsList.test.tsx | 48 ++++++++++++++++++++++++ test/settings/reducers/settings.test.ts | 13 ++++++- test/short-urls/ShortUrlsList.test.tsx | 2 +- 10 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 src/settings/ShortUrlsList.tsx create mode 100644 test/settings/ShortUrlsList.test.tsx diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index e9d3369f..81d047ed 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -16,13 +16,20 @@ const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => ( ); -const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC, Visits: FC, Tags: FC) => () => ( +const Settings = ( + RealTimeUpdates: FC, + ShortUrlCreation: FC, + ShortUrlsList: FC, + UserInterface: FC, + Visits: FC, + Tags: FC, +) => () => ( ], // eslint-disable-line react/jsx-key - [ , ], // eslint-disable-line react/jsx-key - [ , ], // eslint-disable-line react/jsx-key + [ , ], // eslint-disable-line react/jsx-key + [ , ], // eslint-disable-line react/jsx-key + [ , ], // eslint-disable-line react/jsx-key ]} /> diff --git a/src/settings/ShortUrlsList.tsx b/src/settings/ShortUrlsList.tsx new file mode 100644 index 00000000..aadbb502 --- /dev/null +++ b/src/settings/ShortUrlsList.tsx @@ -0,0 +1,24 @@ +import { FC } from 'react'; +import { FormGroup } from 'reactstrap'; +import SortingDropdown from '../utils/SortingDropdown'; +import { SORTABLE_FIELDS } from '../short-urls/data'; +import { SimpleCard } from '../utils/SimpleCard'; +import { DEFAULT_SHORT_URLS_ORDERING, Settings, ShortUrlsListSettings } from './reducers/settings'; + +interface ShortUrlsListProps { + settings: Settings; + setShortUrlsListSettings: (settings: ShortUrlsListSettings) => void; +} + +export const ShortUrlsList: FC = ({ settings: { shortUrlsList }, setShortUrlsListSettings }) => ( + + + + setShortUrlsListSettings({ defaultOrdering: { field, dir } })} + /> + + +); diff --git a/src/settings/Tags.tsx b/src/settings/Tags.tsx index 63bc4aab..cbbd2af8 100644 --- a/src/settings/Tags.tsx +++ b/src/settings/Tags.tsx @@ -13,7 +13,7 @@ interface TagsProps { } export const Tags: FC = ({ settings: { tags }, setTagsSettings }) => ( - + ({ + type: SET_SETTINGS, + shortUrlsList: settings, +}); + export const setUiSettings = (settings: UiSettings): PartialSettingsAction => ({ type: SET_SETTINGS, ui: settings, diff --git a/src/settings/services/provideServices.ts b/src/settings/services/provideServices.ts index 93d82584..c54d37aa 100644 --- a/src/settings/services/provideServices.ts +++ b/src/settings/services/provideServices.ts @@ -4,6 +4,7 @@ import Settings from '../Settings'; import { setRealTimeUpdatesInterval, setShortUrlCreationSettings, + setShortUrlsListSettings, setTagsSettings, setUiSettings, setVisitsSettings, @@ -15,10 +16,20 @@ import { ShortUrlCreation } from '../ShortUrlCreation'; import { UserInterface } from '../UserInterface'; import { Visits } from '../Visits'; import { Tags } from '../Tags'; +import { ShortUrlsList } from '../ShortUrlsList'; const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components - bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation', 'UserInterface', 'Visits', 'Tags'); + bottle.serviceFactory( + 'Settings', + Settings, + 'RealTimeUpdates', + 'ShortUrlCreation', + 'ShortUrlsListSettings', + 'UserInterface', + 'Visits', + 'Tags', + ); bottle.decorator('Settings', withoutSelectedServer); bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ])); @@ -40,10 +51,14 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('Tags', () => Tags); bottle.decorator('Tags', connect([ 'settings' ], [ 'setTagsSettings' ])); + bottle.serviceFactory('ShortUrlsListSettings', () => ShortUrlsList); + bottle.decorator('ShortUrlsListSettings', connect([ 'settings' ], [ 'setShortUrlsListSettings' ])); + // Actions bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates); bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval); bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings); + bottle.serviceFactory('setShortUrlsListSettings', () => setShortUrlsListSettings); bottle.serviceFactory('setUiSettings', () => setUiSettings); bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings); bottle.serviceFactory('setTagsSettings', () => setTagsSettings); diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index b3e63943..09227fff 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -33,7 +33,7 @@ const ShortUrlsList = (ShortUrlsTable: FC, SearchBar: FC) = settings, }: ShortUrlsListProps) => { const serverId = getServerId(selectedServer); - const initialOrderBy = settings.shortUrlList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING; + const initialOrderBy = settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING; const [ order, setOrder ] = useState(initialOrderBy); const [{ tags, search, startDate, endDate }, toFirstPage ] = useShortUrlsQuery({ history, match, location }); const selectedTags = useMemo(() => tags?.split(',') ?? [], [ tags ]); diff --git a/test/settings/Settings.test.tsx b/test/settings/Settings.test.tsx index e8f3560c..0810cf8f 100644 --- a/test/settings/Settings.test.tsx +++ b/test/settings/Settings.test.tsx @@ -4,7 +4,7 @@ import NoMenuLayout from '../../src/common/NoMenuLayout'; describe('', () => { const Component = () => null; - const Settings = createSettings(Component, Component, Component, Component, Component); + const Settings = createSettings(Component, Component, Component, Component, Component, Component); it('renders a no-menu layout with the expected settings sections', () => { const wrapper = shallow(); @@ -13,6 +13,6 @@ describe('', () => { expect(layout).toHaveLength(1); expect(sections).toHaveLength(1); - expect((sections.prop('items') as any[]).flat()).toHaveLength(5); + expect((sections.prop('items') as any[]).flat()).toHaveLength(6); }); }); diff --git a/test/settings/ShortUrlsList.test.tsx b/test/settings/ShortUrlsList.test.tsx new file mode 100644 index 00000000..3c1136de --- /dev/null +++ b/test/settings/ShortUrlsList.test.tsx @@ -0,0 +1,48 @@ +import { shallow, ShallowWrapper } from 'enzyme'; +import { Mock } from 'ts-mockery'; +import { DEFAULT_SHORT_URLS_ORDERING, Settings, ShortUrlsListSettings } from '../../src/settings/reducers/settings'; +import { ShortUrlsList } from '../../src/settings/ShortUrlsList'; +import SortingDropdown from '../../src/utils/SortingDropdown'; +import { ShortUrlsOrder } from '../../src/short-urls/data'; + +describe('', () => { + let wrapper: ShallowWrapper; + const setSettings = jest.fn(); + const createWrapper = (shortUrlsList?: ShortUrlsListSettings) => { + wrapper = shallow( + ({ shortUrlsList })} setShortUrlsListSettings={setSettings} />, + ); + + return wrapper; + }; + + afterEach(() => wrapper?.unmount()); + afterEach(jest.clearAllMocks); + + it.each([ + [ undefined, DEFAULT_SHORT_URLS_ORDERING ], + [{}, DEFAULT_SHORT_URLS_ORDERING ], + [{ defaultOrdering: {} }, {}], + [{ defaultOrdering: { field: 'longUrl', dir: 'DESC' } as ShortUrlsOrder }, { field: 'longUrl', dir: 'DESC' }], + [{ defaultOrdering: { field: 'visits', dir: 'ASC' } as ShortUrlsOrder }, { field: 'visits', dir: 'ASC' }], + ])('shows expected ordering', (shortUrlsList, expectedOrder) => { + const wrapper = createWrapper(shortUrlsList); + const dropdown = wrapper.find(SortingDropdown); + + expect(dropdown.prop('order')).toEqual(expectedOrder); + }); + + it.each([ + [ undefined, undefined ], + [ 'longUrl', 'ASC' ], + [ 'visits', undefined ], + [ 'title', 'DESC' ], + ])('invokes setSettings when ordering changes', (field, dir) => { + const wrapper = createWrapper(); + const dropdown = wrapper.find(SortingDropdown); + + expect(setSettings).not.toHaveBeenCalled(); + dropdown.simulate('change', field, dir); + expect(setSettings).toHaveBeenCalledWith({ defaultOrdering: { field, dir } }); + }); +}); diff --git a/test/settings/reducers/settings.test.ts b/test/settings/reducers/settings.test.ts index 272e8905..09a7b14c 100644 --- a/test/settings/reducers/settings.test.ts +++ b/test/settings/reducers/settings.test.ts @@ -7,6 +7,7 @@ import reducer, { setUiSettings, setVisitsSettings, setTagsSettings, + setShortUrlsListSettings, } from '../../../src/settings/reducers/settings'; describe('settingsReducer', () => { @@ -14,8 +15,8 @@ describe('settingsReducer', () => { const shortUrlCreation = { validateUrls: false }; const ui = { theme: 'light' }; const visits = { defaultInterval: 'last30Days' }; - const shortUrlList = { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING }; - const settings = { realTimeUpdates, shortUrlCreation, ui, visits, shortUrlList }; + const shortUrlsList = { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING }; + const settings = { realTimeUpdates, shortUrlCreation, ui, visits, shortUrlsList }; describe('reducer', () => { it('returns realTimeUpdates when action is SET_SETTINGS', () => { @@ -70,4 +71,12 @@ describe('settingsReducer', () => { expect(result).toEqual({ type: SET_SETTINGS, tags: { defaultMode: 'list' } }); }); }); + + describe('setShortUrlsListSettings', () => { + it('creates action to set short URLs list settings', () => { + const result = setShortUrlsListSettings({ defaultOrdering: DEFAULT_SHORT_URLS_ORDERING }); + + expect(result).toEqual({ type: SET_SETTINGS, shortUrlsList: { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING } }); + }); + }); }); diff --git a/test/short-urls/ShortUrlsList.test.tsx b/test/short-urls/ShortUrlsList.test.tsx index e6379e14..3602667e 100644 --- a/test/short-urls/ShortUrlsList.test.tsx +++ b/test/short-urls/ShortUrlsList.test.tsx @@ -41,7 +41,7 @@ describe('', () => { shortUrlsList={shortUrlsList} history={Mock.of({ push })} selectedServer={Mock.of({ id: '1' })} - settings={Mock.of({ shortUrlList: { defaultOrdering } })} + settings={Mock.of({ shortUrlsList: { defaultOrdering } })} />, ).dive(); // Dive is needed as this component is wrapped in a HOC From 86c6acb7b8f29b77a2399948fabf1f51ef588d47 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 24 Dec 2021 14:16:42 +0100 Subject: [PATCH 8/9] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce34677b..18fbd3ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * [#535](https://github.com/shlinkio/shlink-web-client/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer. * [#531](https://github.com/shlinkio/shlink-web-client/pull/531) Added custom slug field to the basic creation form in the Overview page. +* [#537](https://github.com/shlinkio/shlink-web-client/pull/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs. ### Changed * [#534](https://github.com/shlinkio/shlink-web-client/pull/534) Updated axios. From eaadd6f7af0e4fdce9e59739e45b115ffade911d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 24 Dec 2021 15:05:15 +0100 Subject: [PATCH 9/9] Removed params param when dispatching list short RULs action, as it was used by a reducer that has been deleted --- src/short-urls/reducers/shortUrlsList.ts | 5 ++--- test/short-urls/reducers/shortUrlsList.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/short-urls/reducers/shortUrlsList.ts b/src/short-urls/reducers/shortUrlsList.ts index e50d931c..894975ac 100644 --- a/src/short-urls/reducers/shortUrlsList.ts +++ b/src/short-urls/reducers/shortUrlsList.ts @@ -24,7 +24,6 @@ export interface ShortUrlsList { export interface ListShortUrlsAction extends Action { shortUrls: ShlinkShortUrlsResponse; - params: ShlinkShortUrlsListParams; } export type ListShortUrlsCombinedAction = ( @@ -108,8 +107,8 @@ export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( try { const shortUrls = await listShortUrls(params); - dispatch({ type: LIST_SHORT_URLS, shortUrls, params }); + dispatch({ type: LIST_SHORT_URLS, shortUrls }); } catch (e) { - dispatch({ type: LIST_SHORT_URLS_ERROR, params }); + dispatch({ type: LIST_SHORT_URLS_ERROR }); } }; diff --git a/test/short-urls/reducers/shortUrlsList.test.ts b/test/short-urls/reducers/shortUrlsList.test.ts index e4ae692d..031f6a23 100644 --- a/test/short-urls/reducers/shortUrlsList.test.ts +++ b/test/short-urls/reducers/shortUrlsList.test.ts @@ -175,7 +175,7 @@ describe('shortUrlsListReducer', () => { expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_SHORT_URLS_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS, shortUrls: [], params: {} }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS, shortUrls: [] }); expect(listShortUrlsMock).toHaveBeenCalledTimes(1); }); @@ -188,7 +188,7 @@ describe('shortUrlsListReducer', () => { expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_SHORT_URLS_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS_ERROR, params: {} }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS_ERROR }); expect(listShortUrlsMock).toHaveBeenCalledTimes(1); });