Skip to content

Commit 9eaa98a

Browse files
committed
feat: simple single select field tet selector in new and search
1 parent 6ef45d8 commit 9eaa98a

File tree

8 files changed

+57
-101
lines changed

8 files changed

+57
-101
lines changed

i18n/en.pot

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ msgstr ""
55
"Content-Type: text/plain; charset=utf-8\n"
66
"Content-Transfer-Encoding: 8bit\n"
77
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
8-
"POT-Creation-Date: 2025-12-18T09:03:09.491Z\n"
9-
"PO-Revision-Date: 2025-12-18T09:03:09.492Z\n"
8+
"POT-Creation-Date: 2025-12-19T18:08:41.230Z\n"
9+
"PO-Revision-Date: 2025-12-19T18:08:41.230Z\n"
1010

1111
msgid "Choose one or more dates..."
1212
msgstr "Choose one or more dates..."
@@ -809,9 +809,6 @@ msgstr "Save as new"
809809
msgid "New"
810810
msgstr "New"
811811

812-
msgid "Create for"
813-
msgstr "Create for"
814-
815812
msgid "You can also choose a program from the top bar and create in that program"
816813
msgstr "You can also choose a program from the top bar and create in that program"
817814

@@ -1004,9 +1001,6 @@ msgstr "Choose a program"
10041001
msgid "Search for {{titleText}}"
10051002
msgstr "Search for {{titleText}}"
10061003

1007-
msgid "Search for"
1008-
msgstr "Search for"
1009-
10101004
msgid ""
10111005
"You can also select a program from the top bar to search within that "
10121006
"program."

