Skip to content

Commit 7f7855b

Browse files
committed
feat: implement new filters
1 parent e874eec commit 7f7855b

21 files changed

Lines changed: 1202 additions & 110 deletions

File tree

packages/app-builder/src/hooks/useDateRangeSearchParams.ts

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useSearchParams } from '@remix-run/react';
22
import { subDays, subMonths } from 'date-fns';
3-
import { useCallback, useMemo } from 'react';
3+
import { useCallback, useEffect, useState } from 'react';
44

55
type StaticDateRange = { type: 'static'; startDate: string; endDate: string };
66
type DynamicDateRange = { type: 'dynamic'; fromNow: string };
@@ -17,39 +17,58 @@ function getDefaultRange(): { start: string; end: string } {
1717
return { start: toIso(start), end: toIso(end) };
1818
}
1919

20+
function parseQ(qValue: string | null): { range: IsoRange; compareRange: IsoRange | null } {
21+
if (qValue) {
22+
try {
23+
const obj = JSON.parse(atob(qValue)) as {
24+
range?: { start?: string; end?: string } | null;
25+
compareRange?: { start?: string; end?: string } | null;
26+
};
27+
if (obj?.range?.start && obj?.range?.end) {
28+
return {
29+
range: { start: obj.range.start, end: obj.range.end },
30+
compareRange:
31+
obj.compareRange?.start && obj.compareRange?.end
32+
? { start: obj.compareRange.start, end: obj.compareRange.end }
33+
: null,
34+
};
35+
}
36+
} catch {
37+
// ignore malformed q
38+
}
39+
}
40+
const defaults = getDefaultRange();
41+
return { range: { start: defaults.start, end: defaults.end }, compareRange: null };
42+
}
43+
2044
export function useDateRangeSearchParams() {
2145
const [searchParams, setSearchParams] = useSearchParams();
22-
const parseQ = useCallback(
23-
(qValue: string | null): { range: IsoRange; compareRange: IsoRange | null } => {
24-
if (qValue) {
25-
try {
26-
const obj = JSON.parse(atob(qValue)) as {
27-
range?: { start?: string; end?: string } | null;
28-
compareRange?: { start?: string; end?: string } | null;
29-
};
30-
if (obj?.range?.start && obj?.range?.end) {
31-
return {
32-
range: { start: obj.range.start, end: obj.range.end },
33-
compareRange:
34-
obj.compareRange?.start && obj.compareRange?.end
35-
? { start: obj.compareRange.start, end: obj.compareRange.end }
36-
: null,
37-
};
38-
}
39-
} catch {
40-
// ignore malformed q
41-
}
46+
const [dateRangeState, setDateRangeState] = useState(() => {
47+
// Initialize state with parsed values
48+
const qValue = searchParams.get('q');
49+
return parseQ(qValue);
50+
});
51+
52+
// Parse and update state only when the parsed values actually change
53+
useEffect(() => {
54+
const qValue = searchParams.get('q');
55+
const parsed = parseQ(qValue);
56+
57+
// Only update state if values actually changed
58+
setDateRangeState((current) => {
59+
if (
60+
current.range.start !== parsed.range.start ||
61+
current.range.end !== parsed.range.end ||
62+
current.compareRange?.start !== parsed.compareRange?.start ||
63+
current.compareRange?.end !== parsed.compareRange?.end
64+
) {
65+
return parsed;
4266
}
43-
const defaults = getDefaultRange();
44-
return { range: { start: defaults.start, end: defaults.end }, compareRange: null };
45-
},
46-
[],
47-
);
67+
return current;
68+
});
69+
}, [searchParams, parseQ]);
4870

49-
const { range, compareRange } = useMemo(
50-
() => parseQ(searchParams.get('q')),
51-
[parseQ, searchParams],
52-
);
71+
const { range, compareRange } = dateRangeState;
5372

5473
const computeDynamicRange = useCallback((fromNow: string): IsoRange => {
5574
const now = new Date();
@@ -75,7 +94,7 @@ export function useDateRangeSearchParams() {
7594
params.delete('end');
7695
return params;
7796
},
78-
[parseQ],
97+
[],
7998
);
8099

81100
const setDateRangeFilter = useCallback(
@@ -95,7 +114,7 @@ export function useDateRangeSearchParams() {
95114
const dynamicRange = computeDynamicRange(dateRange.fromNow);
96115
return writeQ(prev, { range: dynamicRange, compareRange: undefined });
97116
},
98-
{ replace: true },
117+
{ replace: false },
99118
);
100119
},
101120
[computeDynamicRange, setSearchParams, writeQ],
@@ -116,7 +135,7 @@ export function useDateRangeSearchParams() {
116135

117136
return writeQ(prev, { compareRange: nextCompare });
118137
},
119-
{ replace: true },
138+
{ replace: false },
120139
);
121140
},
122141
[computeDynamicRange, setSearchParams, writeQ],
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { type AvailableFiltersRequestDto, type AvailableFiltersResponseDto } from 'marble-api';
2+
import * as R from 'remeda';
3+
4+
export type AvailableFilterType = 'string' | 'number' | 'boolean';
5+
export type FilterSource = 'triggerObject';
6+
7+
export type AvailableFiltersRequest = {
8+
scenarioId: string;
9+
start: string;
10+
end: string;
11+
};
12+
13+
export type AvailableFiltersResponse = {
14+
name: string;
15+
type: AvailableFilterType;
16+
source: FilterSource;
17+
}[];
18+
19+
export const adaptAvailableFiltersResponse = (
20+
response: AvailableFiltersResponseDto,
21+
): AvailableFiltersResponse => {
22+
return response.map((item) => ({
23+
name: item.name,
24+
type: item.type as AvailableFilterType,
25+
source: R.toCamelCase(item.source) as FilterSource,
26+
}));
27+
};
28+
29+
export const transformAvailableFiltersRequest = (
30+
request: AvailableFiltersRequest,
31+
): AvailableFiltersRequestDto => {
32+
return {
33+
scenario_id: request.scenarioId,
34+
start: request.start,
35+
end: request.end,
36+
};
37+
};

