Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4fbb5c9
feat: temp
henrikmv Aug 1, 2025
e6c48f0
Revert "feat: temp"
henrikmv Aug 1, 2025
f90aad2
fix: add no value checkbox
henrikmv Aug 1, 2025
b124806
feat: add is not empty
henrikmv Aug 1, 2025
3317287
fix: is not empty bug for input field
henrikmv Aug 1, 2025
91d2fbe
feat: generic
henrikmv Aug 1, 2025
fa7c494
fix: code clean up
henrikmv Aug 1, 2025
6d810f1
feat: toggle feature
henrikmv Aug 5, 2025
21b0824
fix: store not nul and null filters for wl
henrikmv Aug 5, 2025
9baf0d2
feat: code clean up
henrikmv Aug 5, 2025
a53148f
feat: make generic
henrikmv Aug 5, 2025
4462d7c
feat: check boxs check off
henrikmv Aug 5, 2025
10e78f1
feat: imports imports
henrikmv Aug 5, 2025
c74268c
fix: import paths
henrikmv Aug 5, 2025
c9b8ebf
fix: delete folder
henrikmv Aug 5, 2025
f3bd33a
fix: rename and clean up helpers
henrikmv Aug 6, 2025
9d44f3e
fix: rename consts
henrikmv Aug 6, 2025
2f6667d
fix: temp clean up
henrikmv Aug 6, 2025
2116a36
fix: change helpers file
henrikmv Aug 6, 2025
6d4e303
fix: create label const
henrikmv Aug 6, 2025
6768e2d
fix: label bug for stored wl
henrikmv Aug 6, 2025
90f08ce
fix: change value on commit
henrikmv Aug 12, 2025
6c53252
feat: isempty converting
henrikmv Aug 15, 2025
c3eb46b
fix: move files
henrikmv Aug 15, 2025
69a1c11
Merge branch 'master' into hv/feat/DHIS2-18904_AddNoValueToFilters
henrikmv Aug 15, 2025
614096c
feat: code clean up
henrikmv Aug 15, 2025
54e8af1
fix: review comments for structure improvements
henrikmv Aug 26, 2025
c958503
Merge branch 'master' into hv/feat/DHIS2-18904_AddNoValueToFilters
henrikmv Aug 26, 2025
3ac77db
fix: build failing
henrikmv Aug 27, 2025
d65d5af
Merge branch 'master' into hv/feat/DHIS2-18904_AddNoValueToFilters
henrikmv Sep 17, 2025
7cbc46c
fix: missing type in file
henrikmv Sep 17, 2025
09b6135
fix: convert files to typescript
henrikmv Sep 17, 2025
f3caab9
fix: handle enter key press
henrikmv Sep 17, 2025
6143981
fix: cypress test failing
henrikmv Oct 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress/e2e/WorkingLists/sharedSteps.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ When(/^you set the first name filter to (.*)$/, (name) => {
.click();

cy.get('[data-test="list-view-filter-contents"]')
.find('input')
.find('input[placeholder="Contains text"]')
.type(name)
.blur();
});
Expand Down
6 changes: 6 additions & 0 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,12 @@ msgstr "Absolute range"
msgid "Relative range"
msgstr "Relative range"

msgid "Is empty"
msgstr "Is empty"

msgid "Is not empty"
msgstr "Is not empty"

msgid "Max"
msgstr "Max"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const FEATURES = Object.freeze({
kotlinRuleEngine: 'kotlinRuleEngine',
orgUnitReplaceOuQueryParam: 'orgUnitReplaceOuQueryParam',
enrollmentStatusReplaceProgramStatusQueryParam: 'enrollmentStatusReplaceProgramStatusQueryParam',
emptyValueFilter: 'emptyValueFilter',
});

