From 1da7119c5c4befdf87170992c9999dc88bc5d7b2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 25 Sep 2021 08:20:56 +0200 Subject: [PATCH] Added new setting to determine default display mode for tags --- src/settings/UserInterface.tsx | 36 ++++++++++++++++++-------- src/settings/reducers/settings.ts | 3 +++ src/tags/TagsList.tsx | 8 +++--- src/tags/TagsModeDropdown.tsx | 8 +++--- src/tags/services/provideServices.ts | 2 +- src/utils/utils.ts | 2 ++ test/settings/UserInterface.test.tsx | 38 ++++++++++++++++++++++------ test/tags/TagsList.test.tsx | 2 ++ test/utils/utils.test.ts | 13 +++++++++- 9 files changed, 84 insertions(+), 28 deletions(-) diff --git a/src/settings/UserInterface.tsx b/src/settings/UserInterface.tsx index 9a915078..fbd41a8c 100644 --- a/src/settings/UserInterface.tsx +++ b/src/settings/UserInterface.tsx @@ -1,9 +1,12 @@ import { FC } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSun, faMoon } from '@fortawesome/free-solid-svg-icons'; +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'; @@ -14,17 +17,28 @@ interface UserInterfaceProps { export const UserInterface: FC = ({ settings: { ui }, setUiSettings }) => ( - - { - const theme: Theme = useDarkTheme ? 'dark' : 'light'; + + + { + const theme: Theme = useDarkTheme ? 'dark' : 'light'; - setUiSettings({ theme }); - changeThemeInMarkup(theme); - }} - > - Use dark theme. - + setUiSettings({ ...ui, theme }); + changeThemeInMarkup(theme); + }} + > + 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/reducers/settings.ts b/src/settings/reducers/settings.ts index f0505001..3fb0ca3d 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -24,8 +24,11 @@ export interface ShortUrlCreationSettings { tagFilteringMode?: TagFilteringMode; } +export type TagsMode = 'cards' | 'list'; + export interface UiSettings { theme: Theme; + tagsMode?: TagsMode; } export interface VisitsSettings { diff --git a/src/tags/TagsList.tsx b/src/tags/TagsList.tsx index 513c449b..4fdbf442 100644 --- a/src/tags/TagsList.tsx +++ b/src/tags/TagsList.tsx @@ -7,21 +7,23 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Result } from '../utils/Result'; import { ShlinkApiError } from '../api/ShlinkApiError'; import { Topics } from '../mercure/helpers/Topics'; +import { Settings, TagsMode } from '../settings/reducers/settings'; import { TagsList as TagsListState } from './reducers/tagsList'; import { TagsListChildrenProps } from './data/TagsListChildrenProps'; -import { TagsMode, TagsModeDropdown } from './TagsModeDropdown'; +import { TagsModeDropdown } from './TagsModeDropdown'; export interface TagsListProps { filterTags: (searchTerm: string) => void; forceListTags: Function; tagsList: TagsListState; selectedServer: SelectedServer; + settings: Settings; } const TagsList = (TagsCards: FC, TagsTable: FC) => boundToMercureHub(( - { filterTags, forceListTags, tagsList, selectedServer }: TagsListProps, + { filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps, ) => { - const [ mode, setMode ] = useState('cards'); + const [ mode, setMode ] = useState(settings.ui?.tagsMode ?? 'cards'); useEffect(() => { forceListTags(); diff --git a/src/tags/TagsModeDropdown.tsx b/src/tags/TagsModeDropdown.tsx index 816aa2b9..147e43d1 100644 --- a/src/tags/TagsModeDropdown.tsx +++ b/src/tags/TagsModeDropdown.tsx @@ -3,16 +3,16 @@ import { DropdownItem } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBars as listIcon, faThLarge as cardsIcon } from '@fortawesome/free-solid-svg-icons'; import { DropdownBtn } from '../utils/DropdownBtn'; +import { TagsMode } from '../settings/reducers/settings'; interface TagsModeDropdownProps { mode: TagsMode; onChange: (newMode: TagsMode) => void; + renderTitle?: (mode: TagsMode) => string; } -export type TagsMode = 'cards' | 'list'; - -export const TagsModeDropdown: FC = ({ mode, onChange }) => ( - +export const TagsModeDropdown: FC = ({ mode, onChange, renderTitle }) => ( + onChange('cards')}> Cards diff --git a/src/tags/services/provideServices.ts b/src/tags/services/provideServices.ts index f28d048b..f17e856e 100644 --- a/src/tags/services/provideServices.ts +++ b/src/tags/services/provideServices.ts @@ -38,7 +38,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable'); bottle.decorator('TagsList', connect( - [ 'tagsList', 'selectedServer', 'mercureInfo' ], + [ 'tagsList', 'selectedServer', 'mercureInfo', 'settings' ], [ 'forceListTags', 'filterTags', 'createNewVisits', 'loadMercureInfo' ], )); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 56a044ed..2b62871e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -45,3 +45,5 @@ export type RecursivePartial = { }; export const nonEmptyValueOrNull = (value: T): T | null => isEmpty(value) ? null : value; + +export const capitalize = (value: T): string => `${value.charAt(0).toUpperCase()}${value.slice(1)}`; diff --git a/test/settings/UserInterface.test.tsx b/test/settings/UserInterface.test.tsx index 02a0a037..3b205c0e 100644 --- a/test/settings/UserInterface.test.tsx +++ b/test/settings/UserInterface.test.tsx @@ -2,21 +2,17 @@ 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, UiSettings } from '../../src/settings/reducers/settings'; +import { Settings, TagsMode, 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; const setUiSettings = jest.fn(); const createWrapper = (ui?: UiSettings) => { - wrapper = shallow( - ({ ui })} - setUiSettings={setUiSettings} - />, - ); + wrapper = shallow(({ ui })} setUiSettings={setUiSettings} />); return wrapper; }; @@ -49,7 +45,7 @@ describe('', () => { it.each([ [ true, 'dark' ], [ false, 'light' ], - ])('invokes setUiSettings when toggle value changes', (checked, theme) => { + ])('invokes setUiSettings when theme toggle value changes', (checked, theme) => { const wrapper = createWrapper(); const toggle = wrapper.find(ToggleSwitch); @@ -57,4 +53,30 @@ 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 }); + }); }); diff --git a/test/tags/TagsList.test.tsx b/test/tags/TagsList.test.tsx index 5f1efc23..85932fb6 100644 --- a/test/tags/TagsList.test.tsx +++ b/test/tags/TagsList.test.tsx @@ -8,6 +8,7 @@ import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { Result } from '../../src/utils/Result'; import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown'; import SearchField from '../../src/utils/SearchField'; +import { Settings } from '../../src/settings/reducers/settings'; describe('', () => { let wrapper: ShallowWrapper; @@ -23,6 +24,7 @@ describe('', () => { forceListTags={identity} filterTags={filterTags} tagsList={Mock.of(tagsList)} + settings={Mock.all()} />, ).dive(); // Dive is needed as this component is wrapped in a HOC diff --git a/test/utils/utils.test.ts b/test/utils/utils.test.ts index 8bf04278..22dcb8a4 100644 --- a/test/utils/utils.test.ts +++ b/test/utils/utils.test.ts @@ -1,4 +1,4 @@ -import { determineOrderDir, nonEmptyValueOrNull, rangeOf } from '../../src/utils/utils'; +import { capitalize, determineOrderDir, nonEmptyValueOrNull, rangeOf } from '../../src/utils/utils'; describe('utils', () => { describe('determineOrderDir', () => { @@ -60,4 +60,15 @@ describe('utils', () => { expect(nonEmptyValueOrNull(value)).toEqual(expected); }); }); + + describe('capitalize', () => { + it.each([ + [ 'foo', 'Foo' ], + [ 'BAR', 'BAR' ], + [ 'bAZ', 'BAZ' ], + [ 'with spaces', 'With spaces' ], + ])('sets first letter in uppercase', (value, expectedResult) => { + expect(capitalize(value)).toEqual(expectedResult); + }); + }); });