From 5598fe0f530e37ddae556f90ffd4e8b4f8a87476 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 23 Dec 2021 17:53:06 +0100 Subject: [PATCH] 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 }); - }); });