mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +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 { FC } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSun, faMoon } from '@fortawesome/free-solid-svg-icons';
|
import { faSun, faMoon } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FormGroup } from 'reactstrap';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import ToggleSwitch from '../utils/ToggleSwitch';
|
import ToggleSwitch from '../utils/ToggleSwitch';
|
||||||
import { changeThemeInMarkup, Theme } from '../utils/theme';
|
import { changeThemeInMarkup, Theme } from '../utils/theme';
|
||||||
|
import { TagsModeDropdown } from '../tags/TagsModeDropdown';
|
||||||
|
import { capitalize } from '../utils/utils';
|
||||||
import { Settings, UiSettings } from './reducers/settings';
|
import { Settings, UiSettings } from './reducers/settings';
|
||||||
import './UserInterface.scss';
|
import './UserInterface.scss';
|
||||||
|
|
||||||
|
@ -14,17 +17,28 @@ interface UserInterfaceProps {
|
||||||
|
|
||||||
export const UserInterface: FC<UserInterfaceProps> = ({ settings: { ui }, setUiSettings }) => (
|
export const UserInterface: FC<UserInterfaceProps> = ({ settings: { ui }, setUiSettings }) => (
|
||||||
<SimpleCard title="User interface" className="h-100">
|
<SimpleCard title="User interface" className="h-100">
|
||||||
<FontAwesomeIcon icon={ui?.theme === 'dark' ? faMoon : faSun} className="user-interface__theme-icon" />
|
<FormGroup>
|
||||||
<ToggleSwitch
|
<FontAwesomeIcon icon={ui?.theme === 'dark' ? faMoon : faSun} className="user-interface__theme-icon" />
|
||||||
checked={ui?.theme === 'dark'}
|
<ToggleSwitch
|
||||||
onChange={(useDarkTheme) => {
|
checked={ui?.theme === 'dark'}
|
||||||
const theme: Theme = useDarkTheme ? 'dark' : 'light';
|
onChange={(useDarkTheme) => {
|
||||||
|
const theme: Theme = useDarkTheme ? 'dark' : 'light';
|
||||||
|
|
||||||
setUiSettings({ theme });
|
setUiSettings({ ...ui, theme });
|
||||||
changeThemeInMarkup(theme);
|
changeThemeInMarkup(theme);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Use dark theme.
|
Use dark theme.
|
||||||
</ToggleSwitch>
|
</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>
|
</SimpleCard>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,8 +24,11 @@ export interface ShortUrlCreationSettings {
|
||||||
tagFilteringMode?: TagFilteringMode;
|
tagFilteringMode?: TagFilteringMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TagsMode = 'cards' | 'list';
|
||||||
|
|
||||||
export interface UiSettings {
|
export interface UiSettings {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
|
tagsMode?: TagsMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VisitsSettings {
|
export interface VisitsSettings {
|
||||||
|
|
|
@ -7,21 +7,23 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
|
import { Settings, TagsMode } from '../settings/reducers/settings';
|
||||||
import { TagsList as TagsListState } from './reducers/tagsList';
|
import { TagsList as TagsListState } from './reducers/tagsList';
|
||||||
import { TagsListChildrenProps } from './data/TagsListChildrenProps';
|
import { TagsListChildrenProps } from './data/TagsListChildrenProps';
|
||||||
import { TagsMode, TagsModeDropdown } from './TagsModeDropdown';
|
import { TagsModeDropdown } from './TagsModeDropdown';
|
||||||
|
|
||||||
export interface TagsListProps {
|
export interface TagsListProps {
|
||||||
filterTags: (searchTerm: string) => void;
|
filterTags: (searchTerm: string) => void;
|
||||||
forceListTags: Function;
|
forceListTags: Function;
|
||||||
tagsList: TagsListState;
|
tagsList: TagsListState;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsListChildrenProps>) => boundToMercureHub((
|
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(() => {
|
useEffect(() => {
|
||||||
forceListTags();
|
forceListTags();
|
||||||
|
|
|
@ -3,16 +3,16 @@ import { DropdownItem } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faBars as listIcon, faThLarge as cardsIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faBars as listIcon, faThLarge as cardsIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { DropdownBtn } from '../utils/DropdownBtn';
|
import { DropdownBtn } from '../utils/DropdownBtn';
|
||||||
|
import { TagsMode } from '../settings/reducers/settings';
|
||||||
|
|
||||||
interface TagsModeDropdownProps {
|
interface TagsModeDropdownProps {
|
||||||
mode: TagsMode;
|
mode: TagsMode;
|
||||||
onChange: (newMode: TagsMode) => void;
|
onChange: (newMode: TagsMode) => void;
|
||||||
|
renderTitle?: (mode: TagsMode) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TagsMode = 'cards' | 'list';
|
export const TagsModeDropdown: FC<TagsModeDropdownProps> = ({ mode, onChange, renderTitle }) => (
|
||||||
|
<DropdownBtn text={renderTitle?.(mode) ?? `Display mode: ${mode}`}>
|
||||||
export const TagsModeDropdown: FC<TagsModeDropdownProps> = ({ mode, onChange }) => (
|
|
||||||
<DropdownBtn text={`Display mode: ${mode}`}>
|
|
||||||
<DropdownItem outline active={mode === 'cards'} onClick={() => onChange('cards')}>
|
<DropdownItem outline active={mode === 'cards'} onClick={() => onChange('cards')}>
|
||||||
<FontAwesomeIcon icon={cardsIcon} fixedWidth className="mr-1" /> Cards
|
<FontAwesomeIcon icon={cardsIcon} fixedWidth className="mr-1" /> Cards
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
|
@ -38,7 +38,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
|
|
||||||
bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable');
|
bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable');
|
||||||
bottle.decorator('TagsList', connect(
|
bottle.decorator('TagsList', connect(
|
||||||
[ 'tagsList', 'selectedServer', 'mercureInfo' ],
|
[ 'tagsList', 'selectedServer', 'mercureInfo', 'settings' ],
|
||||||
[ 'forceListTags', 'filterTags', 'createNewVisits', 'loadMercureInfo' ],
|
[ '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 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 { Mock } from 'ts-mockery';
|
||||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
|
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
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 { UserInterface } from '../../src/settings/UserInterface';
|
||||||
import ToggleSwitch from '../../src/utils/ToggleSwitch';
|
import ToggleSwitch from '../../src/utils/ToggleSwitch';
|
||||||
import { Theme } from '../../src/utils/theme';
|
import { Theme } from '../../src/utils/theme';
|
||||||
|
import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown';
|
||||||
|
|
||||||
describe('<UserInterface />', () => {
|
describe('<UserInterface />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const setUiSettings = jest.fn();
|
const setUiSettings = jest.fn();
|
||||||
const createWrapper = (ui?: UiSettings) => {
|
const createWrapper = (ui?: UiSettings) => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(<UserInterface settings={Mock.of<Settings>({ ui })} setUiSettings={setUiSettings} />);
|
||||||
<UserInterface
|
|
||||||
settings={Mock.of<Settings>({ ui })}
|
|
||||||
setUiSettings={setUiSettings}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
@ -49,7 +45,7 @@ describe('<UserInterface />', () => {
|
||||||
it.each([
|
it.each([
|
||||||
[ true, 'dark' ],
|
[ true, 'dark' ],
|
||||||
[ false, 'light' ],
|
[ false, 'light' ],
|
||||||
])('invokes setUiSettings when toggle value changes', (checked, theme) => {
|
])('invokes setUiSettings when theme toggle value changes', (checked, theme) => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
const toggle = wrapper.find(ToggleSwitch);
|
const toggle = wrapper.find(ToggleSwitch);
|
||||||
|
|
||||||
|
@ -57,4 +53,30 @@ describe('<UserInterface />', () => {
|
||||||
toggle.simulate('change', checked);
|
toggle.simulate('change', checked);
|
||||||
expect(setUiSettings).toHaveBeenCalledWith({ theme });
|
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 { Result } from '../../src/utils/Result';
|
||||||
import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown';
|
import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown';
|
||||||
import SearchField from '../../src/utils/SearchField';
|
import SearchField from '../../src/utils/SearchField';
|
||||||
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
|
|
||||||
describe('<TagsList />', () => {
|
describe('<TagsList />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
@ -23,6 +24,7 @@ describe('<TagsList />', () => {
|
||||||
forceListTags={identity}
|
forceListTags={identity}
|
||||||
filterTags={filterTags}
|
filterTags={filterTags}
|
||||||
tagsList={Mock.of<TagsList>(tagsList)}
|
tagsList={Mock.of<TagsList>(tagsList)}
|
||||||
|
settings={Mock.all<Settings>()}
|
||||||
/>,
|
/>,
|
||||||
).dive(); // Dive is needed as this component is wrapped in a HOC
|
).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('utils', () => {
|
||||||
describe('determineOrderDir', () => {
|
describe('determineOrderDir', () => {
|
||||||
|
@ -60,4 +60,15 @@ describe('utils', () => {
|
||||||
expect(nonEmptyValueOrNull(value)).toEqual(expected);
|
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