packages/app-builder/src/models/analytics/decisions-outcomes-perday.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ export interface DecisionOutcomesPerPeriod {
6969

7070
export const triggerFilter = z.object({
7171
field: z.uuidv4(),
72-
op: z.enum(['=', '!=', '>', '>=', '<', '<=']),
73-
values: z.array(z.string()),
72+
op: z.enum(['=', '!=', '>', '>=', '<', '<=', 'in']),
73+
values: z.array(z.union([z.string(), z.number(), z.boolean()])),
7474
});
7575

7676
export const decisionOutcomesPerDayQuery = z.object({

packages/app-builder/src/models/analytics/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './available-filters';
12
export * from './decisions-outcomes-perday';
23
export * as legacyAnalytics from './legacy-analytics';
34

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useAgnosticNavigation } from '@app-builder/contexts/AgnosticNavigationContext';
2+
import { AvailableFiltersResponse } from '@app-builder/models/analytics/available-filters';
3+
import { getRoute } from '@app-builder/utils/routes';
4+
import { useQuery } from '@tanstack/react-query';
5+
6+
export const useGetAvailableFilters = ({
7+
scenarioId,
8+
dateRange,
9+
}: {
10+
scenarioId: string;
11+
dateRange: { start: string; end: string };
12+
}) => {
13+
const navigate = useAgnosticNavigation();
14+
15+
const endpoint = getRoute('/ressources/analytics/available_filters/:scenarioId', {
16+
scenarioId,
17+
});
18+
return useQuery({
19+
queryKey: ['analytics', 'available-filters', scenarioId, dateRange],
20+
queryFn: async () => {
21+
const response = await fetch(endpoint, {
22+
method: 'POST',
23+
headers: {
24+
'Content-Type': 'application/json',
25+
},
26+
body: JSON.stringify({ scenarioId, start: dateRange.start, end: dateRange.end }),
27+
});
28+
const result = await response.json();
29+
if (result.redirectTo) {
30+
navigate(result.redirectTo);
31+
return;
32+
}
33+
34+
return result as Promise<AvailableFiltersResponse>;
35+
},
36+
});
37+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useAgnosticNavigation } from '@app-builder/contexts/AgnosticNavigationContext';
2+
import {
3+
DecisionOutcomesPerDayQuery,
4+
DecisionOutcomesPerPeriod,
5+
} from '@app-builder/models/analytics';
6+
import { getRoute } from '@app-builder/utils/routes';
7+
import { keepPreviousData, useQuery } from '@tanstack/react-query';
8+
9+
export const useGetDecisionsOutcomesPerDay = ({
10+
scenarioId,
11+
scenarioVersion,
12+
dateRange,
13+
compareDateRange = undefined,
14+
trigger = [],
15+
}: DecisionOutcomesPerDayQuery) => {
16+
const navigate = useAgnosticNavigation();
17+
const endpoint = getRoute('/ressources/analytics/decisions_outcomes_per_day/:scenarioId', {
18+
scenarioId,
19+
});
20+
return useQuery({
21+
queryKey: [
22+
'analytics',
23+
'decisions',
24+
scenarioId,
25+
scenarioVersion,
26+
dateRange,
27+
compareDateRange,
28+
trigger,
29+
],
30+
queryFn: async () => {
31+
const response = await fetch(endpoint, {
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
},
36+
body: JSON.stringify({
37+
scenarioId,
38+
scenarioVersion,
39+
dateRange: {
40+
start: dateRange.start,
41+
end: dateRange.end,
42+
},
43+
compareDateRange: compareDateRange
44+
? {
45+
start: compareDateRange.start,
46+
end: compareDateRange.end,
47+
}
48+
: undefined,
49+
trigger,
50+
}),
51+
});
52+
53+
const result = await response.json();
54+
55+
if (result.redirectTo) {
56+
navigate(result.redirectTo);
57+
return;
58+
}
59+
60+
return result as Promise<DecisionOutcomesPerPeriod>;
61+
},
62+
placeholderData: keepPreviousData,
63+
});
64+
};

packages/app-builder/src/repositories/AnalyticsRepository.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import {
99
mergeDateRanges,
1010
transformDecisionOutcomesPerDayQuery,
1111
} from '@app-builder/models/analytics';
12+
import {
13+
type AvailableFiltersRequest,
14+
type AvailableFiltersResponse,
15+
adaptAvailableFiltersResponse,
16+
transformAvailableFiltersRequest,
17+
} from '@app-builder/models/analytics/available-filters';
1218

1319
import { compareAsc, compareDesc, differenceInDays } from 'date-fns';
1420

@@ -17,6 +23,7 @@ export interface AnalyticsRepository {
1723
getDecisionOutcomesPerDay(
1824
args: DecisionOutcomesPerDayQuery,
1925
): Promise<DecisionOutcomesPerPeriod | null>;
26+
getAvailableFilters(args: AvailableFiltersRequest): Promise<AvailableFiltersResponse>;
2027
}
2128

2229
export function makeGetAnalyticsRepository() {
@@ -85,5 +92,13 @@ export function makeGetAnalyticsRepository() {
8592
return null;
8693
}
8794
},
95+
96+
getAvailableFilters: async (
97+
args: AvailableFiltersRequest,
98+
): Promise<AvailableFiltersResponse> => {
99+
return client
100+
.getAvailableFilters(transformAvailableFiltersRequest(args))
101+
.then((response) => adaptAvailableFiltersResponse(response));
102+
},
88103
});
89104
}

0 commit comments

Comments
 (0)