src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ const RegistrationDataEntryPlain = ({
188188
<div className={classes.tetypeContainer}>
189189
<TrackedEntityTypeSelector
190190
onSelect={handleRegistrationScopeSelection}
191-
headerText={i18n.t('Create for')}
192191
footerText={i18n.t('You can also choose a program from the top bar and create in that program')}
193192
accessNeeded="write"
194193
/>
@@ -256,7 +255,6 @@ const RegistrationDataEntryPlain = ({
256255
<div className={classes.tetypeContainer}>
257256
<TrackedEntityTypeSelector
258257
onSelect={handleRegistrationScopeSelection}
259-
headerText={i18n.t('Create for')}
260258
footerText={i18n.t('You can also choose a program from the top bar and create in that program')}
261259
accessNeeded="write"
262260
/>

src/core_modules/capture-core/components/SearchBox/SearchBox.component.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ const Index = ({
101101
{selectedSearchScopeType !== searchScopes.PROGRAM && (
102102
<TrackedEntityTypeSelector
103103
onSelect={handleSearchScopeSelection}
104-
headerText={i18n.t('Search for')}
105104
footerText={i18n.t(
106105
'You can also select a program from the top bar to search within that program.',
107106
)}

src/core_modules/capture-core/components/TeiSearch/SearchProgramSelector/SearchProgramSelector.types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export type SelectOption = {
22
label: string;
3-
value: any;
4-
icon?: React.ReactNode | null;
3+
value: string;
54
};
65

76
export type SearchProgramSelectorProps = {
Lines changed: 50 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,71 @@
11
import React, { useMemo, type ComponentType } from 'react';
22
import i18n from '@dhis2/d2-i18n';
33
import { withStyles, type WithStyles } from 'capture-core-utils/styles';
4-
5-
import { spacers, SimpleSingleSelect } from '@dhis2/ui';
4+
import { spacers } from '@dhis2/ui';
5+
// @ts-expect-error - SimpleSingleSelectField exists but types may not be available
6+
import { SimpleSingleSelectField } from '@dhis2-ui/select';
67
import type { Props } from './TrackedEntityTypeSelector.types';
78
import { scopeTypes } from '../../metaData';
89
import { useTrackedEntityTypesWithCorrelatedPrograms, useCurrentTrackedEntityTypeId } from '../../hooks';
9-
import { InfoIconText } from '../InfoIconText';
1010

1111
const styles: Readonly<any> = ({ typography }: any) => ({
1212
searchRow: {
13-
maxWidth: typography.pxToRem(400),
14-
display: 'flex',
15-
flexDirection: 'column',
16-
justifyContent: 'start',
17-
marginBottom: spacers.dp8,
18-
},
19-
searchRowSelectElement: {
20-
width: '100%',
21-
},
22-
informativeIcon: {
23-
marginLeft: 8,
24-
},
25-
customEmpty: {
26-
textAlign: 'center',
27-
padding: '8px 24px',
13+
maxWidth: typography.pxToRem(450),
14+
marginBottom: spacers.dp24,
15+
2816
},
2917
});
3018

3119
type ComponentProps = Props & WithStyles<typeof styles>;
3220

3321
export const TrackedEntityTypeSelectorPlain =
34-
({ classes, onSelect, onSetTrackedEntityTypeIdOnUrl, accessNeeded, headerText, footerText }: ComponentProps) => {
35-
const trackedEntityTypesWithCorrelatedPrograms = useTrackedEntityTypesWithCorrelatedPrograms();
36-
const selectedSearchScopeId = useCurrentTrackedEntityTypeId();
37-
38-
const options = useMemo(() =>
39-
Object.values(trackedEntityTypesWithCorrelatedPrograms)
40-
.filter(({ trackedEntityTypeAccess }: any) => {
41-
if (accessNeeded === 'write') {
42-
return trackedEntityTypeAccess?.data?.write;
43-
}
44-
if (accessNeeded === 'read') {
45-
return trackedEntityTypeAccess?.data?.read;
46-
}
47-
return false;
48-
})
49-
.map(({ trackedEntityTypeName, trackedEntityTypeId }: any) => ({
50-
value: trackedEntityTypeId,
51-
label: trackedEntityTypeName,
52-
})),
53-
[accessNeeded, trackedEntityTypesWithCorrelatedPrograms],
54-
);
22+
({ classes, onSelect, onSetTrackedEntityTypeIdOnUrl, accessNeeded, footerText }: ComponentProps) => {
23+
const trackedEntityTypesWithCorrelatedPrograms = useTrackedEntityTypesWithCorrelatedPrograms();
24+
const selectedSearchScopeId = useCurrentTrackedEntityTypeId();
5525

56-
const selectedOption = selectedSearchScopeId
57-
? options.find(opt => opt.value === selectedSearchScopeId) ?? null
58-
: null;
26+
const options = useMemo(() =>
27+
Object.values(trackedEntityTypesWithCorrelatedPrograms)
28+
.filter(({ trackedEntityTypeAccess }: any) => {
29+
if (accessNeeded === 'write') {
30+
return trackedEntityTypeAccess?.data?.write;
31+
}
32+
if (accessNeeded === 'read') {
33+
return trackedEntityTypeAccess?.data?.read;
34+
}
35+
return false;
36+
})
37+
.map(({ trackedEntityTypeName, trackedEntityTypeId }: any) => ({
38+
value: trackedEntityTypeId,
39+
label: trackedEntityTypeName,
40+
})),
41+
[accessNeeded, trackedEntityTypesWithCorrelatedPrograms],
42+
);
5943

60-
const handleSelectionChange = (nextValue: string | { label: string; value: string }) => {
61-
// Handle both string and object (implementation sends object, types say string)
62-
const value = typeof nextValue === 'string' ? nextValue : nextValue?.value || '';
63-
onSelect(value, scopeTypes.TRACKED_ENTITY_TYPE as keyof typeof scopeTypes);
64-
onSetTrackedEntityTypeIdOnUrl({ trackedEntityTypeId: value });
65-
};
44+
const selectedOption = useMemo(() => {
45+
if (!selectedSearchScopeId) {
46+
return undefined;
47+
}
48+
return options.find(opt => opt.value === selectedSearchScopeId);
49+
}, [selectedSearchScopeId, options]);
6650

67-
return (<>
51+
const handleSelectionChange = (option: { value: string; label: string }) => {
52+
onSelect(option.value, scopeTypes.TRACKED_ENTITY_TYPE as keyof typeof scopeTypes);
53+
onSetTrackedEntityTypeIdOnUrl({ trackedEntityTypeId: option.value });
54+
};
6855

69-
<div className={classes.searchRow}>
70-
<div className={classes.searchRowSelectElement}>
71-
{headerText && (
72-
<div style={{ display: 'block', marginBottom: '8px' }}>
73-
{headerText}
74-
</div>
75-
)}
76-
<SimpleSingleSelect
77-
name="tracked-entity-type-selector"
78-
options={options}
79-
// @ts-expect-error - selected is not typed correctly
80-
selected={selectedOption}
81-
placeholder={i18n.t('Select tracked entity type')}
82-
onChange={handleSelectionChange}
83-
empty={
84-
<div className={classes.customEmpty}>
85-
{i18n.t('No tracked entity types available')}
86-
</div>
87-
}
88-
/>
89-
</div>
90-
</div>
91-
{
92-
!selectedSearchScopeId &&
93-
<div className={classes.informativeIcon}>
94-
<InfoIconText>
95-
{ footerText }
96-
</InfoIconText>
97-
</div>
98-
}
99-
</>
100-
);
101-
};
56+
return (
57+
<div className={classes.searchRow}>
58+
<SimpleSingleSelectField
59+
name="tracked-entity-type-selector"
60+
helpText={footerText}
61+
options={options}
62+
selected={selectedOption}
63+
placeholder={i18n.t('Select tracked entity type')}
64+
onChange={handleSelectionChange}
65+
empty={i18n.t('No tracked entity types available')}
66+
/>
67+
</div>
68+
);
69+
};
10270

10371
export const TrackedEntityTypeSelectorComponent = withStyles(styles)(TrackedEntityTypeSelectorPlain) as ComponentType<Props>;

src/core_modules/capture-core/components/TrackedEntityTypeSelector/TrackedEntityTypeSelector.container.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { OwnProps } from './TrackedEntityTypeSelector.types';
44
import { setTrackedEntityTypeIdOnUrl } from './TrackedEntityTypeSelector.actions';
55
import { TrackedEntityTypeSelectorComponent } from './TrackedEntityTypeSelector.component';
66

7-
export const TrackedEntityTypeSelector = ({ onSelect, accessNeeded = 'read', headerText, footerText }: OwnProps) => {
7+
export const TrackedEntityTypeSelector = ({ onSelect, accessNeeded = 'read', footerText }: OwnProps) => {
88
const dispatch = useDispatch();
99

1010
const dispatchSetTrackedEntityTypeIdOnUrl = useCallback(
@@ -18,7 +18,6 @@ export const TrackedEntityTypeSelector = ({ onSelect, accessNeeded = 'read', hea
1818
accessNeeded={accessNeeded}
1919
onSelect={onSelect}
2020
onSetTrackedEntityTypeIdOnUrl={dispatchSetTrackedEntityTypeIdOnUrl}
21-
headerText={headerText}
2221
footerText={footerText}
2322
/>
2423
);

src/core_modules/capture-core/components/TrackedEntityTypeSelector/TrackedEntityTypeSelector.types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { scopeTypes } from '../../metaData';
33
export type OwnProps = {
44
accessNeeded?: 'write' | 'read';
55
onSelect: (searchScopeId: string, searchScopeType: keyof typeof scopeTypes) => void;
6-
headerText: string;
76
footerText: string;
87
};
98

src/core_modules/capture-core/components/WidgetEventSchedule/CategoryOptions/CategorySelector.component.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,9 @@ export class CategorySelector extends React.Component<Props, State> {
155155
onChange={(value: string | null) => {
156156
const selectedOption = value != null ? options.find(opt => opt.value === value) : null;
157157
this.setState({ selectedOption });
158-
if (selectedOption) {
159-
onChange(selectedOption);
160-
}
158+
// Match OptionsSelectVirtualized behavior: always call onChange, even when null
159+
// @ts-expect-error - onChange type doesn't accept null, but OptionsSelectVirtualized called it with null
160+
onChange(selectedOption);
161161
}}
162162
options={options}
163163
/> : null

0 commit comments

Comments
 (0)