mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Added new setting to determine default display mode for tags
This commit is contained in:
parent
01f6f11ee2
commit
1da7119c5c
9 changed files with 84 additions and 28 deletions
|
@ -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<UserInterfaceProps> = ({ settings: { ui }, setUiSettings }) => (
|
||||
<SimpleCard title="User interface" className="h-100">
|
||||
<FormGroup>
|
||||
<FontAwesomeIcon icon={ui?.theme === 'dark' ? faMoon : faSun} className="user-interface__theme-icon" />
|
||||
<ToggleSwitch
|
||||
checked={ui?.theme === 'dark'}
|
||||
onChange={(useDarkTheme) => {
|
||||
const theme: Theme = useDarkTheme ? 'dark' : 'light';
|
||||
|
||||
setUiSettings({ theme });
|
||||
setUiSettings({ ...ui, theme });
|
||||
changeThemeInMarkup(theme);
|
||||
}}
|
||||
>
|
||||
Use dark theme.
|
||||
</ToggleSwitch>
|
||||
</FormGroup>
|
||||
<FormGroup className="mb-0">
|
||||
<label>Default display mode when managing tags:</label>
|
||||
<TagsModeDropdown
|
||||
mode={ui?.tagsMode ?? 'cards'}
|
||||
renderTitle={(tagsMode) => capitalize(tagsMode)}
|
||||
onChange={(tagsMode) => setUiSettings({ ...ui ?? { theme: 'light' }, tagsMode })}
|
||||
/>
|
||||
<small className="form-text text-muted">Tags will be displayed as <b>{ui?.tagsMode ?? 'cards'}</b>.</small>
|
||||
</FormGroup>
|
||||
</SimpleCard>
|
||||
);
|
||||
|
|
|
@ -24,8 +24,11 @@ export interface ShortUrlCreationSettings {
|
|||
tagFilteringMode?: TagFilteringMode;
|
||||
}
|
||||
|
||||
export type TagsMode = 'cards' | 'list';
|
||||
|
||||
export interface UiSettings {
|
||||
theme: Theme;
|
||||
tagsMode?: TagsMode;
|
||||
}
|
||||
|
||||
export interface VisitsSettings {
|
||||
|
|
|
@ -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<TagsListChildrenProps>, TagsTable: FC<TagsListChildrenProps>) => boundToMercureHub((
|
||||
{ filterTags, forceListTags, tagsList, selectedServer }: TagsListProps,
|
||||
{ filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps,
|
||||
) => {
|
||||
const [ mode, setMode ] = useState<TagsMode>('cards');
|
||||
const [ mode, setMode ] = useState<TagsMode>(settings.ui?.tagsMode ?? 'cards');
|
||||
|
||||
useEffect(() => {
|
||||
forceListTags();
|
||||
|
|
|
@ -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<TagsModeDropdownProps> = ({ mode, onChange }) => (
|
||||
<DropdownBtn text={`Display mode: ${mode}`}>
|
||||
export const TagsModeDropdown: FC<TagsModeDropdownProps> = ({ mode, onChange, renderTitle }) => (
|
||||
<DropdownBtn text={renderTitle?.(mode) ?? `Display mode: ${mode}`}>
|
||||
<DropdownItem outline active={mode === 'cards'} onClick={() => onChange('cards')}>
|
||||
<FontAwesomeIcon icon={cardsIcon} fixedWidth className="mr-1" /> Cards
|
||||
</DropdownItem>
|
||||
|
|
|
@ -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' ],
|
||||
));
|
||||
|
||||
|
|
|
@ -45,3 +45,5 @@ export type RecursivePartial<T> = {
|
|||
};
|
||||
|
||||
export const nonEmptyValueOrNull = <T>(value: T): T | null => isEmpty(value) ? null : value;
|
||||
|
||||
export const capitalize = <T extends string>(value: T): string => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
||||
|
|
|
@ -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('<UserInterface />', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
const setUiSettings = jest.fn();
|
||||
const createWrapper = (ui?: UiSettings) => {
|
||||
wrapper = shallow(
|
||||
<UserInterface
|
||||
settings={Mock.of<Settings>({ ui })}
|
||||
setUiSettings={setUiSettings}
|
||||
/>,
|
||||
);
|
||||
wrapper = shallow(<UserInterface settings={Mock.of<Settings>({ ui })} setUiSettings={setUiSettings} />);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
@ -49,7 +45,7 @@ describe('<UserInterface />', () => {
|
|||
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('<UserInterface />', () => {
|
|||
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 <b>${expectedMode}</b>.`);
|
||||
});
|
||||
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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('<TagsList />', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
@ -23,6 +24,7 @@ describe('<TagsList />', () => {
|
|||
forceListTags={identity}
|
||||
filterTags={filterTags}
|
||||
tagsList={Mock.of<TagsList>(tagsList)}
|
||||
settings={Mock.all<Settings>()}
|
||||
/>,
|
||||
).dive(); // Dive is needed as this component is wrapped in a HOC
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue