Skip to content

Commit f6979c1

Browse files
authored
Merge pull request #920 from acelaya-forks/visits-redux-type
Simplify visits reducers types with a status discriminator prop
2 parents e8db269 + dc8cc61 commit f6979c1

36 files changed

+461
-376
lines changed

src/visits/DomainVisits.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ const DomainVisitsBase = boundToMercureHub<DomainVisitsProps>(({ ReportExporter:
3838
visitsInfo={domainVisits}
3939
exportCsv={exportCsv}
4040
>
41-
<VisitsHeader visits={domainVisits.visits} title={`"${authority}" visits`} />
41+
<VisitsHeader
42+
visits={domainVisits.status === 'loaded' ? domainVisits.visits : []}
43+
title={`"${authority}" visits`}
44+
/>
4245
</VisitsStats>
4346
);
4447
}, () => [Topics.visits]);

src/visits/NonOrphanVisits.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ const NonOrphanVisitsBase = boundToMercureHub<NonOrphanVisitsProps>(({ ReportExp
3838
exportCsv={exportCsv}
3939
domains={domainsList.domains}
4040
>
41-
<VisitsHeader title="Non-orphan visits" visits={nonOrphanVisits.visits} />
41+
<VisitsHeader
42+
title="Non-orphan visits"
43+
visits={nonOrphanVisits.status === 'loaded' ? nonOrphanVisits.visits : []}
44+
/>
4245
</VisitsStats>
4346
);
4447
}, () => [Topics.visits]);

src/visits/OrphanVisits.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const OrphanVisitsBase = boundToMercureHub<OrphanVisitsProps>(({ ReportExporter:
4747
isOrphanVisits
4848
domains={domainsList.domains}
4949
>
50-
<VisitsHeader title="Orphan visits" visits={orphanVisits.visits} />
50+
<VisitsHeader title="Orphan visits" visits={orphanVisits.status === 'loaded' ? orphanVisits.visits : []} />
5151
</VisitsStats>
5252
);
5353
}, () => [Topics.orphanVisits]);

src/visits/ShortUrlVisitsHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const Date = ({ shortUrl }: { shortUrl?: ShlinkShortUrl }) => {
3131
};
3232

3333
export const ShortUrlVisitsHeader = ({ shortUrl, loading, shortUrlVisits }: ShortUrlVisitsHeaderProps) => {
34-
const { visits } = shortUrlVisits;
34+
const { visits = [] } = shortUrlVisits.status === 'loaded' ? shortUrlVisits : {};
3535
const shortLink = shortUrl?.shortUrl ?? '';
3636
const longLink = shortUrl?.longUrl ?? '';
3737
const title = shortUrl?.title;

src/visits/TagVisitsHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ interface TagVisitsHeaderProps {
99
}
1010

1111
export const TagVisitsHeader = ({ tagVisits, colorGenerator }: TagVisitsHeaderProps) => {
12-
const { visits, tag } = tagVisits;
12+
const { tag, visits = [] } = tagVisits.status === 'loaded' ? tagVisits : {};
1313
const visitsStatsTitle = (
1414
<span className="flex items-center justify-center">
1515
<span className="mr-2">Visits for</span>
16-
<Tag text={tag} colorGenerator={colorGenerator} />
16+
{tag && <Tag text={tag} colorGenerator={colorGenerator} />}
1717
</span>
1818
);
1919

src/visits/VisitsStats.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ export const VisitsStats: FC<VisitsStatsProps> = (props) => {
9191
isOrphanVisits = false,
9292
domains,
9393
} = props;
94-
const { visits, prevVisits, loading, errorData, fallbackInterval } = visitsInfo;
94+
const { status } = visitsInfo;
95+
const loading = status === 'loading';
96+
const { visits, prevVisits } = status === 'loaded' ? visitsInfo : { visits: [] };
9597
const [{ dateRange, visitsFilter, loadPrevInterval, domain }, updateQuery] = useVisitsQuery();
9698
const visitsSettings = useSetting('visits');
9799
const [activeInterval, setActiveInterval] = useState<DateInterval>();
@@ -107,8 +109,9 @@ export const VisitsStats: FC<VisitsStatsProps> = (props) => {
107109
},
108110
[updateQuery],
109111
);
112+
const { fallbackInterval } = status === 'fallback' ? visitsInfo : {};
110113
const [currentFallbackInterval, setCurrentFallbackInterval] = useState<DateInterval>(
111-
fallbackInterval ?? visitsSettings?.defaultInterval ?? 'last30Days',
114+
status === 'fallback' ? visitsInfo.fallbackInterval : (visitsSettings?.defaultInterval ?? 'last30Days'),
112115
);
113116
const [highlightedVisits, setHighlightedVisits] = useState<NormalizedVisit[]>([]);
114117
const [highlightedLabel, setHighlightedLabel] = useState<string | undefined>();
@@ -183,7 +186,7 @@ export const VisitsStats: FC<VisitsStatsProps> = (props) => {
183186
if (fallbackInterval && currentFallbackInterval === (visitsSettings?.defaultInterval ?? 'last30Days')) {
184187
setCurrentFallbackInterval(fallbackInterval);
185188
}
186-
}, [currentFallbackInterval, fallbackInterval, visitsSettings?.defaultInterval]);
189+
}, [currentFallbackInterval, status, fallbackInterval, visitsSettings?.defaultInterval]);
187190

