Skip to content

Commit

Permalink
Merge pull request #357 from acelaya-forks/feature/settings-components
Browse files Browse the repository at this point in the history
Feature/settings components
  • Loading branch information
acelaya authored May 20, 2024
2 parents 77d6fad + 17f1db3 commit ef312c8
Show file tree
Hide file tree
Showing 58 changed files with 1,195 additions and 135 deletions.
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,24 @@ All notable changes to this project will be documented in this file.

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).

## [0.6.2] - 2024-04.17
## [0.7.0] - 2024-05-20
### Added
* Add new `@shlinkio/shlink-web-client/settings` entry point, to expose a component rendering the settings form and all settings-related types.

### Changed
* Update dependencies

### Deprecated
* *Nothing*

### Removed
* *Nothing*

### Fixed
* *Nothing*


## [0.6.2] - 2024-04-17
### Added
* *Nothing*

Expand Down
2 changes: 1 addition & 1 deletion dev/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/browser';
import type { FC } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import type { Settings } from '../src';
import { ShlinkWebComponent } from '../src';
import type { Settings } from '../src/settings';
import type { SemVer } from '../src/utils/helpers/version';
import { ServerInfoForm } from './server-info/ServerInfoForm';
import type { ServerInfo } from './server-info/useServerInfo';
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: '3'
services:
shlink_web_component:
container_name: shlink_web_component
image: node:20.5-alpine
image: node:22.2-alpine
command: /bin/sh -c "cd /shlink-web-component && npm i && npm run dev"
volumes:
- ./:/shlink-web-component
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"repository": "https://github.com/shlinkio/shlink-web-component",
"license": "MIT",
"type": "module",
"main": "./dist/index.umd.cjs",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
Expand All @@ -19,6 +19,11 @@
"require": "./dist/api-contract.cjs",
"types": "./dist/api-contract.d.ts"
},
"./settings": {
"import": "./dist/settings.js",
"require": "./dist/settings.cjs",
"types": "./dist/settings.d.ts"
},
"./package.json": "./package.json"
},
"files": [
Expand Down
8 changes: 4 additions & 4 deletions src/ShlinkWebComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { BrowserRouter, useInRouterContext } from 'react-router-dom';
import type { ShlinkApiClient } from './api-contract';
import type { Settings } from './settings';
import { SettingsProvider } from './settings';
import { FeaturesProvider, useFeatures } from './utils/features';
import type { SemVerOrLatest } from './utils/helpers/version';
import { RoutesPrefixProvider } from './utils/routesPrefix';
import type { TagColorsStorage } from './utils/services/TagColorsStorage';
import type { Settings } from './utils/settings';
import { SettingsProvider } from './utils/settings';

type ShlinkWebComponentProps = {
serverVersion: SemVerOrLatest; // FIXME Consider making this optional and trying to resolve it if not set
apiClient: ShlinkApiClient;
tagColorsStorage?: TagColorsStorage;
routesPrefix?: string;
settings?: Settings;
settings?: Exclude<Settings, 'ui'>;
createNotFound?: (nonPrefixedHomePath: string) => ReactNode;
};

Expand Down Expand Up @@ -61,7 +61,7 @@ export const createShlinkWebComponent = (

return !theStore ? <></> : (
<ReduxStoreProvider store={theStore}>
<SettingsProvider value={settings}>
<SettingsProvider value={settings ?? {}}>
<FeaturesProvider value={features}>
<RoutesPrefixProvider value={routesPrefix}>
<RouterOrFragment>
Expand Down
9 changes: 0 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,4 @@ export const ShlinkWebComponent = createShlinkWebComponent(bottle);

export type ShlinkWebComponentType = typeof ShlinkWebComponent;

export type {
RealTimeUpdatesSettings,
ShortUrlCreationSettings,
ShortUrlsListSettings,
VisitsSettings,
TagsSettings,
Settings,
} from './utils/settings';

export type { TagColorsStorage } from './utils/services/TagColorsStorage';
2 changes: 1 addition & 1 deletion src/mercure/reducers/mercureInfo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createSlice } from '@reduxjs/toolkit';
import type { ShlinkApiClient, ShlinkMercureInfo } from '../../api-contract';
import type { Settings } from '../../settings';
import { createAsyncThunk } from '../../utils/redux';
import type { Settings } from '../../utils/settings';

const REDUCER_PREFIX = 'shlink/mercure';

Expand Down
2 changes: 1 addition & 1 deletion src/overview/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { componentFactory, useDependencies } from '../container/utils';
import type { MercureBoundProps } from '../mercure/helpers/boundToMercureHub';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics';
import { useSetting } from '../settings';
import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
import type { ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
import { ITEMS_IN_OVERVIEW_PAGE } from '../short-urls/reducers/shortUrlsList';
import type { ShortUrlsTableType } from '../short-urls/ShortUrlsTable';
import type { TagsList } from '../tags/reducers/tagsList';
import { prettify } from '../utils/helpers/numbers';
import { useRoutesPrefix } from '../utils/routesPrefix';
import { useSetting } from '../utils/settings';
import type { VisitsOverview } from '../visits/reducers/visitsOverview';
import { HighlightCard } from './helpers/HighlightCard';
import { VisitsHighlightCard } from './helpers/VisitsHighlightCard';
Expand Down
46 changes: 46 additions & 0 deletions src/settings/components/DateIntervalSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { DropdownBtn } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { DropdownItem } from 'reactstrap';
import type { VisitsSettings } from '..';

export type DateInterval = VisitsSettings['defaultInterval'];

export interface DateIntervalSelectorProps {
active?: DateInterval;
allText: string;
onChange: (interval: DateInterval) => void;
}

export const INTERVAL_TO_STRING_MAP: Record<Exclude<DateInterval, 'all'>, string> = {
today: 'Today',
yesterday: 'Yesterday',
last7Days: 'Last 7 days',
last30Days: 'Last 30 days',
last90Days: 'Last 90 days',
last180Days: 'Last 180 days',
last365Days: 'Last 365 days',
};

const intervalToString = (interval: DateInterval | undefined, fallback: string): string => {
if (!interval || interval === 'all') {
return fallback;
}

return INTERVAL_TO_STRING_MAP[interval];
};

export const DateIntervalSelector: FC<DateIntervalSelectorProps> = ({ onChange, active, allText }) => (
<DropdownBtn text={intervalToString(active, allText)}>
<DropdownItem active={active === 'all'} onClick={() => onChange('all')}>
{allText}
</DropdownItem>
<DropdownItem divider />
{Object.entries(INTERVAL_TO_STRING_MAP).map(
([interval, name]) => (
<DropdownItem key={interval} active={active === interval} onClick={() => onChange(interval as DateInterval)}>
{name}
</DropdownItem>
),
)}
</DropdownBtn>
);
5 changes: 5 additions & 0 deletions src/settings/components/FormText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { FC, PropsWithChildren } from 'react';

export const FormText: FC<PropsWithChildren> = ({ children }) => (
<small className="form-text text-muted d-block">{children}</small>
);
57 changes: 57 additions & 0 deletions src/settings/components/RealTimeUpdatesSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { LabeledFormGroup, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit';
import { clsx } from 'clsx';
import { useId } from 'react';
import { FormGroup, Input } from 'reactstrap';
import { useSetting } from '..';
import { FormText } from './FormText';

export type RealTimeUpdatesProps = {
toggleRealTimeUpdates: (enabled: boolean) => void;
setRealTimeUpdatesInterval: (interval: number) => void;
};

export const RealTimeUpdatesSettings = (
{ toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
) => {
const { enabled, interval } = useSetting('realTimeUpdates', { enabled: true });
const inputId = useId();

return (
<SimpleCard title="Real-time updates" className="h-100">
<FormGroup>
<ToggleSwitch checked={enabled} onChange={toggleRealTimeUpdates}>
Enable or disable real-time updates.
<FormText>
Real-time updates are currently being <b>{enabled ? 'processed' : 'ignored'}</b>.
</FormText>
</ToggleSwitch>
</FormGroup>
<LabeledFormGroup
noMargin
label="Real-time updates frequency (in minutes):"
labelClassName={clsx('form-label', { 'text-muted': !enabled })}
id={inputId}
>
<Input
type="number"
min={0}
placeholder="Immediate"
disabled={!enabled}
value={`${interval ?? ''}`}
id={inputId}
onChange={({ target }) => setRealTimeUpdatesInterval(Number(target.value))}
/>
{enabled && (
<FormText>
{interval ? (
<span>
Updates will be reflected in the UI
every <b>{interval}</b> minute{interval > 1 && 's'}.
</span>
) : 'Updates will be reflected in the UI as soon as they happen.'}
</FormText>
)}
</LabeledFormGroup>
</SimpleCard>
);
};
100 changes: 100 additions & 0 deletions src/settings/components/ShlinkWebSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { mergeDeepRight } from '@shlinkio/data-manipulation';
import { NavPillItem, NavPills } from '@shlinkio/shlink-frontend-kit';
import type { FC, ReactNode } from 'react';
import { useCallback } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import type { DeepPartial } from '../../utils/types';
import { SettingsProvider } from '..';
import type { RealTimeUpdatesSettings, Settings, ShortUrlsListSettings } from '../types';
import { RealTimeUpdatesSettings as RealTimeUpdates } from './RealTimeUpdatesSettings';
import { ShortUrlCreationSettings as ShortUrlCreation } from './ShortUrlCreationSettings';
import { ShortUrlsListSettings as ShortUrlsList } from './ShortUrlsListSettings';
import { TagsSettings as Tags } from './TagsSettings';
import { UserInterfaceSettings } from './UserInterfaceSettings';
import { VisitsSettings as Visits } from './VisitsSettings';

export type ShlinkWebSettingsProps = {
settings: Settings;
defaultShortUrlsListOrdering: NonNullable<ShortUrlsListSettings['defaultOrdering']>;
updateSettings: (settings: Settings) => void;
};

const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => (
<>
{items.map((child, index) => <div key={index} className="mb-3">{child}</div>)}
</>
);

export const ShlinkWebSettings: FC<ShlinkWebSettingsProps> = (
{ settings, updateSettings, defaultShortUrlsListOrdering },
) => {
const updatePartialSettings = useCallback(
(partialSettings: DeepPartial<Settings>) => updateSettings(mergeDeepRight(settings, partialSettings)),
[settings, updateSettings],
);
const toggleRealTimeUpdates = useCallback(
(enabled: boolean) => updatePartialSettings({ realTimeUpdates: { enabled } }),
[updatePartialSettings],
);
const setRealTimeUpdatesInterval = useCallback(
(interval: number) => updatePartialSettings({ realTimeUpdates: { interval } as RealTimeUpdatesSettings }),
[updatePartialSettings],
);
const updateSettingsProp = useCallback(
<Prop extends keyof Settings>(prop: Prop, value: Settings[Prop]) => updatePartialSettings({ [prop]: value }),
[updatePartialSettings],
);

return (
<SettingsProvider value={settings}>
<NavPills className="mb-3">
<NavPillItem to="general">General</NavPillItem>
<NavPillItem to="short-urls">Short URLs</NavPillItem>
<NavPillItem to="other-items">Other items</NavPillItem>
</NavPills>

<Routes>
<Route
path="general"
element={(
<SettingsSections
items={[
<UserInterfaceSettings updateUiSettings={(v) => updateSettingsProp('ui', v)} />,
<RealTimeUpdates
toggleRealTimeUpdates={toggleRealTimeUpdates}
setRealTimeUpdatesInterval={setRealTimeUpdatesInterval}
/>,
]}
/>
)}
/>
<Route
path="short-urls"
element={(
<SettingsSections
items={[
<ShortUrlCreation updateShortUrlCreationSettings={(v) => updateSettingsProp('shortUrlCreation', v)} />,
<ShortUrlsList
defaultOrdering={defaultShortUrlsListOrdering}
updateShortUrlsListSettings={(v) => updateSettingsProp('shortUrlsList', v)}
/>,
]}
/>
)}
/>
<Route
path="other-items"
element={(
<SettingsSections
items={[
<Tags updateTagsSettings={(v) => updateSettingsProp('tags', v)} />,
<Visits updateVisitsSettings={(v) => updateSettingsProp('visits', v)} />,
]}
/>
)}
/>
<Route path="*" element={<Navigate replace to="general" />} />
</Routes>
</SettingsProvider>
);
};
Loading

0 comments on commit ef312c8

Please sign in to comment.