mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 06:47:29 +03:00
Merge pull request #1001 from acelaya-forks/feature/preferred-theme
Set default theme based on system preferences
This commit is contained in:
commit
598540aaac
11 changed files with 68 additions and 57 deletions
2
.github/workflows/deploy-preview.yml
vendored
2
.github/workflows/deploy-preview.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
npm ci && \
|
npm ci && \
|
||||||
node ./scripts/set-homepage.js /shlink-web-client/${GITHUB_HEAD_REF#refs/heads/} && \
|
node ./scripts/set-homepage.cjs /shlink-web-client/${GITHUB_HEAD_REF#refs/heads/} && \
|
||||||
npm run build
|
npm run build
|
||||||
- name: Deploy preview
|
- name: Deploy preview
|
||||||
uses: shlinkio/deploy-preview-action@v1.0.1
|
uses: shlinkio/deploy-preview-action@v1.0.1
|
||||||
|
|
|
@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [#338](https://github.com/shlinkio/shlink-web-client/issues/338) Extract `@shlinkio/shlink-web-component` and `@shlinkio/shlink-frontend-kit` as external libs.
|
* [#338](https://github.com/shlinkio/shlink-web-client/issues/338) Extract `@shlinkio/shlink-web-component` and `@shlinkio/shlink-frontend-kit` as external libs.
|
||||||
|
* [#978](https://github.com/shlinkio/shlink-web-client/issues/978) Use system preferred theme as default theme.
|
||||||
* Use API client from `@shlinkio/shlink-js-sdk` to consume Shlink servers.
|
* Use API client from `@shlinkio/shlink-js-sdk` to consume Shlink servers.
|
||||||
* [#902](https://github.com/shlinkio/shlink-web-client/pull/902) Docker image is no longer running as root. As a side effect, exposed port is `8080`, not `80` anymore.
|
* [#902](https://github.com/shlinkio/shlink-web-client/pull/902) Docker image is no longer running as root. As a side effect, exposed port is `8080`, not `80` anymore.
|
||||||
|
|
||||||
|
|
|
@ -21,3 +21,4 @@ afterEach(() => {
|
||||||
|
|
||||||
HTMLCanvasElement.prototype.getContext = (() => {}) as any;
|
HTMLCanvasElement.prototype.getContext = (() => {}) as any;
|
||||||
(global as any).scrollTo = () => {};
|
(global as any).scrollTo = () => {};
|
||||||
|
(global as any).matchMedia = () => ({ matches: false });
|
||||||
|
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -15,7 +15,7 @@
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@json2csv/plainjs": "^7.0.3",
|
"@json2csv/plainjs": "^7.0.3",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@shlinkio/data-manipulation": "^1.0.2",
|
"@shlinkio/data-manipulation": "^1.0.3",
|
||||||
"@shlinkio/shlink-frontend-kit": "^0.4.1",
|
"@shlinkio/shlink-frontend-kit": "^0.4.1",
|
||||||
"@shlinkio/shlink-js-sdk": "^0.2.0",
|
"@shlinkio/shlink-js-sdk": "^0.2.0",
|
||||||
"@shlinkio/shlink-web-component": "^0.4.1",
|
"@shlinkio/shlink-web-component": "^0.4.1",
|
||||||
|
@ -2934,9 +2934,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@shlinkio/data-manipulation": {
|
"node_modules/@shlinkio/data-manipulation": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@shlinkio/data-manipulation/-/data-manipulation-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@shlinkio/data-manipulation/-/data-manipulation-1.0.3.tgz",
|
||||||
"integrity": "sha512-Nzlr9cnKlehqSkoAFR8W/HJF9Nl2OU+zhOluQbSGBhUQgOjOJi00uAflib8LADKX2KUQexqDiz+sCwl8Pf4VKA=="
|
"integrity": "sha512-DPEU5IykmDrf7Z71miIi2xavSKkxAiqu8KuUsqEGmzGb31ENW0eZvMOfN+wNJbSLaRYqPsW465AwJqxTWUAGVA=="
|
||||||
},
|
},
|
||||||
"node_modules/@shlinkio/eslint-config-js-coding-standard": {
|
"node_modules/@shlinkio/eslint-config-js-coding-standard": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
|
@ -13049,9 +13049,9 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@shlinkio/data-manipulation": {
|
"@shlinkio/data-manipulation": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@shlinkio/data-manipulation/-/data-manipulation-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@shlinkio/data-manipulation/-/data-manipulation-1.0.3.tgz",
|
||||||
"integrity": "sha512-Nzlr9cnKlehqSkoAFR8W/HJF9Nl2OU+zhOluQbSGBhUQgOjOJi00uAflib8LADKX2KUQexqDiz+sCwl8Pf4VKA=="
|
"integrity": "sha512-DPEU5IykmDrf7Z71miIi2xavSKkxAiqu8KuUsqEGmzGb31ENW0eZvMOfN+wNJbSLaRYqPsW465AwJqxTWUAGVA=="
|
||||||
},
|
},
|
||||||
"@shlinkio/eslint-config-js-coding-standard": {
|
"@shlinkio/eslint-config-js-coding-standard": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
"repository": "https://github.com/shlinkio/shlink-web-client",
|
"repository": "https://github.com/shlinkio/shlink-web-client",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "npm run lint:css && npm run lint:js",
|
"lint": "npm run lint:css && npm run lint:js",
|
||||||
"lint:css": "stylelint src/*.scss src/**/*.scss",
|
"lint:css": "stylelint src/*.scss src/**/*.scss",
|
||||||
|
@ -31,7 +32,7 @@
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@json2csv/plainjs": "^7.0.3",
|
"@json2csv/plainjs": "^7.0.3",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@shlinkio/data-manipulation": "^1.0.2",
|
"@shlinkio/data-manipulation": "^1.0.3",
|
||||||
"@shlinkio/shlink-frontend-kit": "^0.4.1",
|
"@shlinkio/shlink-frontend-kit": "^0.4.1",
|
||||||
"@shlinkio/shlink-js-sdk": "^0.2.0",
|
"@shlinkio/shlink-js-sdk": "^0.2.0",
|
||||||
"@shlinkio/shlink-web-component": "^0.4.1",
|
"@shlinkio/shlink-web-component": "^0.4.1",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { changeThemeInMarkup } from '@shlinkio/shlink-frontend-kit';
|
import { changeThemeInMarkup, getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
@ -58,7 +58,7 @@ const App: FCWithDeps<AppProps, AppDeps> = (
|
||||||
}, [fetchServers]);
|
}, [fetchServers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
changeThemeInMarkup(settings.ui?.theme ?? 'light');
|
changeThemeInMarkup(settings.ui?.theme ?? getSystemPreferredTheme());
|
||||||
}, [settings.ui?.theme]);
|
}, [settings.ui?.theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,27 +1,34 @@
|
||||||
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 type { Theme } from '@shlinkio/shlink-frontend-kit';
|
import type { Theme } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit';
|
import { getSystemPreferredTheme, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import type { AppSettings, UiSettings } from './reducers/settings';
|
import type { AppSettings, UiSettings } from './reducers/settings';
|
||||||
import './UserInterfaceSettings.scss';
|
import './UserInterfaceSettings.scss';
|
||||||
|
|
||||||
interface UserInterfaceProps {
|
interface UserInterfaceProps {
|
||||||
settings: AppSettings;
|
settings: AppSettings;
|
||||||
setUiSettings: (settings: UiSettings) => void;
|
setUiSettings: (settings: UiSettings) => void;
|
||||||
|
|
||||||
|
/* Test seam */
|
||||||
|
_matchMedia?: typeof window.matchMedia;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserInterfaceSettings: FC<UserInterfaceProps> = ({ settings: { ui }, setUiSettings }) => (
|
export const UserInterfaceSettings: FC<UserInterfaceProps> = ({ settings: { ui }, setUiSettings, _matchMedia }) => {
|
||||||
<SimpleCard title="User interface" className="h-100">
|
const currentTheme = useMemo(() => ui?.theme ?? getSystemPreferredTheme(_matchMedia), [ui?.theme, _matchMedia]);
|
||||||
<FontAwesomeIcon icon={ui?.theme === 'dark' ? faMoon : faSun} className="user-interface__theme-icon" />
|
return (
|
||||||
<ToggleSwitch
|
<SimpleCard title="User interface" className="h-100">
|
||||||
checked={ui?.theme === 'dark'}
|
<FontAwesomeIcon icon={currentTheme === 'dark' ? faMoon : faSun} className="user-interface__theme-icon" />
|
||||||
onChange={(useDarkTheme) => {
|
<ToggleSwitch
|
||||||
const theme: Theme = useDarkTheme ? 'dark' : 'light';
|
checked={currentTheme === 'dark'}
|
||||||
setUiSettings({ ...ui, theme });
|
onChange={(useDarkTheme) => {
|
||||||
}}
|
const theme: Theme = useDarkTheme ? 'dark' : 'light';
|
||||||
>
|
setUiSettings({ ...ui, theme });
|
||||||
Use dark theme.
|
}}
|
||||||
</ToggleSwitch>
|
>
|
||||||
</SimpleCard>
|
Use dark theme.
|
||||||
);
|
</ToggleSwitch>
|
||||||
|
</SimpleCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { PayloadAction, PrepareAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { mergeDeepRight } from '@shlinkio/data-manipulation';
|
import { mergeDeepRight } from '@shlinkio/data-manipulation';
|
||||||
import type { Theme } from '@shlinkio/shlink-frontend-kit';
|
import type { Theme } from '@shlinkio/shlink-frontend-kit';
|
||||||
|
import { getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type {
|
import type {
|
||||||
Settings,
|
Settings,
|
||||||
ShortUrlCreationSettings,
|
ShortUrlCreationSettings,
|
||||||
|
@ -34,7 +35,7 @@ const initialState: AppSettings = {
|
||||||
validateUrls: false,
|
validateUrls: false,
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
theme: 'light',
|
theme: getSystemPreferredTheme(),
|
||||||
},
|
},
|
||||||
visits: {
|
visits: {
|
||||||
defaultInterval: 'last30Days',
|
defaultInterval: 'last30Days',
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { createMemoryHistory } from 'history';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { Router } from 'react-router-dom';
|
|
||||||
import { AppFactory } from '../../src/app/App';
|
import { AppFactory } from '../../src/app/App';
|
||||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||||
|
|
||||||
|
@ -18,22 +17,17 @@ describe('<App />', () => {
|
||||||
ShlinkVersionsContainer: () => <>ShlinkVersions</>,
|
ShlinkVersionsContainer: () => <>ShlinkVersions</>,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const setUp = (activeRoute = '/') => {
|
const setUp = (activeRoute = '/') => render(
|
||||||
const history = createMemoryHistory();
|
<MemoryRouter initialEntries={[{ pathname: activeRoute }]}>
|
||||||
history.push(activeRoute);
|
<App
|
||||||
|
fetchServers={() => {}}
|
||||||
return render(
|
servers={{}}
|
||||||
<Router location={history.location} navigator={history}>
|
settings={fromPartial({})}
|
||||||
<App
|
appUpdated
|
||||||
fetchServers={() => {}}
|
resetAppUpdate={() => {}}
|
||||||
servers={{}}
|
/>
|
||||||
settings={fromPartial({})}
|
</MemoryRouter>,
|
||||||
appUpdated
|
);
|
||||||
resetAppUpdate={() => {}}
|
|
||||||
/>
|
|
||||||
</Router>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { Theme } from '@shlinkio/shlink-frontend-kit';
|
|
||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import type { UiSettings } from '../../src/settings/reducers/settings';
|
import type { UiSettings } from '../../src/settings/reducers/settings';
|
||||||
|
@ -8,18 +7,25 @@ import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<UserInterfaceSettings />', () => {
|
describe('<UserInterfaceSettings />', () => {
|
||||||
const setUiSettings = vi.fn();
|
const setUiSettings = vi.fn();
|
||||||
const setUp = (ui?: UiSettings) => renderWithEvents(
|
const setUp = (ui?: UiSettings, defaultDarkTheme = false) => renderWithEvents(
|
||||||
<UserInterfaceSettings settings={fromPartial({ ui })} setUiSettings={setUiSettings} />,
|
<UserInterfaceSettings
|
||||||
|
settings={fromPartial({ ui })}
|
||||||
|
setUiSettings={setUiSettings}
|
||||||
|
_matchMedia={vi.fn().mockReturnValue({ matches: defaultDarkTheme })}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{ theme: 'dark' as Theme }, true],
|
[{ theme: 'dark' as const }, true, true],
|
||||||
[{ theme: 'light' as Theme }, false],
|
[{ theme: 'dark' as const }, false, true],
|
||||||
[undefined, false],
|
[{ theme: 'light' as const }, true, false],
|
||||||
])('toggles switch if theme is dark', (ui, expectedChecked) => {
|
[{ theme: 'light' as const }, false, false],
|
||||||
setUp(ui);
|
[undefined, false, false],
|
||||||
|
[undefined, true, true],
|
||||||
|
])('toggles switch if theme is dark', (ui, defaultDarkTheme, expectedChecked) => {
|
||||||
|
setUp(ui, defaultDarkTheme);
|
||||||
|
|
||||||
if (expectedChecked) {
|
if (expectedChecked) {
|
||||||
expect(screen.getByLabelText('Use dark theme.')).toBeChecked();
|
expect(screen.getByLabelText('Use dark theme.')).toBeChecked();
|
||||||
|
@ -29,8 +35,8 @@ describe('<UserInterfaceSettings />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{ theme: 'dark' as Theme }],
|
[{ theme: 'dark' as const }],
|
||||||
[{ theme: 'light' as Theme }],
|
[{ theme: 'light' as const }],
|
||||||
[undefined],
|
[undefined],
|
||||||
])('shows different icons based on theme', (ui) => {
|
])('shows different icons based on theme', (ui) => {
|
||||||
setUp(ui);
|
setUp(ui);
|
||||||
|
@ -38,8 +44,8 @@ describe('<UserInterfaceSettings />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['light' as Theme, 'dark' as Theme],
|
['light' as const, 'dark' as const],
|
||||||
['dark' as Theme, 'light' as Theme],
|
['dark' as const, 'light' as const],
|
||||||
])('invokes setUiSettings when theme toggle value changes', async (initialTheme, expectedTheme) => {
|
])('invokes setUiSettings when theme toggle value changes', async (initialTheme, expectedTheme) => {
|
||||||
const { user } = setUp({ theme: initialTheme });
|
const { user } = setUp({ theme: initialTheme });
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue