Skip to content

Commit c2f0a8c

Browse files
authored
feat: [DHIS2-19144] working lists performance options (#3989)
For event programs, enable optional configuration in the datastore for the working lists. BREAKING CHANGE: Bump version to 102.0.0 to facilitate potential fixes for the bundled app in the 2.42 release
1 parent 197a245 commit c2f0a8c

File tree

26 files changed

+347
-69
lines changed

26 files changed

+347
-69
lines changed

src/core_modules/capture-core/components/FiltersForTypes/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ export type { NumericFilterData } from './Numeric/types';
1717
export type { OptionSetFilterData } from './OptionSet/types';
1818
export type { TextFilterData } from './Text/types';
1919
export type { TrueOnlyFilterData } from './TrueOnly/types';
20-
export type { UpdatableFilterContent, FilterData } from './types';
20+
export type { UpdatableFilterContent, FilterData, FilterDataInput } from './types';
2121

2222
export type { Options } from '../FormFields/Options/SelectBoxes';

src/core_modules/capture-core/components/FiltersForTypes/types/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ export type FilterData =
1616
OptionSetFilterData |
1717
TextFilterData |
1818
TrueOnlyFilterData;
19+
20+
export type FilterDataInput = FilterData & { locked?: boolean };

src/core_modules/capture-core/components/ListView/Filters/FilterButton/FilterButtonMain.component.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { ChevronDown, ChevronUp } from 'capture-ui/Icons';
77
import { ActiveFilterButton } from './ActiveFilterButton.component';
88
import { FilterSelectorContents } from '../Contents';
99
import type { UpdateFilter, ClearFilter, RemoveFilter } from '../../types';
10-
import type { FilterData, Options } from '../../../FiltersForTypes';
10+
import type { FilterData, Options, FilterDataInput } from '../../../FiltersForTypes';
11+
import { LockedFilterButton } from './LockedFilterButton.component';
1112

1213
const getStyles = (theme: Theme) => ({
1314
icon: {
@@ -40,7 +41,7 @@ type Props = {
4041
isRemovable?: boolean,
4142
onSetVisibleSelector: Function,
4243
selectorVisible: boolean,
43-
filterValue?: FilterData,
44+
filterValue?: FilterDataInput,
4445
buttonText?: string,
4546
disabled?: boolean,
4647
tooltipContent?: string,
@@ -118,6 +119,7 @@ class FilterButtonMainPlain extends Component<Props, State> {
118119
id={id}
119120
onUpdate={this.handleFilterUpdate}
120121
onClose={this.onClose}
122+
// $FlowFixMe
121123
filterValue={filterValue}
122124
onRemove={this.onRemove}
123125
isRemovable={isRemovable}
@@ -129,7 +131,7 @@ class FilterButtonMainPlain extends Component<Props, State> {
129131
this.activeFilterButtonInstance = activeFilterButtonInstance;
130132
}
131133

132-
renderWithAppliedFilter() {
134+
renderActiveFilterButton() {
133135
const { selectorVisible, classes, title, buttonText } = this.props;
134136

135137
const arrowIconElement = selectorVisible ? (
@@ -155,6 +157,21 @@ class FilterButtonMainPlain extends Component<Props, State> {
155157
);
156158
}
157159

160+
renderWithAppliedFilter() {
161+
const { filterValue, title, buttonText } = this.props;
162+
163+
if (filterValue?.locked) {
164+
return (
165+
<LockedFilterButton
166+
title={title}
167+
buttonText={buttonText}
168+
/>
169+
);
170+
}
171+
172+
return this.renderActiveFilterButton();
173+
}
174+
158175
renderWithoutAppliedFilter() {
159176
const { selectorVisible, classes, title, disabled, tooltipContent } = this.props;
160177

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// @flow
2+
import React, { useCallback, useMemo } from 'react';
3+
import i18n from '@dhis2/d2-i18n';
4+
import { withStyles } from '@material-ui/core/styles';
5+
import { Tooltip, Button } from '@dhis2/ui';
6+
7+
8+
const getStyles = () => ({
9+
button: {
10+
cursor: 'not-allowed !important',
11+
},
12+
});
13+
14+
type Props = {
15+
classes: {
16+
button: string,
17+
},
18+
title: string,
19+
buttonText?: string,
20+
};
21+
22+
const MAX_LENGTH_OF_VALUE = 10;
23+
24+
const LockedFilterButtonPlain = ({ classes, title, buttonText = '' }: Props) => {
25+
const getCappedValue = useCallback((value: string) => {
26+
const cappedValue = value.substring(0, MAX_LENGTH_OF_VALUE - 3).trimRight();
27+
return `${cappedValue}...`;
28+
}, []);
29+
30+
const viewValueForFiter = useMemo(() => {
31+
const calculatedValue = buttonText.length > MAX_LENGTH_OF_VALUE ? getCappedValue(buttonText) : buttonText;
32+
return `: ${calculatedValue}`;
33+
}, [buttonText, getCappedValue]);
34+
35+
return (
36+
<Tooltip
37+
content={`${i18n.t('Locked to:')} ${buttonText}`}
38+
placement={'bottom'}
39+
openDelay={300}
40+
>
41+
<Button
42+
className={classes.button}
43+
disabled
44+
>
45+
{title}
46+
{viewValueForFiter}
47+
</Button>
48+
</Tooltip>
49+
);
50+
};
51+
52+
export const LockedFilterButton = withStyles(getStyles)(LockedFilterButtonPlain);

src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function convertDate(filter: DateFilterData): string {
6363
return translatedPeriods[filter.period];
6464
}
6565
if (areRelativeRangeValuesSupported(filter.startBuffer, filter.endBuffer)) {
66-
return translatedPeriods[periods.RELATIVE_RANGE];
66+
return `${translatedPeriods[periods.RELATIVE_RANGE]} (${filter.startBuffer ?? ''} -> ${filter.endBuffer ?? ''})`;
6767
}
6868
return '';
6969
}

src/core_modules/capture-core/components/Pages/MainPage/EventWorkingListsInit/ConnectionStatusResolver/EventWorkingListsInitConnectionStatusResolver.component.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import React from 'react';
33
import { EventWorkingListsInitHeader } from '../Header';
44
import { EventWorkingListsOffline } from '../../../../WorkingLists/EventWorkingListsOffline';
5-
import { EventWorkingListsInitRunningMutationsHandler } from '../RunningMutationsHandler';
5+
import { EventWorkingListsInitOnline } from '../InitOnline';
66
import type { Props } from './eventWorkingListsInitConnectionStatusResolver.types';
77

88
export const EventWorkingListsInitConnectionStatusResolver = ({ isOnline, storeId, ...passOnProps }: Props) => (
@@ -13,7 +13,7 @@ export const EventWorkingListsInitConnectionStatusResolver = ({ isOnline, storeI
1313
storeId={storeId}
1414
/>
1515
:
16-
<EventWorkingListsInitRunningMutationsHandler
16+
<EventWorkingListsInitOnline
1717
{...passOnProps}
1818
storeId={storeId}
1919
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// @flow
2+
import React from 'react';
3+
import { withLoadingIndicator } from '../../../../../HOC';
4+
import { EventWorkingLists } from '../../../../WorkingLists/EventWorkingLists';
5+
import { useMainViewConfig } from './useMainViewConfig';
6+
import type { Props } from './eventWorkingListsInitOnline.types';
7+
8+
const EventWorkingListsWithLoadingIndicator = withLoadingIndicator()(EventWorkingLists);
9+
10+
export const EventWorkingListsInitOnline = ({
11+
mutationInProgress,
12+
...passOnProps
13+
}: Props) => {
14+
// Retrieving the viewConfig this high up in the component tree because this is capture app specific config
15+
// The EventWorkingLists can potentially be included a standalone Widget library in the future
16+
const { mainViewConfig, mainViewConfigReady } = useMainViewConfig();
17+
18+
return (
19+
<EventWorkingListsWithLoadingIndicator
20+
{...passOnProps}
21+
ready={!mutationInProgress && mainViewConfigReady}
22+
mainViewConfig={mainViewConfig}
23+
/>
24+
);
25+
};

src/core_modules/capture-core/components/Pages/MainPage/EventWorkingListsInit/RunningMutationsHandler/eventWorkingListsInitRunningMutationsHandler.types.js renamed to src/core_modules/capture-core/components/Pages/MainPage/EventWorkingListsInit/InitOnline/eventWorkingListsInitOnline.types.js

File renamed without changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// @flow
2+
export { EventWorkingListsInitOnline } from './EventWorkingListsInitOnline.component';
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// @flow
2+
/* eslint-disable complexity */
3+
import { isNumber, isObject } from 'lodash';
4+
import log from 'loglevel';
5+
import { errorCreator } from 'capture-core-utils';
6+
import { useEffect } from 'react';
7+
import { useApiMetadataQuery } from '../../../../../utils/reactQueryHelpers';
8+
import type { DataStoreWorkingLists, UseMainViewConfig } from './useMainViewConfig.types';
9+
10+
export const useMainViewConfig: UseMainViewConfig = () => {
11+
const {
12+
data: configExists,
13+
isLoading: namespaceIsLoading,
14+
isError: namespaceIsError,
15+
error: namespaceError,
16+
} = useApiMetadataQuery<any>(
17+
['dataStore', 'capture'], {
18+
resource: 'dataStore/capture',
19+
}, {
20+
select: (captureKeys: ?Array<string>) => captureKeys?.includes('workingLists'),
21+
});
22+
23+
const { data: mainViewConfig, isLoading, isError, error } = useApiMetadataQuery<any>(
24+
['dataStore', 'workingListsEvents'], {
25+
resource: 'dataStore/capture/workingLists',
26+
}, {
27+
enabled: !!configExists,
28+
select: (workingLists: DataStoreWorkingLists) => {
29+
// only adding support for relative event date as of now
30+
// we should use Zod here long-term to properly validate the structure of the object
31+
// and give error messages to the user
32+
if (workingLists?.version !== 1) {
33+
return undefined;
34+
}
35+
const occurredAt = workingLists?.global?.event?.mainView?.occurredAt;
36+
if (
37+
!occurredAt ||
38+
!isObject(occurredAt) ||
39+
occurredAt.type !== 'RELATIVE'
40+
) {
41+
return undefined;
42+
}
43+
44+
// The general idea is that the return value here should have the same data structure
45+
// as the response from the working lists api
46+
if (occurredAt.period) {
47+
if (['TODAY', 'THIS_WEEK', 'THIS_MONTH', 'THIS_YEAR', 'LAST_WEEK', 'LAST_MONTH', 'LAST_3_MONTHS']
48+
.includes(occurredAt.period)
49+
) {
50+
return {
51+
eventDate: {
52+
type: occurredAt.type,
53+
period: occurredAt.period,
54+
startBuffer: 0,
55+
endBuffer: 0,
56+
lockedAll: !!occurredAt.lockedInAllViews,
57+
},
58+
};
59+
}
60+
} else {
61+
if ((occurredAt.startBuffer && !isNumber(occurredAt.startBuffer)) ||
62+
(occurredAt.endBuffer && !isNumber(occurredAt.endBuffer)) ||
63+
(!occurredAt.startBuffer && !occurredAt.endBuffer)) {
64+
return undefined;
65+
}
66+
return {
67+
eventDate: {
68+
type: occurredAt.type,
69+
startBuffer: occurredAt.startBuffer,
70+
endBuffer: occurredAt.endBuffer,
71+
lockedAll: !!occurredAt.lockedInAllViews,
72+
},
73+
};
74+
}
75+
return undefined;
76+
},
77+
},
78+
);
79+
80+
useEffect(() => {
81+
if (namespaceIsError) {
82+
log.error(
83+
errorCreator(
84+
'capture namespace could not be fetched from the datastore')({ error }),
85+
);
86+
}
87+
if (isError) {
88+
log.error(
89+
errorCreator(
90+
'workingLists key could not be fetched from the datastore')({ error }),
91+
);
92+
}
93+
}, [isError, error, namespaceIsError, namespaceError]);
94+
95+
return {
96+
mainViewConfig,
97+
mainViewConfigReady: !namespaceIsLoading && !isLoading,
98+
};
99+
};

0 commit comments

Comments
 (0)