const MINOR_VERSION_SUPPORT = Object.freeze({
Expand All @@ -38,6 +39,7 @@ const MINOR_VERSION_SUPPORT = Object.freeze({
[FEATURES.kotlinRuleEngine]: 42,
[FEATURES.orgUnitReplaceOuQueryParam]: 42,
[FEATURES.enrollmentStatusReplaceProgramStatusQueryParam]: 42,
[FEATURES.emptyValueFilter]: 42,
});

export const hasAPISupportForFeature = (minorVersion: string | number, featureName: string) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { Checkbox, MenuDivider } from '@dhis2/ui';
import { useFeature, FEATURES } from 'capture-core-utils/featuresSupport';
import {
EMPTY_VALUE_FILTER,
NOT_EMPTY_VALUE_FILTER,
EMPTY_VALUE_FILTER_LABEL,
NOT_EMPTY_VALUE_FILTER_LABEL,
} from './constants';
import type { EmptyValueFilterCheckboxesProps } from './types';

export const EmptyValueFilterCheckboxes = ({
value,
onEmptyChange,
onNotEmptyChange,
}: EmptyValueFilterCheckboxesProps) => {
const emptyValueFilterSupported = useFeature(FEATURES.emptyValueFilter);

if (!emptyValueFilterSupported) {
return null;
}

return (
<div>
<Checkbox
label={EMPTY_VALUE_FILTER_LABEL}
checked={value === EMPTY_VALUE_FILTER}
onChange={onEmptyChange}
/>
<Checkbox
label={NOT_EMPTY_VALUE_FILTER_LABEL}
checked={value === NOT_EMPTY_VALUE_FILTER}
onChange={onNotEmptyChange}
/>
<MenuDivider />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import i18n from '@dhis2/d2-i18n';

export const EMPTY_VALUE_FILTER = 'EMPTY_VALUE_FILTER';
export const NOT_EMPTY_VALUE_FILTER = 'NOT_EMPTY_VALUE_FILTER';

export const API_NOT_EMPTY_VALUE_FILTER = '!null';
export const API_EMPTY_VALUE_FILTER = 'null';

export const EMPTY_VALUE_FILTER_LABEL = i18n.t('Is empty');
export const NOT_EMPTY_VALUE_FILTER_LABEL = i18n.t('Is not empty');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './emptyValue.const';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
API_EMPTY_VALUE_FILTER,
EMPTY_VALUE_FILTER_LABEL,
NOT_EMPTY_VALUE_FILTER_LABEL,
} from './constants';
import type { EmptyValueFilterData } from './types';

export const fromApiEmptyValueFilter = (filter: Record<string, unknown>): EmptyValueFilterData | undefined => {
if (typeof filter?.[API_EMPTY_VALUE_FILTER] === 'boolean') {
return {
isEmpty: filter[API_EMPTY_VALUE_FILTER] as boolean,
value: filter[API_EMPTY_VALUE_FILTER] ? EMPTY_VALUE_FILTER_LABEL : NOT_EMPTY_VALUE_FILTER_LABEL,
};
}
return undefined;
};

export const toApiEmptyValueFilter = (filter: EmptyValueFilterData): Record<string, boolean> | undefined => {
if (filter.isEmpty === true) {
return { [API_EMPTY_VALUE_FILTER]: true };
}
if (filter.isEmpty === false) {
return { [API_EMPTY_VALUE_FILTER]: false };
}
return undefined;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {
EMPTY_VALUE_FILTER,
NOT_EMPTY_VALUE_FILTER,
} from './constants';

export const isEmptyValueFilter = (value?: string | null): boolean =>
value === EMPTY_VALUE_FILTER || value === NOT_EMPTY_VALUE_FILTER;

export const makeCheckboxHandler =
(flag: string) =>
(onCommit: (value?: string | null) => void) =>
({ checked }: { checked: boolean }) =>
onCommit(checked ? flag : '');
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { EmptyValueFilterCheckboxes } from './EmptyValueFilterCheckboxes.component';
export * from './emptyValueFilterHelpers';
export * from './constants';
export * from './converters';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type EmptyValueFilterCheckboxesProps = {
value?: string | null;
onEmptyChange: (args: { checked: boolean }) => void;
onNotEmptyChange: (args: { checked: boolean }) => void;
};

export type EmptyValueFilterData = {
value: string;
isEmpty?: boolean;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { EmptyValueFilterCheckboxesProps, EmptyValueFilterData } from './emptyValue.types';
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ type Props = {
};

class InputPlain extends React.Component<Props> {
handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
handleKeyDown = (payload: { value?: string }, event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
this.props.onEnterKey(this.props.value);
if (payload.value) {
this.props.onEnterKey(payload.value);
}
}
}
};

render() {
const { onEnterKey, ...passOnProps } = this.props;
return (
<D2TextField
onKeyPress={this.handleKeyPress}
onKeyDown={this.handleKeyDown}
placeholder={i18n.t('Contains text')}
{...passOnProps}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import { Input } from './Input.component';
import { getTextFilterData } from './textFilterDataGetter';
import type { UpdatableFilterContent } from '../types';
import type { TextFilterProps, Value } from './Text.types';
import {
makeCheckboxHandler,
isEmptyValueFilter,
EMPTY_VALUE_FILTER,
NOT_EMPTY_VALUE_FILTER,
EmptyValueFilterCheckboxes,
} from '../EmptyValue';

export class TextFilter extends Component<TextFilterProps> implements UpdatableFilterContent<Value> {
onGetUpdateData(updatedValue?: Value) {
const value = typeof updatedValue !== 'undefined' ? updatedValue : this.props.value;

if (!value) {
return null;
}

return getTextFilterData(value);
}

Expand All @@ -20,22 +23,36 @@ export class TextFilter extends Component<TextFilterProps> implements UpdatableF
}

handleBlur = (value: string) => {
this.props.onCommitValue(value);
}
if (value) {
this.props.onCommitValue(value);
}
};

handleChange = (value: string) => {
handleInputChange = (value: string) => {
this.props.onCommitValue(value);
}
};

handleEmptyValueCheckboxChange = makeCheckboxHandler(EMPTY_VALUE_FILTER)(this.props.onCommitValue);
handleNotEmptyValueCheckboxChange = makeCheckboxHandler(NOT_EMPTY_VALUE_FILTER)(this.props.onCommitValue);

render() {
const { value } = this.props;

return (
<Input
onChange={this.handleChange}
onBlur={this.handleBlur}
onEnterKey={this.handleEnterKey}
value={value}
/>
<>
<EmptyValueFilterCheckboxes
value={value}
onEmptyChange={this.handleEmptyValueCheckboxChange}
onNotEmptyChange={this.handleNotEmptyValueCheckboxChange}
/>

<Input
onChange={this.handleInputChange}
onBlur={this.handleBlur}
onEnterKey={this.handleEnterKey}
value={!isEmptyValueFilter(value) ? value : ''}
/>
</>
);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { TextFilter } from './TextFilter.component';
import type { TextFilterData } from './types';
import { EMPTY_VALUE_FILTER, NOT_EMPTY_VALUE_FILTER } from '../EmptyValue';
import type { Value } from './Text.types';

type Props = {
Expand All @@ -16,9 +17,10 @@ type State = {

export class TextFilterManager extends React.Component<Props, State> {
static calculateDefaultState(filter: TextFilterData | null | undefined) {
return {
value: (filter && filter.value ? filter.value : undefined),
};
if (filter?.isEmpty === true) return { value: EMPTY_VALUE_FILTER };
if (filter?.isEmpty === false) return { value: NOT_EMPTY_VALUE_FILTER };

return { value: filter?.value || undefined };
}

constructor(props: Props) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import {
isEmptyValueFilter,
EMPTY_VALUE_FILTER,
EMPTY_VALUE_FILTER_LABEL,
NOT_EMPTY_VALUE_FILTER_LABEL,
} from '../EmptyValue';
import type { TextFilterData } from './types';

export function getTextFilterData(value: string): TextFilterData {
return {
value,
};
}
export const getTextFilterData = (value: string | null | undefined): TextFilterData | null | undefined => {
if (isEmptyValueFilter(value)) {
return value === EMPTY_VALUE_FILTER
? { value: EMPTY_VALUE_FILTER_LABEL, isEmpty: true }
: { value: NOT_EMPTY_VALUE_FILTER_LABEL, isEmpty: false };
}
return value ? { value } : null;
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type TextFilterData = {
value: string;
isEmpty?: boolean,
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { BooleanFilter } from './Boolean';
export { DateFilter } from './Date';
export { OptionSetFilter } from './OptionSet';
export { AssigneeFilter, modeKeys as assigneeFilterModeKeys } from './Assignee';
export { EmptyValueFilterCheckboxes } from './EmptyValue';

export { assigneeFilterModes } from './Assignee/constants';
export { dateFilterTypes } from './Date/constants';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type Props = {
placeholder?: string;
dataTest?: string;
onKeyPress?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
onKeyDown?: (payload: { value?: string }, event: React.KeyboardEvent<HTMLInputElement>) => void;
};

export class D2TextField extends Component<Props> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getCustomColumnsConfiguration } from '../getCustomColumnsConfiguration'
import { getOptionSetFilter } from './optionSet';
import { apiAssigneeFilterModes, apiDateFilterTypes } from '../../../constants';
import type { QuerySingleResource } from '../../../../../../utils/api/api.types';

import { fromApiEmptyValueFilter } from '../../../../../FiltersForTypes/EmptyValue';
import {
filterTypesObject,
type AssigneeFilterData,
Expand Down Expand Up @@ -174,6 +174,11 @@ const getDataElementFilters = (
return null;
}

const emptyValueFilter = fromApiEmptyValueFilter(serverFilter);
if (emptyValueFilter) {
return { id: serverFilter.dataItem, ...emptyValueFilter };
}

if (isOptionSetFilter(element.type, serverFilter)) {
return {
...getOptionSetFilter(serverFilter as any, element.type),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
ApiDataFilterAssignee,
ApiEventQueryCriteria,
} from '../../../types';
import { toApiEmptyValueFilter } from '../../../../../FiltersForTypes/EmptyValue';

type ColumnForConverterBase = {
id: string,
Expand Down Expand Up @@ -117,6 +118,10 @@ const typeConvertFilters = (filters: any, columns: ColumnsForConverter) => Objec
return null;
}

if (typeof filter.isEmpty === 'boolean') {
return { ...toApiEmptyValueFilter(filter), dataItem: key };
}

if (filter.usingOptionSet) {
return {
...getApiOptionSetFilter(filter, element.type),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { areRelativeRangeValuesSupported }
import { DATE_TYPES, ASSIGNEE_MODES, MAIN_FILTERS } from '../../../constants';
import { ADDITIONAL_FILTERS } from '../../eventFilters';
import { type DataElement } from '../../../../../../metaData';
import { fromApiEmptyValueFilter } from '../../../../../FiltersForTypes/EmptyValue';

const getTextFilter = (
filter: ApiDataFilterText & ApiDataFilterTextUnique,
Expand Down Expand Up @@ -164,6 +165,12 @@ const convertDataElementFilters = (
if (!element || !getFilterByType[element.type]) {
return acc;
}

const emptyValueFilter = fromApiEmptyValueFilter(serverFilter);
if (emptyValueFilter) {
return { ...acc, [serverFilter.dataItem]: emptyValueFilter };
}

const value = isOptionSetFilter(element.type, serverFilter)
? getOptionSetFilter(serverFilter, element.type)
: getFilterByType[element.type](serverFilter);
Expand All @@ -181,6 +188,12 @@ const convertAttributeFilters = (
if (!element || !getFilterByType[element.type]) {
return acc;
}

const emptyValueFilter = fromApiEmptyValueFilter(serverFilter);
if (emptyValueFilter) {
return { ...acc, [serverFilter.attribute]: emptyValueFilter };
}

const value = isOptionSetFilter(element.type, serverFilter)
? getOptionSetFilter(serverFilter, element.type)
: getFilterByType[element.type](serverFilter, element);
Expand Down
Loading
Loading