Skip to content

Commit 15a17f6

Browse files
authored
Merge pull request #701 from AppQuality/prototype-ux-filters
Prototype ux filters
2 parents f032061 + ef34d93 commit 15a17f6

File tree

26 files changed

+1421
-347
lines changed

26 files changed

+1421
-347
lines changed

src/app/store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import filterReducer from '../features/campaignsFilter/campaignsFilterSlice';
88
import expressReducer from '../features/express/expressSlice';
99
import { strapiSlice } from '../features/backoffice/strapi';
1010
import bugsPageReducer from '../features/bugsPage/bugsPageSlice';
11+
import uxFilterReducer from '../features/uxFilters';
1112

1213
export const store = configureStore({
1314
reducer: {
@@ -17,6 +18,7 @@ export const store = configureStore({
1718
filters: filterReducer,
1819
express: expressReducer,
1920
bugsPage: bugsPageReducer,
21+
uxFilters: uxFilterReducer,
2022
[unguessApiSlice.reducerPath]: unguessApiSlice.reducer,
2123
[strapiSlice.reducerPath]: strapiSlice.reducer,
2224
},

src/common/components/CustomStatusDrawer/formModel.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as Yup from 'yup';
22
import { BugCustomStatus } from 'src/features/api';
3+
import { t } from 'i18next';
34

45
export interface CustomStatusFormProps {
56
custom_statuses: BugCustomStatus[];
@@ -10,8 +11,8 @@ export const validationSchema = Yup.object().shape({
1011
Yup.object().shape({
1112
id: Yup.number(),
1213
name: Yup.string()
13-
.max(17, '__BUGS_PAGE_CUSTOM_STATUS_DRAWER_CUSTOM_STATUS_MAX')
14-
.required('__BUGS_PAGE_CUSTOM_STATUS_DRAWER_CUSTOM_STATUS_REQUIRED'),
14+
.max(17, t('__BUGS_PAGE_CUSTOM_STATUS_DRAWER_CUSTOM_STATUS_MAX'))
15+
.required(t('__BUGS_PAGE_CUSTOM_STATUS_DRAWER_CUSTOM_STATUS_REQUIRED')),
1516
color: Yup.string(),
1617
phase: Yup.object().shape({
1718
id: Yup.number().required(),

src/common/schema.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ export interface paths {
7777
};
7878
};
7979
};
80+
'/campaigns/{cid}/clusters': {
81+
get: operations['get-campaigns-cid-clusters'];
82+
parameters: {
83+
path: {
84+
cid: string;
85+
};
86+
};
87+
};
8088
'/campaigns/{cid}/custom_statuses': {
8189
get: operations['get-campaigns-cid-custom-statuses'];
8290
delete: operations['delete-campaigns-cid-custom_statuses'];
@@ -521,6 +529,11 @@ export interface components {
521529
CampaignWithOutput: components['schemas']['Campaign'] & {
522530
outputs?: components['schemas']['Output'][];
523531
};
532+
/** Cluster */
533+
Cluster: {
534+
id?: number;
535+
name?: string;
536+
};
524537
/**
525538
* Coin
526539
* @description A coin package is a set of coins (free or paid).
@@ -1247,6 +1260,26 @@ export interface operations {
12471260
500: components['responses']['Error'];
12481261
};
12491262
};
1263+
'get-campaigns-cid-clusters': {
1264+
parameters: {
1265+
path: {
1266+
cid: string;
1267+
};
1268+
};
1269+
responses: {
1270+
/** OK */
1271+
200: {
1272+
content: {
1273+
'application/json': {
1274+
items?: components['schemas']['Cluster'][];
1275+
};
1276+
};
1277+
};
1278+
400: components['responses']['Error'];
1279+
403: components['responses']['Error'];
1280+
500: components['responses']['Error'];
1281+
};
1282+
};
12501283
'get-campaigns-cid-custom-statuses': {
12511284
parameters: {
12521285
path: {
@@ -1636,6 +1669,8 @@ export interface operations {
16361669
};
16371670
query: {
16381671
showAsCustomer?: boolean;
1672+
/** filterBy[<fieldName>]=<fieldValue> */
1673+
filterBy?: components['parameters']['filterBy'];
16391674
};
16401675
};
16411676
responses: {

src/features/api/index.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ const injectedRtkApi = api.injectEndpoints({
9797
>({
9898
query: (queryArg) => ({ url: `/campaigns/${queryArg.cid}/bugTypes` }),
9999
}),
100+
getCampaignsByCidClusters: build.query<
101+
GetCampaignsByCidClustersApiResponse,
102+
GetCampaignsByCidClustersApiArg
103+
>({
104+
query: (queryArg) => ({ url: `/campaigns/${queryArg.cid}/clusters` }),
105+
}),
100106
getCampaignsByCidCustomStatuses: build.query<
101107
GetCampaignsByCidCustomStatusesApiResponse,
102108
GetCampaignsByCidCustomStatusesApiArg
@@ -231,7 +237,10 @@ const injectedRtkApi = api.injectEndpoints({
231237
>({
232238
query: (queryArg) => ({
233239
url: `/campaigns/${queryArg.cid}/ux`,
234-
params: { showAsCustomer: queryArg.showAsCustomer },
240+
params: {
241+
showAsCustomer: queryArg.showAsCustomer,
242+
filterBy: queryArg.filterBy,
243+
},
235244
}),
236245
}),
237246
getCampaignsByCidWidgets: build.query<
@@ -438,6 +447,9 @@ const injectedRtkApi = api.injectEndpoints({
438447
body: queryArg.body,
439448
}),
440449
}),
450+
getMediaById: build.query<GetMediaByIdApiResponse, GetMediaByIdApiArg>({
451+
query: (queryArg) => ({ url: `/media/${queryArg.id}` }),
452+
}),
441453
}),
442454
overrideExisting: false,
443455
});
@@ -611,6 +623,12 @@ export type GetCampaignsByCidBugTypesApiArg = {
611623
/** Campaign id */
612624
cid: string;
613625
};
626+
export type GetCampaignsByCidClustersApiResponse = /** status 200 OK */ {
627+
items?: Cluster[];
628+
};
629+
export type GetCampaignsByCidClustersApiArg = {
630+
cid: string;
631+
};
614632
export type GetCampaignsByCidCustomStatusesApiResponse =
615633
/** status 200 OK */ BugCustomStatus[];
616634
export type GetCampaignsByCidCustomStatusesApiArg = {
@@ -814,6 +832,8 @@ export type GetCampaignsByCidUxApiArg = {
814832
/** Campaign id */
815833
cid: string;
816834
showAsCustomer?: boolean;
835+
/** filterBy[<fieldName>]=<fieldValue> */
836+
filterBy?: any;
817837
};
818838
export type GetCampaignsByCidWidgetsApiResponse =
819839
/** status 200 OK */
@@ -1092,6 +1112,10 @@ export type DeleteWorkspacesByWidUsersApiArg = {
10921112
include_shared?: boolean;
10931113
};
10941114
};
1115+
export type GetMediaByIdApiResponse = unknown;
1116+
export type GetMediaByIdApiArg = {
1117+
id: string;
1118+
};
10951119
export type Error = {
10961120
message: string;
10971121
code: number;
@@ -1293,6 +1317,10 @@ export type BugAdditionalField = {
12931317
name: string;
12941318
value: string;
12951319
} & (BugAdditionalFieldRegex | BugAdditionalFieldSelect);
1320+
export type Cluster = {
1321+
id: number;
1322+
name: string;
1323+
};
12961324
export type ReportExtensions =
12971325
| 'pdf'
12981326
| 'doc'
@@ -1439,6 +1467,7 @@ export const {
14391467
usePatchCampaignsByCidBugsAndBidMutation,
14401468
useGetCampaignsByCidBugsAndBidSiblingsQuery,
14411469
useGetCampaignsByCidBugTypesQuery,
1470+
useGetCampaignsByCidClustersQuery,
14421471
useGetCampaignsByCidCustomStatusesQuery,
14431472
usePatchCampaignsByCidCustomStatusesMutation,
14441473
useDeleteCampaignsByCidCustomStatusesMutation,
@@ -1477,4 +1506,5 @@ export const {
14771506
useGetWorkspacesByWidUsersQuery,
14781507
usePostWorkspacesByWidUsersMutation,
14791508
useDeleteWorkspacesByWidUsersMutation,
1509+
useGetMediaByIdQuery,
14801510
} = injectedRtkApi;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useAppSelector } from 'src/app/hooks';
2+
3+
export type ClusterFilterType = {
4+
clusters: {
5+
available: { id: number; name: string }[];
6+
selected: { id: number; name: string }[];
7+
};
8+
};
9+
10+
export const ClusterFilter = {
11+
reset: (state: ClusterFilterType) => ({
12+
clusters: {
13+
...ClusterFilter.getCurrent(state),
14+
selected: [],
15+
},
16+
}),
17+
getCurrent: (state?: ClusterFilterType) => ({
18+
available: state?.clusters?.available ? state.clusters.available : [],
19+
selected: state?.clusters?.selected ? state.clusters.selected : [],
20+
}),
21+
setAvailable: (
22+
state: ClusterFilterType,
23+
clusters?: { id: number; name: string }[]
24+
) => ({
25+
clusters: {
26+
...ClusterFilter.getCurrent(state),
27+
...(clusters ? { available: clusters } : {}),
28+
},
29+
}),
30+
filter: (
31+
state: ClusterFilterType,
32+
clusters?: { id: number; name: string }[]
33+
) => ({
34+
clusters: {
35+
...ClusterFilter.getCurrent(state),
36+
...(clusters ? { selected: clusters } : {}),
37+
},
38+
}),
39+
getValues: () => {
40+
const uxDataSlice = useAppSelector((state) => state.uxFilters);
41+
42+
if (!uxDataSlice.currentCampaign) return undefined;
43+
44+
if (!uxDataSlice.campaigns[uxDataSlice.currentCampaign].clusters)
45+
return undefined;
46+
47+
const campaign: ClusterFilterType =
48+
uxDataSlice.campaigns[uxDataSlice.currentCampaign];
49+
50+
if (!campaign.clusters.selected) return undefined;
51+
return campaign.clusters.selected;
52+
},
53+
getIds: () => {
54+
const values = ClusterFilter.getValues();
55+
if (!values) return undefined;
56+
return values.map((t) => t.id);
57+
},
58+
};

src/features/uxFilters/index.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { createSlice } from '@reduxjs/toolkit';
2+
import { useAppSelector } from 'src/app/hooks';
3+
import { ClusterFilter, ClusterFilterType } from './clusterFilter';
4+
import { SeverityFilter, SeverityFilterType } from './severityFilter';
5+
import { InsightState, InsightStateType } from './insights';
6+
7+
export interface FilterState {
8+
currentCampaign?: number;
9+
campaigns: {
10+
[campaign_id: string]: ClusterFilterType &
11+
SeverityFilterType &
12+
InsightStateType;
13+
};
14+
}
15+
16+
const initialState: FilterState = {
17+
campaigns: {},
18+
};
19+
20+
const filtersSlice = createSlice({
21+
name: 'uxfilters',
22+
initialState,
23+
reducers: {
24+
selectUxCampaign: (state, action) => {
25+
const { campaignId, filters } = action.payload;
26+
state.campaigns[campaignId as number] = {
27+
...(state.campaigns[campaignId as number]
28+
? state.campaigns[campaignId as number]
29+
: {}),
30+
...ClusterFilter.setAvailable(
31+
state.campaigns[campaignId as number],
32+
filters.clusters
33+
),
34+
...SeverityFilter.setAvailable(
35+
state.campaigns[campaignId as number],
36+
filters.severities
37+
),
38+
...InsightState.setAvailable(
39+
state.campaigns[campaignId as number],
40+
filters.insights
41+
),
42+
};
43+
state.currentCampaign = campaignId;
44+
},
45+
updateFilters: (state, action) => {
46+
const { filters } = action.payload;
47+
if (!state.currentCampaign) return;
48+
49+
state.campaigns[state.currentCampaign] = {
50+
...state.campaigns[state.currentCampaign],
51+
...ClusterFilter.filter(
52+
state.campaigns[state.currentCampaign],
53+
filters.clusters
54+
),
55+
...SeverityFilter.filter(
56+
state.campaigns[state.currentCampaign],
57+
filters.severities
58+
),
59+
};
60+
},
61+
resetFilters(state) {
62+
if (!state.currentCampaign) return;
63+
state.campaigns[state.currentCampaign] = {
64+
...state.campaigns[state.currentCampaign],
65+
...ClusterFilter.reset(state.campaigns[state.currentCampaign]),
66+
...SeverityFilter.reset(state.campaigns[state.currentCampaign]),
67+
};
68+
},
69+
},
70+
});
71+
72+
export const getSelectedUxFiltersIds = () => ({
73+
clusters: ClusterFilter.getIds(),
74+
severities: SeverityFilter.getIds(),
75+
});
76+
77+
export const getSelectedUxFilters = () => ({
78+
clusters: ClusterFilter.getValues(),
79+
severities: SeverityFilter.getValues(),
80+
});
81+
82+
export const getCurrentUxData = () => {
83+
const { currentCampaign, campaigns } = useAppSelector(
84+
(state) => state.uxFilters
85+
);
86+
if (!currentCampaign || !campaigns[currentCampaign as number]) return null;
87+
88+
const campaign = campaigns[currentCampaign as number];
89+
if (!campaign) return false;
90+
return campaign;
91+
};
92+
93+
export const { selectUxCampaign, updateFilters, resetFilters } =
94+
filtersSlice.actions;
95+
96+
export default filtersSlice.reducer;

src/features/uxFilters/insights.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useAppSelector } from 'src/app/hooks';
2+
3+
export type InsightStateType = {
4+
insights: {
5+
available: { id: number; name: string }[];
6+
};
7+
};
8+
9+
export const InsightState = {
10+
getCurrent: (state?: InsightStateType) => ({
11+
available: state?.insights?.available ? state.insights.available : [],
12+
}),
13+
setAvailable: (
14+
state: InsightStateType,
15+
insights?: { id: number; name: string }[]
16+
) => ({
17+
insights: {
18+
...InsightState.getCurrent(state),
19+
...(insights ? { available: insights } : {}),
20+
},
21+
}),
22+
getValues: () => {
23+
const uxDataSlice = useAppSelector((state) => state.uxFilters);
24+
25+
if (!uxDataSlice.currentCampaign) return undefined;
26+
27+
if (!uxDataSlice.campaigns[uxDataSlice.currentCampaign].insights)
28+
return undefined;
29+
30+
const campaign: InsightStateType =
31+
uxDataSlice.campaigns[uxDataSlice.currentCampaign];
32+
33+
if (!campaign.insights.available) return undefined;
34+
return campaign.insights.available;
35+
},
36+
getIndex: (id: number) => {
37+
const values = InsightState.getValues();
38+
if (!values) return undefined;
39+
return values.findIndex((t) => t.id === id);
40+
},
41+
};

0 commit comments

Comments
 (0)