188191
return (
189192
<div className="flex flex-col gap-y-4">
@@ -240,8 +243,8 @@ export const VisitsStats: FC<VisitsStatsProps> = (props) => {
240243
</section>
241244

242245
<section className="flex flex-col gap-4">
243-
<VisitsLoadingFeedback info={visitsInfo} />
244-
{!loading && !errorData && (
246+
{status !== 'loaded' && status !== 'fallback' && <VisitsLoadingFeedback info={visitsInfo} />}
247+
{!loading && status !== 'error' && (
245248
<>
246249
<NavPills fill className="sticky top-(--header-height) z-2">
247250
{Object.values(sections).map(({ title, icon, subPath, shouldRender }: VisitsNavLinkOptions, index) => (

src/visits/helpers/VisitsLoadingFeedback.tsx

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,24 @@ const ProgressBar: FC<ProgressBarProps> = ({ className, value, ...rest }) => {
2929
};
3030

3131
export const VisitsLoadingFeedback: FC<VisitsLoadingFeedbackProps> = ({ info }) => {
32-
const { loading, errorData, progress } = info;
33-
return (
34-
<>
35-
{loading && progress === null && <Message loading />}
36-
{loading && progress !== null && (
37-
<Message loading>
38-
This is going to take a while... :S
39-
<ProgressBar value={progress} className="mt-4" />
40-
</Message>
41-
)}
42-
{errorData && (
43-
<Result variant="error">
44-
<ShlinkApiError errorData={errorData} fallbackMessage="An error occurred while loading visits :(" />
45-
</Result>
46-
)}
47-
</>
48-
);
32+
const { status } = info;
33+
34+
if (status === 'error') {
35+
return (
36+
<Result variant="error">
37+
<ShlinkApiError errorData={info.error} fallbackMessage="An error occurred while loading visits :(" />
38+
</Result>
39+
);
40+
}
41+
42+
if (status === 'loading') {
43+
return info.progress === null ? <Message loading /> : (
44+
<Message loading>
45+
This is going to take a while... :S
46+
<ProgressBar value={info.progress} className="mt-4" />
47+
</Message>
48+
);
49+
}
50+
51+
return null;
4952
};

src/visits/reducers/common/createVisitsReducer.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,50 @@ import type { ActionReducerMapBuilder } from '@reduxjs/toolkit';
22
import { createSlice } from '@reduxjs/toolkit';
33
import { parseApiError } from '../../../api-contract/utils';
44
import type { CreateVisit } from '../../types';
5-
import type { VisitsInfo } from '../types';
5+
import type { LoadVisits, VisitsInfo } from '../types';
66
import { createNewVisits } from '../visitCreation';
77
import type { createVisitsAsyncThunk } from './createVisitsAsyncThunk';
88

9-
interface VisitsReducerOptions<State extends VisitsInfo, AT extends ReturnType<typeof createVisitsAsyncThunk>> {
9+
interface VisitsReducerOptions<State, T extends LoadVisits> {
1010
name: string;
11-
asyncThunk: AT;
12-
initialState: State;
13-
filterCreatedVisits: (state: State, createdVisits: CreateVisit[]) => CreateVisit[];
14-
extraReducers?: (builder: ActionReducerMapBuilder<State>) => void;
11+
asyncThunk: ReturnType<typeof createVisitsAsyncThunk<T>>;
12+
initialState: VisitsInfo<State>;
13+
filterCreatedVisits: (state: VisitsInfo<State>, createdVisits: CreateVisit[]) => CreateVisit[];
14+
extraReducers?: (builder: ActionReducerMapBuilder<VisitsInfo<State>>) => void;
1515
}
1616

17-
export const createVisitsReducer = <State extends VisitsInfo, AT extends ReturnType<typeof createVisitsAsyncThunk>>(
18-
{ name, asyncThunk, initialState, filterCreatedVisits, extraReducers }: VisitsReducerOptions<State, AT>,
17+
export const createVisitsReducer = <State, T extends LoadVisits>(
18+
{ name, asyncThunk, initialState, filterCreatedVisits, extraReducers }: VisitsReducerOptions<State, T>,
1919
) => {
2020
const { pending, rejected, fulfilled, progressChanged, fallbackToInterval } = asyncThunk;
2121
const { reducer, actions } = createSlice({
2222
name,
2323
initialState,
2424
reducers: {
25-
cancelGetVisits: (state) => ({ ...state, cancelLoad: true }),
25+
cancelGetVisits: () => ({ status: 'canceled' as const }),
2626
},
2727
extraReducers: (builder) => {
28-
builder.addCase(pending, () => ({ ...initialState, loading: true }));
29-
builder.addCase(rejected, (_, { error }) => (
30-
{ ...initialState, errorData: parseApiError(error) ?? null }
31-
));
32-
builder.addCase(fulfilled, (state, { payload }) => (
33-
// Unpack the whole payload, as it could have different props depending on the concrete reducer
34-
{ ...state, ...payload, loading: false, progress: null, errorData: null }
28+
builder.addCase(pending, () => ({ status: 'loading', progress: null }));
29+
builder.addCase(progressChanged, (state, { payload: progress }) => (
30+
// Update progress only if already loading
31+
state.status !== 'loading' ? state : { status: 'loading', progress }
3532
));
3633

37-
builder.addCase(progressChanged, (state, { payload: progress }) => ({ ...state, progress }));
38-
builder.addCase(fallbackToInterval, (state, { payload: fallbackInterval }) => (
39-
{ ...state, fallbackInterval }
34+
builder.addCase(rejected, (_, { error }) => ({ status: 'error', error: parseApiError(error) }));
35+
36+
// Unpack the whole payload, as it could have different props depending on the concrete reducer
37+
builder.addCase(fulfilled, (state, { payload }) => ({ ...state, ...payload, status: 'loaded' }));
38+
39+
builder.addCase(fallbackToInterval, (_, { payload: fallbackInterval }) => (
40+
{ status: 'fallback', fallbackInterval }
4041
));
4142

4243
builder.addCase(createNewVisits, (state, { payload }) => {
44+
if (state.status !== 'loaded') {
45+
return state;
46+
}
47+
4348
const { visits } = state;
44-
// @ts-expect-error TODO Fix type inference
4549
const newVisits = filterCreatedVisits(state, payload.createdVisits).map(({ visit }) => visit);
4650

4751
return !newVisits.length ? state : { ...state, visits: [...newVisits, ...visits] };

src/visits/reducers/domainVisits.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,16 @@ import type { LoadVisits, VisitsInfo } from './types';
99

1010
const REDUCER_PREFIX = 'shlink/domainVisits';
1111

12-
interface WithDomain {
12+
type WithDomain = {
1313
domain: string;
14-
}
14+
};
1515

16-
export interface DomainVisits extends VisitsInfo, WithDomain {}
16+
export type DomainVisits = VisitsInfo<WithDomain>;
1717

18-
export interface LoadDomainVisits extends LoadVisits, WithDomain {}
18+
export type LoadDomainVisits = LoadVisits & WithDomain;
1919

2020
const initialState: DomainVisits = {
21-
visits: [],
22-
domain: '',
23-
loading: false,
24-
cancelLoad: false,
25-
errorData: null,
26-
progress: null,
21+
status: 'idle',
2722
};
2823

2924
export const getDomainVisitsThunk = createVisitsAsyncThunk({
@@ -37,19 +32,16 @@ export const getDomainVisitsThunk = createVisitsAsyncThunk({
3732

3833
return { visitsLoader, lastVisitLoader };
3934
},
40-
shouldCancel: (getState) => getState().domainVisits.cancelLoad,
35+
shouldCancel: (getState) => getState().domainVisits.status === 'canceled',
4136
});
4237

4338
export const { reducer: domainVisitsReducer, cancelGetVisits: cancelGetDomainVisits } = createVisitsReducer({
4439
name: REDUCER_PREFIX,
45-
initialState,
46-
// @ts-expect-error TODO Fix type inference
40+
initialState: initialState as DomainVisits,
4741
asyncThunk: getDomainVisitsThunk,
48-
filterCreatedVisits: ({ domain, params }, createdVisits) => filterCreatedVisitsByDomain(
49-
createdVisits,
50-
domain,
51-
params?.dateRange,
52-
),
42+
filterCreatedVisits: (state, createdVisits) => state.status !== 'loaded'
43+
? createdVisits
44+
: filterCreatedVisitsByDomain(createdVisits, state.domain, state.params?.dateRange),
5345
});
5446

5547
export const useDomainVisits = () => {

src/visits/reducers/nonOrphanVisits.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ import type { LoadWithDomainVisits, VisitsInfo } from './types';
1010
const REDUCER_PREFIX = 'shlink/orphanVisits';
1111

1212
const initialState: VisitsInfo = {
13-
visits: [],
14-
loading: false,
15-
cancelLoad: false,
16-
errorData: null,
17-
progress: null,
13+
status: 'idle',
1814
};
1915

2016
export const getNonOrphanVisitsThunk = createVisitsAsyncThunk({
@@ -28,16 +24,19 @@ export const getNonOrphanVisitsThunk = createVisitsAsyncThunk({
2824

2925
return { visitsLoader, lastVisitLoader };
3026
},
31-
shouldCancel: (getState) => getState().orphanVisits.cancelLoad,
27+
shouldCancel: (getState) => getState().nonOrphanVisits.status === 'canceled',
3228
});
3329

3430
export const { reducer: nonOrphanVisitsReducer, cancelGetVisits: cancelGetNonOrphanVisits } = createVisitsReducer({
3531
name: REDUCER_PREFIX,
36-
initialState,
37-
// @ts-expect-error TODO Fix type inference
32+
initialState: initialState as VisitsInfo,
3833
asyncThunk: getNonOrphanVisitsThunk,
39-
filterCreatedVisits: ({ params }, createdVisits) => {
40-
const { startDate, endDate } = params?.dateRange ?? {};
34+
filterCreatedVisits: (state, createdVisits) => {
35+
if (state.status !== 'loaded') {
36+
return createdVisits;
37+
}
38+
39+
const { startDate, endDate } = state.params?.dateRange ?? {};
4140
return createdVisits.filter(({ visit }) => isBetween(visit.date, startDate, endDate));
4241
},
4342
});

0 commit comments

Comments
 (0)