Skip to content

Commit 2cdbbf4

Browse files
authored
Merge pull request #651 from acelaya-forks/feature/qr-settigs
Allow QR code settings to be configured
2 parents 76605d3 + 25dd6ff commit 2cdbbf4

31 files changed

+467
-115
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
66

7-
## [Unreleased]
7+
## [0.13.3]
88
### Added
99
* [#641](https://github.com/shlinkio/shlink-web-component/issues/641) Allow custom logos to be selected for the short URL QR code.
10+
* [#640](https://github.com/shlinkio/shlink-web-component/issues/640) Allow default QR code settings to be handled via app settings.
1011

1112
### Changed
1213
* *Nothing*

src/settings/components/RealTimeUpdatesSettings.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { FormText } from './FormText';
77

88
export type RealTimeUpdatesProps = {
99
toggleRealTimeUpdates: (enabled: boolean) => void;
10-
setRealTimeUpdatesInterval: (interval: number) => void;
10+
onIntervalChange: (interval: number) => void;
1111
};
1212

1313
export const RealTimeUpdatesSettings = (
14-
{ toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
14+
{ toggleRealTimeUpdates, onIntervalChange }: RealTimeUpdatesProps,
1515
) => {
1616
const { enabled, interval } = useSetting('realTimeUpdates', { enabled: true });
1717
const inputId = useId();
@@ -39,7 +39,7 @@ export const RealTimeUpdatesSettings = (
3939
disabled={!enabled}
4040
value={`${interval ?? ''}`}
4141
id={inputId}
42-
onChange={({ target }) => setRealTimeUpdatesInterval(Number(target.value))}
42+
onChange={({ target }) => onIntervalChange(Number(target.value))}
4343
/>
4444
{enabled && (
4545
<FormText>

src/settings/components/ShlinkWebSettings.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { mergeDeepRight } from '@shlinkio/data-manipulation';
22
import { NavPillItem, NavPills } from '@shlinkio/shlink-frontend-kit';
3+
import { clsx } from 'clsx';
34
import type { FC, PropsWithChildren } from 'react';
4-
import { Children , useCallback } from 'react';
5+
import { useCallback } from 'react';
56
import { Navigate, Route, Routes } from 'react-router';
67
import type { DeepPartial } from '../../utils/types';
8+
import type { QrCodeSettings } from '..';
79
import { SettingsProvider } from '..';
810
import type { RealTimeUpdatesSettings, Settings, ShortUrlsListSettings } from '../types';
11+
import { QrCodeColorSettings } from './qr-codes/QrCodeColorSettings';
12+
import { QrCodeFormatSettings } from './qr-codes/QrCodeFormatSettings';
13+
import { QrCodeSizeSettings } from './qr-codes/QrCodeSizeSettings';
914
import { RealTimeUpdatesSettings as RealTimeUpdates } from './RealTimeUpdatesSettings';
1015
import { ShortUrlCreationSettings as ShortUrlCreation } from './ShortUrlCreationSettings';
1116
import { ShortUrlsListSettings as ShortUrlsList } from './ShortUrlsListSettings';
@@ -23,9 +28,10 @@ export type ShlinkWebSettingsProps = {
2328
updateSettings?: (settings: Settings) => void;
2429
};
2530

26-
const SettingsSections: FC<PropsWithChildren> = ({ children }) => Children.map(
27-
children,
28-
(child, index) => <div key={index} className="mb-3">{child}</div>,
31+
const SettingsSections: FC<PropsWithChildren<{ className?: string }>> = ({ children, className }) => (
32+
<div className={clsx('d-flex flex-column gap-3', className)}>
33+
{children}
34+
</div>
2935
);
3036

3137
export const ShlinkWebSettings: FC<ShlinkWebSettingsProps> = ({
@@ -50,12 +56,17 @@ export const ShlinkWebSettings: FC<ShlinkWebSettingsProps> = ({
5056
<Prop extends keyof Settings>(prop: Prop, value: Settings[Prop]) => updatePartialSettings({ [prop]: value }),
5157
[updatePartialSettings],
5258
);
59+
const updateQrCodeSettings = useCallback(
60+
(s: QrCodeSettings) => updateSettingsProp('qrCodes', s),
61+
[updateSettingsProp],
62+
);
5363

5464
return (
5565
<SettingsProvider value={settings}>
5666
<NavPills className="mb-3">
5767
<NavPillItem to="../general">General</NavPillItem>
5868
<NavPillItem to="../short-urls">Short URLs</NavPillItem>
69+
<NavPillItem to="../qr-codes">QR codes</NavPillItem>
5970
<NavPillItem to="../other-items">Other items</NavPillItem>
6071
</NavPills>
6172

@@ -64,10 +75,10 @@ export const ShlinkWebSettings: FC<ShlinkWebSettingsProps> = ({
6475
path="general"
6576
element={(
6677
<SettingsSections>
67-
<UserInterfaceSettings updateUiSettings={(v) => updateSettingsProp('ui', v)} />
78+
<UserInterfaceSettings onChange={(v) => updateSettingsProp('ui', v)} />
6879
<RealTimeUpdates
6980
toggleRealTimeUpdates={toggleRealTimeUpdates}
70-
setRealTimeUpdatesInterval={setRealTimeUpdatesInterval}
81+
onIntervalChange={setRealTimeUpdatesInterval}
7182
/>
7283
</SettingsSections>
7384
)}
@@ -76,10 +87,10 @@ export const ShlinkWebSettings: FC<ShlinkWebSettingsProps> = ({
7687
path="short-urls"
7788
element={(
7889
<SettingsSections>
79-
<ShortUrlCreation updateShortUrlCreationSettings={(v) => updateSettingsProp('shortUrlCreation', v)} />
90+
<ShortUrlCreation onChange={(v) => updateSettingsProp('shortUrlCreation', v)} />
8091
<ShortUrlsList
8192
defaultOrdering={defaultShortUrlsListOrdering}
82-
updateShortUrlsListSettings={(v) => updateSettingsProp('shortUrlsList', v)}
93+
onChange={(v) => updateSettingsProp('shortUrlsList', v)}
8394
/>
8495
</SettingsSections>
8596
)}
@@ -88,8 +99,20 @@ export const ShlinkWebSettings: FC<ShlinkWebSettingsProps> = ({
8899
path="other-items"
89100
element={(
90101
<SettingsSections>
91-
<Tags updateTagsSettings={(v) => updateSettingsProp('tags', v)} />
92-
<Visits updateVisitsSettings={(v) => updateSettingsProp('visits', v)} />
102+
<Tags onChange={(v) => updateSettingsProp('tags', v)} />
103+
<Visits onChange={(v) => updateSettingsProp('visits', v)} />
104+
</SettingsSections>
105+
)}
106+
/>
107+
<Route
108+
path="qr-codes"
109+
element={(
110+
<SettingsSections>
111+
<div className="d-flex flex-column flex-lg-row gap-3">
112+
<QrCodeSizeSettings onChange={updateQrCodeSettings} className="w-100" />
113+
<QrCodeColorSettings onChange={updateQrCodeSettings} className="w-100" />
114+
</div>
115+
<QrCodeFormatSettings onChange={updateQrCodeSettings} />
93116
</SettingsSections>
94117
)}
95118
/>

src/settings/components/ShortUrlCreationSettings.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { FormText } from './FormText';
88
type TagFilteringMode = NonNullable<ShortUrlsSettings['tagFilteringMode']>;
99

1010
interface ShortUrlCreationProps {
11-
updateShortUrlCreationSettings: (settings: ShortUrlsSettings) => void;
11+
onChange: (settings: ShortUrlsSettings) => void;
1212
}
1313

1414
const tagFilteringModeText = (tagFilteringMode: TagFilteringMode | undefined): string =>
@@ -19,17 +19,17 @@ const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): R
1919
: <>The list of suggested tags will contain those <b>starting with</b> provided input.</>
2020
);
2121

22-
export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ updateShortUrlCreationSettings }) => {
22+
export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ onChange }) => {
2323
const shortUrlCreation = useSetting('shortUrlCreation', { validateUrls: false });
24-
const changeTagsFilteringMode = (tagFilteringMode: TagFilteringMode) => () => updateShortUrlCreationSettings(
24+
const changeTagsFilteringMode = (tagFilteringMode: TagFilteringMode) => () => onChange(
2525
{ ...shortUrlCreation ?? { validateUrls: false }, tagFilteringMode },
2626
);
2727

2828
return (
2929
<SimpleCard title="Short URLs form" className="h-100" bodyClassName="d-flex flex-column gap-3">
3030
<ToggleSwitch
3131
checked={shortUrlCreation.validateUrls ?? false}
32-
onChange={(validateUrls) => updateShortUrlCreationSettings({ ...shortUrlCreation, validateUrls })}
32+
onChange={(validateUrls) => onChange({ ...shortUrlCreation, validateUrls })}
3333
>
3434
Request validation on long URLs when creating new short URLs.{' '}
3535
<b>This option is ignored by Shlink {'>='}4.0.0</b>
@@ -40,7 +40,7 @@ export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ updateShor
4040
</ToggleSwitch>
4141
<ToggleSwitch
4242
checked={shortUrlCreation.forwardQuery ?? true}
43-
onChange={(forwardQuery) => updateShortUrlCreationSettings({ ...shortUrlCreation, forwardQuery })}
43+
onChange={(forwardQuery) => onChange({ ...shortUrlCreation, forwardQuery })}
4444
>
4545
Make all new short URLs forward their query params to the long URL.
4646
<FormText>

src/settings/components/ShortUrlsListSettings.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useSetting } from '..';
55
import { FormText } from './FormText';
66

77
export type ShortUrlsListSettingsProps = {
8-
updateShortUrlsListSettings: (settings: ShortUrlsSettings) => void;
8+
onChange: (settings: ShortUrlsSettings) => void;
99
defaultOrdering: NonNullable<ShortUrlsSettings['defaultOrdering']>;
1010
};
1111

@@ -17,17 +17,15 @@ const SHORT_URLS_ORDERABLE_FIELDS = {
1717
visits: 'Visits',
1818
};
1919

20-
export const ShortUrlsListSettings: FC<ShortUrlsListSettingsProps> = (
21-
{ updateShortUrlsListSettings, defaultOrdering },
22-
) => {
20+
export const ShortUrlsListSettings: FC<ShortUrlsListSettingsProps> = ({ onChange, defaultOrdering }) => {
2321
const shortUrlsList = useSetting('shortUrlsList');
2422
const confirmDeletions = shortUrlsList?.confirmDeletions ?? true;
2523

2624
return (
2725
<SimpleCard title="Short URLs list" className="h-100" bodyClassName="d-flex flex-column gap-3">
2826
<ToggleSwitch
2927
checked={confirmDeletions}
30-
onChange={(confirmDeletions) => updateShortUrlsListSettings({ ...shortUrlsList, confirmDeletions })}
28+
onChange={(confirmDeletions) => onChange({ ...shortUrlsList, confirmDeletions })}
3129
>
3230
Request confirmation before deleting a short URL.
3331
<FormText>
@@ -38,7 +36,7 @@ export const ShortUrlsListSettings: FC<ShortUrlsListSettingsProps> = (
3836
<OrderingDropdown
3937
items={SHORT_URLS_ORDERABLE_FIELDS}
4038
order={shortUrlsList?.defaultOrdering ?? defaultOrdering}
41-
onChange={(field, dir) => updateShortUrlsListSettings({ defaultOrdering: { field, dir } })}
39+
onChange={(field, dir) => onChange({ defaultOrdering: { field, dir } })}
4240
/>
4341
</LabeledFormGroup>
4442
</SimpleCard>

src/settings/components/TagsSettings.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { TagsSettings as TagsSettingsOptions } from '..';
44
import { useSetting } from '..';
55

66
export type TagsProps = {
7-
updateTagsSettings: (settings: TagsSettingsOptions) => void;
7+
onChange: (settings: TagsSettingsOptions) => void;
88
};
99

1010
const TAGS_ORDERABLE_FIELDS = {
@@ -13,7 +13,7 @@ const TAGS_ORDERABLE_FIELDS = {
1313
visits: 'Visits',
1414
};
1515

16-
export const TagsSettings: FC<TagsProps> = ({ updateTagsSettings }) => {
16+
export const TagsSettings: FC<TagsProps> = ({ onChange }) => {
1717
const tags = useSetting('tags', {});
1818

1919
return (
@@ -22,7 +22,7 @@ export const TagsSettings: FC<TagsProps> = ({ updateTagsSettings }) => {
2222
<OrderingDropdown
2323
items={TAGS_ORDERABLE_FIELDS}
2424
order={tags.defaultOrdering ?? {}}
25-
onChange={(field, dir) => updateTagsSettings({ ...tags, defaultOrdering: { field, dir } })}
25+
onChange={(field, dir) => onChange({ ...tags, defaultOrdering: { field, dir } })}
2626
/>
2727
</LabeledFormGroup>
2828
</SimpleCard>

src/settings/components/UserInterfaceSettings.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,27 @@
11
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3-
import type { Theme } from '@shlinkio/shlink-frontend-kit';
43
import { getSystemPreferredTheme, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit';
54
import type { FC } from 'react';
65
import { useMemo } from 'react';
76
import { useSetting } from '..';
87
import type { UiSettings } from '../types';
98

109
interface UserInterfaceProps {
11-
updateUiSettings: (settings: UiSettings) => void;
10+
onChange: (settings: UiSettings) => void;
1211

1312
/* Test seam */
1413
_matchMedia?: typeof window.matchMedia;
1514
}
1615

17-
export const UserInterfaceSettings: FC<UserInterfaceProps> = ({ updateUiSettings, _matchMedia }) => {
16+
export const UserInterfaceSettings: FC<UserInterfaceProps> = ({ onChange, _matchMedia }) => {
1817
const ui = useSetting('ui');
1918
const currentTheme = useMemo(() => ui?.theme ?? getSystemPreferredTheme(_matchMedia), [ui?.theme, _matchMedia]);
2019

2120
return (
2221
<SimpleCard title="User interface" className="h-100" bodyClassName="d-flex justify-content-between align-items-center">
2322
<ToggleSwitch
2423
checked={currentTheme === 'dark'}
25-
onChange={(useDarkTheme) => {
26-
const theme: Theme = useDarkTheme ? 'dark' : 'light';
27-
updateUiSettings({ ...ui, theme });
28-
}}
24+
onChange={(useDarkTheme) => onChange({ ...ui, theme: useDarkTheme ? 'dark' : 'light' })}
2925
>
3026
Use dark theme.
3127
</ToggleSwitch>

src/settings/components/VisitsSettings.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@ import { DateIntervalSelector } from './DateIntervalSelector';
88
import { FormText } from './FormText';
99

1010
export type VisitsProps = {
11-
updateVisitsSettings: (settings: VisitsSettingsConfig) => void;
11+
onChange: (settings: VisitsSettingsConfig) => void;
1212
};
1313

1414
const currentDefaultInterval = (visitsSettings?: VisitsSettingsConfig): DateInterval =>
1515
visitsSettings?.defaultInterval ?? 'last30Days';
1616

17-
export const VisitsSettings: FC<VisitsProps> = ({ updateVisitsSettings }) => {
17+
export const VisitsSettings: FC<VisitsProps> = ({ onChange }) => {
1818
const visitsSettings = useSetting('visits');
1919
const updateSettings = useCallback(
20-
({ defaultInterval, ...rest }: Partial<VisitsSettingsConfig>) => updateVisitsSettings(
20+
({ defaultInterval, ...rest }: Partial<VisitsSettingsConfig>) => onChange(
2121
{ defaultInterval: defaultInterval ?? currentDefaultInterval(visitsSettings), ...rest },
2222
),
23-
[updateVisitsSettings, visitsSettings],
23+
[onChange, visitsSettings],
2424
);
2525

2626
return (
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { SimpleCard } from '@shlinkio/shlink-frontend-kit';
2+
import type { FC } from 'react';
3+
import { useId } from 'react';
4+
import type { QrCodeSettings } from '../../index';
5+
import { defaultQrCodeSettings, useSetting } from '../../index';
6+
7+
export type QrCodeColorSettingsProps = {
8+
onChange: (s: QrCodeSettings) => void;
9+
className?: string;
10+
};
11+
12+
export const QrCodeColorSettings: FC<QrCodeColorSettingsProps> = ({ onChange, className }) => {
13+
const qrCodesSettings = useSetting('qrCodes', defaultQrCodeSettings);
14+
const { color, bgColor } = qrCodesSettings;
15+
const colorId = useId();
16+
const bgColorId = useId();
17+
18+
return (
19+
<SimpleCard title="Colors" className={className} bodyClassName="d-flex flex-column gap-3">
20+
<div className="d-flex flex-column gap-1">
21+
<label htmlFor={colorId}>Default color:</label>
22+
<input
23+
id={colorId}
24+
type="color"
25+
value={color}
26+
onChange={(e) => onChange({ ...qrCodesSettings, color: e.target.value })}
27+
/>
28+
<small className="text-muted">
29+
QR codes will initially use <b data-testid="color">{color}</b> color.
30+
</small>
31+
</div>
32+
<div className="d-flex flex-column gap-1">
33+
<label htmlFor={bgColorId}>Default background color:</label>
34+
<input
35+
id={bgColorId}
36+
type="color"
37+
value={bgColor}
38+
onChange={(e) => onChange({ ...qrCodesSettings, bgColor: e.target.value })}
39+
/>
40+
<small className="text-muted">
41+
QR codes will initially use <b data-testid="bg-color">{bgColor}</b> background color.
42+
</small>
43+
</div>
44+
</SimpleCard>
45+
);
46+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { SimpleCard } from '@shlinkio/shlink-frontend-kit';
2+
import type { FC } from 'react';
3+
import { QrErrorCorrectionDropdown } from '../../../short-urls/helpers/qr-codes/QrErrorCorrectionDropdown';
4+
import { QrFormatDropdown } from '../../../short-urls/helpers/qr-codes/QrFormatDropdown';
5+
import { defaultQrCodeSettings, useSetting } from '../../index';
6+
import type { QrCodeSettings } from '../../types';
7+
8+
export type QrCodeFormatSettingsProps = {
9+
onChange: (s: QrCodeSettings) => void;
10+
};
11+
12+
export const QrCodeFormatSettings: FC<QrCodeFormatSettingsProps> = ({ onChange }) => {
13+
const qrCodesSettings = useSetting('qrCodes', defaultQrCodeSettings);
14+
const { format, errorCorrection } = qrCodesSettings;
15+
16+
return (
17+
<SimpleCard title="Format" bodyClassName="d-flex flex-column gap-3">
18+
<div className="d-flex flex-column gap-1">
19+
<QrFormatDropdown
20+
format={format}
21+
onChange={(format) => onChange({ ...qrCodesSettings, format })}
22+
/>
23+
<small className="text-muted">
24+
When downloading a QR code, it will use <b data-testid="format">{format}</b> format by default.
25+
</small>
26+
</div>
27+
<div className="d-flex flex-column gap-1">
28+
<QrErrorCorrectionDropdown
29+
errorCorrection={errorCorrection}
30+
onChange={(errorCorrection) => onChange({ ...qrCodesSettings, errorCorrection })}
31+
/>
32+
<small className="text-muted">
33+
QR codes will initially have a <b data-testid="error-correction">{errorCorrection}</b> error correction.
34+
</small>
35+
</div>
36+
</SimpleCard>
37+
);
38+
};

0 commit comments

Comments
 (0)