@iapps/ng-dhis2-ui 1.1.47 → 1.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,9 +25,9 @@ import { Layer } from '@dhis2-ui/layer';
25
25
  import { CircularLoader } from '@dhis2-ui/loader';
26
26
  import { MenuItem, MenuDivider } from '@dhis2-ui/menu';
27
27
  import parse from 'html-react-parser';
28
- import { Button as Button$1, IconDimensionData16, IconClock16, IconDimensionOrgUnit16, spacers as spacers$1, colors as colors$1, IconEdit16, theme as theme$1, Tooltip, SingleSelectField, SingleSelectOption, InputField as InputField$1, TabBar, Tab, Transfer, Modal as Modal$1, ModalTitle as ModalTitle$1, ModalContent as ModalContent$1, ModalActions as ModalActions$1, ButtonStrip as ButtonStrip$1, Checkbox, OrganisationUnitTree, MultiSelect, MultiSelectOption, IconWarningFilled16, Layer as Layer$1, Popper, FlyoutMenu, MenuItem as MenuItem$1, IconMore24, CircularLoader as CircularLoader$1, IconFilter16, DataTable, TableHead, DataTableRow, DataTableColumnHeader, TableBody, DataTableCell, TableFoot, Pagination, FileInputField, FileListItem, MultiSelectField, TextAreaField } from '@dhis2/ui';
28
+ import { Button as Button$1, IconDimensionData16, IconClock16, IconDimensionOrgUnit16, spacers as spacers$1, colors as colors$1, IconEdit16, theme as theme$1, Tooltip, SingleSelectField, SingleSelectOption, InputField as InputField$1, TabBar, Tab, Transfer, Modal as Modal$1, ModalTitle as ModalTitle$1, ModalContent as ModalContent$1, ModalActions as ModalActions$1, ButtonStrip as ButtonStrip$1, Checkbox, OrganisationUnitTree, MultiSelect, MultiSelectOption, IconWarningFilled16, Layer as Layer$1, Popper, FlyoutMenu, MenuItem as MenuItem$1, IconMore24, CircularLoader as CircularLoader$1, IconFilter16, DataTable, TableHead, DataTableRow, DataTableColumnHeader, TableBody, DataTableCell, TableFoot, Pagination, Chip, FileInputField, FileListItem, MultiSelectField, TextAreaField } from '@dhis2/ui';
29
29
  import { getNowInCalendar, generateFixedPeriods } from '@dhis2/multi-calendar-dates';
30
- import { isObject, isString, sortBy, isArray, find, camelCase, flatten, head, isEmpty } from 'lodash';
30
+ import { isObject, isString, sortBy, isArray, find, camelCase, flatten, debounce, isUndefined, head, isEmpty } from 'lodash';
31
31
  import cx from 'classnames';
32
32
  import { map as map$1, switchMap } from 'rxjs/operators';
33
33
  import * as i2 from '@angular/forms';
@@ -1564,6 +1564,62 @@ TransferOption.propTypes = {
1564
1564
  // Use of this source code is governed by a BSD-style
1565
1565
  // license that can be found in the LICENSE file.
1566
1566
 
1567
+ // Helper function to generate a unique class name
1568
+ const generateClassName$2 = (base) => `${base}-${Math.random().toString(36).substr(2, 9)}`;
1569
+ // Helper function to convert camelCased properties to hyphenated CSS properties
1570
+ const convertStylesToCSS$2 = (className, styles) => {
1571
+ let cssString = `.${className} {`;
1572
+ for (const key in styles) {
1573
+ if (typeof styles[key] === 'object' && key.startsWith('@media')) {
1574
+ const mediaStyles = convertStylesToCSS$2(className, styles[key]);
1575
+ cssString += `${key} { ${mediaStyles} }`;
1576
+ }
1577
+ else if (key.startsWith('&:')) {
1578
+ const pseudoClass = key.replace('&', '');
1579
+ cssString += `${pseudoClass} { ${convertStylesToCSS$2('', styles[key])} }`;
1580
+ }
1581
+ else {
1582
+ const cssKey = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
1583
+ cssString += `${cssKey}: ${styles[key]}; `;
1584
+ }
1585
+ }
1586
+ cssString += '}';
1587
+ return cssString;
1588
+ };
1589
+ // Custom hook for dynamic styles with generics
1590
+ const useDynamicStyles$2 = (stylesObj) => {
1591
+ const [classNames, setClassNames] = useState({});
1592
+ useEffect(() => {
1593
+ const convertedStyles = Object.keys(stylesObj).map((key) => {
1594
+ const className = generateClassName$2(key);
1595
+ const styleElement = document.createElement('style');
1596
+ styleElement.innerHTML = convertStylesToCSS$2(className, stylesObj[key]);
1597
+ document.head.appendChild(styleElement);
1598
+ return {
1599
+ key,
1600
+ className,
1601
+ styleElement,
1602
+ };
1603
+ });
1604
+ setClassNames(convertedStyles.reduce((classNameEntity, convertedStyle) => {
1605
+ return {
1606
+ ...classNameEntity,
1607
+ [convertedStyle.key]: convertedStyle.className,
1608
+ };
1609
+ }, {}));
1610
+ return () => {
1611
+ convertedStyles.forEach((convertedStyle) => {
1612
+ document.head.removeChild(convertedStyle.styleElement);
1613
+ });
1614
+ };
1615
+ }, []);
1616
+ return classNames;
1617
+ };
1618
+
1619
+ // Copyright 2025 UDSM DHIS2 Lab. All rights reserved.
1620
+ // Use of this source code is governed by a BSD-style
1621
+ // license that can be found in the LICENSE file.
1622
+
1567
1623
  // Copyright 2023 UDSM DHIS2 Lab. All rights reserved.
1568
1624
  // Use of this source code is governed by a BSD-style
1569
1625
  // license that can be found in the LICENSE file.
@@ -3364,6 +3420,9 @@ class LineListTableComponent extends ReactWrapperModule {
3364
3420
  const [filteredColumns, setFilteredColumns] = useState();
3365
3421
  const [inputValues, setInputValues] = useState({});
3366
3422
  const [filters, setFilters] = useState(false);
3423
+ //TODO: this will be migrated on the parent component
3424
+ const [checkValue, setCheckValue] = useState([]);
3425
+ const [valueMatch, setValuesMatch] = useState(false);
3367
3426
  // Store updaters in refs for Angular to access
3368
3427
  const updateRefs = useRef({
3369
3428
  setProgramIdState,
@@ -3413,10 +3472,16 @@ class LineListTableComponent extends ReactWrapperModule {
3413
3472
  const { columns, data, filteredEntityColumns } = getTrackedEntityData(response, this.programId, pager, this.filters);
3414
3473
  entityColumns = columns;
3415
3474
  entityData = data;
3416
- let firstTei = response.data
3417
- ?.trackedEntityInstances?.[0]?.trackedEntityInstance;
3418
- if (firstTei) {
3419
- this.firstValue.emit(firstTei);
3475
+ //TODO: Should be done on the parent
3476
+ let checkValues = [
3477
+ ...new Set(response.data?.trackedEntityInstances?.[0]?.enrollments?.[0]?.events?.flatMap((event) => event.dataValues
3478
+ .filter((dataValue) => /^[A-Za-z]{3}$/.test(dataValue.value))
3479
+ .map((dataValue) => dataValue.value))),
3480
+ ];
3481
+ if (checkValues) {
3482
+ // this.firstValue.emit(firstTei);
3483
+ setCheckValue(checkValues);
3484
+ setValuesMatch(!checkValues.includes(this.buttonFilter));
3420
3485
  }
3421
3486
  // setFilteredColumns(filteredEntityColumns);
3422
3487
  // Ensure filter inputs do not disappear when no data is returned
@@ -3508,9 +3573,12 @@ class LineListTableComponent extends ReactWrapperModule {
3508
3573
  padding: 8,
3509
3574
  marginBottom: 16,
3510
3575
  } },
3511
- this.dispatchTeis &&
3512
- (isButtonLoading ? (React__default.createElement(CircularLoader$1, { small: true })) : (React__default.createElement(Button$1, { style: {
3513
- backgroundColor: '#3b82f6', // Blue color
3576
+ valueMatch &&
3577
+ this.dispatchTeis &&
3578
+ (isButtonLoading ? (React__default.createElement(CircularLoader$1, { small: true })) : (
3579
+ //TODO: this implementation should be done where the library will be consumed
3580
+ React__default.createElement(Button$1, { style: {
3581
+ backgroundColor: '#3b82f6',
3514
3582
  color: 'white',
3515
3583
  padding: '8px 16px',
3516
3584
  borderRadius: '4px',
@@ -3559,7 +3627,7 @@ class LineListTableComponent extends ReactWrapperModule {
3559
3627
  React__default.createElement(DataTableRow, null, columns.map((col) => (React__default.createElement(DataTableColumnHeader, { key: col.key }, col.label))))),
3560
3628
  React__default.createElement(TableBody, null, data.length > 0 ? (data.map((row) => (React__default.createElement(DataTableRow, { key: row.index }, columns.map((col) => (React__default.createElement(DataTableCell, { key: col.key }, col.key === 'actions' ? (React__default.createElement(DropdownMenu$1, { dropdownOptions: getDropdownOptions(row), onClick: (option) => option.onClick?.() })) : (row[col.key] || '--' // Display '--' if value is undefined or null
3561
3629
  )))))))) : (React__default.createElement(DataTableRow, null,
3562
- React__default.createElement(DataTableCell, { colSpan: columns.length, style: {
3630
+ React__default.createElement(DataTableCell, { colSpan: columns.length?.toString(), style: {
3563
3631
  textAlign: 'center',
3564
3632
  fontWeight: 'bold',
3565
3633
  color: 'grey',
@@ -3612,7 +3680,7 @@ class LineListTableComponent extends ReactWrapperModule {
3612
3680
  this.render();
3613
3681
  }
3614
3682
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.8", ngImport: i0, type: LineListTableComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
3615
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.8", type: LineListTableComponent, isStandalone: false, selector: "app-line-list", inputs: { programId: "programId", orgUnit: "orgUnit", programStageId: "programStageId", actionOptions: "actionOptions", attributeFilters: "attributeFilters", startDate: "startDate", endDate: "endDate", filters: "filters", ouMode: "ouMode", dispatchTeis: "dispatchTeis", showFilterButton: "showFilterButton", isButtonLoading: "isButtonLoading", buttonLabel: "buttonLabel" }, outputs: { actionSelected: "actionSelected", approvalSelected: "approvalSelected", firstValue: "firstValue" }, usesInheritance: true, usesOnChanges: true, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: ["[data-test=dhis2-uiwidgets-pagination]{display:flex!important;flex-direction:row!important;justify-content:space-between!important;align-items:center!important}\n"] }); }
3683
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.8", type: LineListTableComponent, isStandalone: false, selector: "app-line-list", inputs: { programId: "programId", orgUnit: "orgUnit", programStageId: "programStageId", actionOptions: "actionOptions", attributeFilters: "attributeFilters", startDate: "startDate", endDate: "endDate", filters: "filters", ouMode: "ouMode", dispatchTeis: "dispatchTeis", showFilterButton: "showFilterButton", isButtonLoading: "isButtonLoading", buttonLabel: "buttonLabel", buttonFilter: "buttonFilter" }, outputs: { actionSelected: "actionSelected", approvalSelected: "approvalSelected", firstValue: "firstValue" }, usesInheritance: true, usesOnChanges: true, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: ["[data-test=dhis2-uiwidgets-pagination]{display:flex!important;flex-direction:row!important;justify-content:space-between!important;align-items:center!important}\n"] }); }
3616
3684
  }
3617
3685
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.8", ngImport: i0, type: LineListTableComponent, decorators: [{
3618
3686
  type: Component,
@@ -3649,6 +3717,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.8", ngImpor
3649
3717
  type: Input
3650
3718
  }], firstValue: [{
3651
3719
  type: Output
3720
+ }], buttonFilter: [{
3721
+ type: Input
3652
3722
  }] } });
3653
3723
 
3654
3724
  class LineListModule {
@@ -3818,7 +3888,7 @@ class DataTableUIComponent extends ReactWrapperModule {
3818
3888
  React__default.createElement(DropdownMenu, { dropdownOptions: getDropdownOptions(row), onClick: (option) => option.onClick?.(row) }))))))),
3819
3889
  React__default.createElement(TableFoot, null,
3820
3890
  React__default.createElement(DataTableRow, null,
3821
- React__default.createElement(DataTableCell, { colSpan: columns.length + 1 })))))));
3891
+ React__default.createElement(DataTableCell, { colSpan: (columns.length || 0 + 1).toString() })))))));
3822
3892
  };
3823
3893
  }
3824
3894
  async ngAfterViewInit() {
@@ -4801,6 +4871,289 @@ class FormValue {
4801
4871
  // Use of this source code is governed by a BSD-style
4802
4872
  // license that can be found in the LICENSE file.
4803
4873
 
4874
+ // Copyright 2025 UDSM DHIS2 Lab. All rights reserved.
4875
+ // Use of this source code is governed by a BSD-style
4876
+ // license that can be found in the LICENSE file.
4877
+ const useFieldValidation = (props) => {
4878
+ const { form, touched, field, value } = props;
4879
+ const hasError = useMemo(() => {
4880
+ return touched && (form.get(field.id) || form.get(field.key))?.invalid;
4881
+ }, [value, touched]);
4882
+ const validationError = useMemo(() => {
4883
+ if (!touched) {
4884
+ return undefined;
4885
+ }
4886
+ const error = (form.get(field.id) || form.get(field.key))?.errors;
4887
+ if (!error) {
4888
+ return undefined;
4889
+ }
4890
+ if (error['min']) {
4891
+ return `${field.label || 'Value'} should not be less than ${error['min'].min}!`;
4892
+ }
4893
+ else if (error['max']) {
4894
+ return `${field.label || 'Value'} should not be greater than ${error['max'].max}!`;
4895
+ }
4896
+ else if (error['pattern']) {
4897
+ if (field.type === 'email') {
4898
+ return `The email address provided is not valid`;
4899
+ }
4900
+ else if (field.type === 'tel') {
4901
+ return 'Phone number provided is not valid';
4902
+ }
4903
+ else {
4904
+ return `${field.label || 'Value'} should have appropriate format`;
4905
+ }
4906
+ }
4907
+ return error['required']
4908
+ ? `${field.label || 'Value'} is required`
4909
+ : undefined;
4910
+ }, [hasError]);
4911
+ return { hasError, validationError };
4912
+ };
4913
+
4914
+ // Copyright 2025 UDSM DHIS2 Lab. All rights reserved.
4915
+ // Use of this source code is governed by a BSD-style
4916
+ // license that can be found in the LICENSE file.
4917
+
4918
+ // Copyright 2025 UDSM DHIS2 Lab. All rights reserved.
4919
+ // Use of this source code is governed by a BSD-style
4920
+ // license that can be found in the LICENSE file.
4921
+ // @flow
4922
+ const orgUnitFieldStyles = {
4923
+ container: {
4924
+ border: '1px solid',
4925
+ borderColor: colors$1.grey400,
4926
+ borderRadius: '3px',
4927
+ height: '100%',
4928
+ width: '100%',
4929
+ zIndex: 0,
4930
+ backgroundColor: 'white',
4931
+ },
4932
+ searchField: {
4933
+ height: '40px',
4934
+ overflow: 'hidden',
4935
+ borderBottomLeftRadius: 0,
4936
+ borderBottomRightRadius: 0,
4937
+ boxShadow: 'none',
4938
+ border: 'none',
4939
+ },
4940
+ debounceFieldContainer: {
4941
+ padding: '8px',
4942
+ background: colors$1.grey100,
4943
+ borderTopLeftRadius: '3px',
4944
+ borderTopRightRadius: '3px',
4945
+ },
4946
+ orgUnitTreeContainer: {
4947
+ borderBottomLeftRadius: '3px',
4948
+ borderBottomRightRadius: '3px',
4949
+ overflow: 'auto',
4950
+ },
4951
+ orgUnitTreeLoader: {
4952
+ padding: '8px',
4953
+ },
4954
+ orgUnitFieldLabel: {
4955
+ marginBottom: '8px',
4956
+ display: 'block',
4957
+ boxSizing: 'border-box',
4958
+ fontSize: '14px',
4959
+ lineHeight: '19px',
4960
+ color: 'rgb(33, 41, 52)',
4961
+ padding: '0',
4962
+ },
4963
+ orgUnitFieldRequiredLabel: {
4964
+ paddingInlineStart: '4px',
4965
+ },
4966
+ };
4967
+ const OrgUnitFormField = (props) => {
4968
+ const { onSelectOrgUnit, label, required, selected, disabled, key } = props;
4969
+ const d2 = window?.d2Web;
4970
+ const classes = useDynamicStyles$2(orgUnitFieldStyles);
4971
+ const config = useMemo(() => {
4972
+ return d2.systemInfo?.toInitObject();
4973
+ }, []);
4974
+ const rootOrgUnits = useMemo(() => {
4975
+ return d2.currentUser?.organisationUnits || [];
4976
+ }, []);
4977
+ const getExpandedItems = () => {
4978
+ if (rootOrgUnits && rootOrgUnits.length === 1) {
4979
+ return [`/${rootOrgUnits[0].id}`];
4980
+ }
4981
+ else if (rootOrgUnits?.length > 1) {
4982
+ return rootOrgUnits.map((root) => root.path);
4983
+ }
4984
+ return undefined;
4985
+ };
4986
+ const initiallyExpanded = getExpandedItems();
4987
+ const [searchText, setSearchText] = useState(undefined);
4988
+ const [loading, setLoading] = useState(true);
4989
+ const [searchLoading, setSearchLoading] = useState();
4990
+ const [searchData, setSearchData] = useState();
4991
+ const [selectedOrgUnit, setSelectedOrgUnit] = React__default.useState();
4992
+ const [expanded, setExpanded] = React__default.useState(initiallyExpanded);
4993
+ const [showOrgUnitTree, setShowOrgUnitTree] = useState(!selected);
4994
+ useEffect(() => {
4995
+ setLoading(true);
4996
+ if (selected) {
4997
+ d2.httpInstance
4998
+ .get(`organisationUnits/${selected}.json?fields=id,name,path,code,ancestors[id,name]`)
4999
+ .then((response) => {
5000
+ if (response.data) {
5001
+ setSelectedOrgUnit(response.data);
5002
+ handleExpand({ path: response.data['path'] });
5003
+ setLoading(false);
5004
+ }
5005
+ });
5006
+ }
5007
+ else {
5008
+ setLoading(false);
5009
+ }
5010
+ }, [selected]);
5011
+ const highlighted = useMemo(() => {
5012
+ if (selectedOrgUnit) {
5013
+ return [selectedOrgUnit['path']];
5014
+ }
5015
+ return undefined;
5016
+ }, [selectedOrgUnit]);
5017
+ const selectedOrgUnitLabel = useMemo(() => {
5018
+ if (selectedOrgUnit) {
5019
+ return [
5020
+ ...(selectedOrgUnit['ancestors'] || []),
5021
+ selectedOrgUnit,
5022
+ ]
5023
+ .map((orgUnit) => orgUnit['name'])
5024
+ .join(' / ');
5025
+ }
5026
+ return '';
5027
+ }, [selectedOrgUnit]);
5028
+ const handleExpand = ({ path }) => {
5029
+ if (expanded && !expanded.includes(path)) {
5030
+ setExpanded([...expanded, path]);
5031
+ }
5032
+ };
5033
+ const handleCollapse = ({ path }) => {
5034
+ const pathIndex = expanded?.indexOf(path);
5035
+ if (pathIndex && pathIndex !== -1 && expanded) {
5036
+ const updatedExpanded = pathIndex === 0
5037
+ ? expanded.slice(1)
5038
+ : [...expanded.slice(0, pathIndex), ...expanded.slice(pathIndex + 1)];
5039
+ setExpanded(updatedExpanded);
5040
+ }
5041
+ };
5042
+ const handleOrgUnitSearch = async (query) => {
5043
+ setSearchLoading(true);
5044
+ try {
5045
+ const searchResponse = await d2.httpInstance.get(`organisationUnits.json?fields=id,displayName,path,publicAccess,access,lastUpdated,children[id,displayName,publicAccess,access,path,children::isNotEmpty]&paging=true&query=${query}&withinUserSearchHierarchy=true&pageSize=15`);
5046
+ setSearchData((searchResponse.data || {})['organisationUnits']);
5047
+ setSearchLoading(false);
5048
+ }
5049
+ catch (e) {
5050
+ setSearchLoading(false);
5051
+ }
5052
+ };
5053
+ const debouncedSearch = useCallback(debounce((value) => {
5054
+ handleOrgUnitSearch(value);
5055
+ }, 500), []);
5056
+ const handleFilterChange = (event) => {
5057
+ setSearchText(event.value);
5058
+ debouncedSearch(event.value);
5059
+ };
5060
+ const ready = useMemo(() => {
5061
+ return searchText?.length ? !searchLoading : !loading;
5062
+ }, [searchText, searchLoading, loading]);
5063
+ const renderOrgUnitTree = () => {
5064
+ if (searchText && searchText?.length > 0) {
5065
+ if (searchLoading) {
5066
+ return (React__default.createElement("div", { className: classes.orgUnitTreeLoader },
5067
+ React__default.createElement(CircularLoader$1, { small: true })));
5068
+ }
5069
+ return (ready && (React__default.createElement(OrganisationUnitTree, { key: `${key}-search`, roots: searchData?.map((orgUnit) => orgUnit.id) || [], singleSelection: true, expanded: expanded, handleExpand: handleExpand, handleCollapse: handleCollapse, selected: highlighted, onChange: (event) => {
5070
+ setSelectedOrgUnit(event);
5071
+ onSelectOrgUnit(event.id);
5072
+ } })));
5073
+ }
5074
+ return (React__default.createElement(OrganisationUnitTree, { key: key, roots: rootOrgUnits?.map((orgUnit) => orgUnit.id) || [], singleSelection: true, expanded: expanded, handleExpand: handleExpand, handleCollapse: handleCollapse, selected: highlighted, onChange: (event) => {
5075
+ setSelectedOrgUnit(event);
5076
+ onSelectOrgUnit(event.id);
5077
+ } }));
5078
+ };
5079
+ const handleBlur = () => {
5080
+ // onBlur && onBlur(null);
5081
+ };
5082
+ return (config && (React__default.createElement(Provider, { config: config, plugin: false, parentAlertsAdd: undefined, showAlertsInPlugin: false },
5083
+ React__default.createElement("div", null,
5084
+ label && (React__default.createElement("div", { className: classes.orgUnitFieldLabel },
5085
+ React__default.createElement("span", null, label),
5086
+ required ? (React__default.createElement("span", { className: classes.orgUnitFieldRequiredLabel }, "*")) : (React__default.createElement(React__default.Fragment, null)))),
5087
+ selected && !showOrgUnitTree ? (selectedOrgUnit ? (React__default.createElement(Tooltip, { content: selectedOrgUnitLabel },
5088
+ React__default.createElement(Chip, { onRemove: () => {
5089
+ setShowOrgUnitTree(true);
5090
+ } }, selectedOrgUnit.name))) : (React__default.createElement("div", { className: classes.orgUnitTreeLoader },
5091
+ React__default.createElement(CircularLoader$1, { small: true })))) : (React__default.createElement("div", { className: classes.container },
5092
+ React__default.createElement("div", { className: classes.debounceFieldContainer },
5093
+ React__default.createElement(InputField$1, { placeholder: "Search", value: searchText, disabled: disabled, onChange: handleFilterChange })),
5094
+ loading ? (React__default.createElement("div", { className: classes.orgUnitTreeLoader },
5095
+ React__default.createElement(CircularLoader$1, { small: true }))) : (React__default.createElement("div", { className: classes.orgUnitTreeContainer, style: { maxHeight: '200px', overflowY: 'auto' } }, renderOrgUnitTree()))))))));
5096
+ };
5097
+
5098
+ // Copyright 2025 UDSM DHIS2 Lab. All rights reserved.
5099
+ // Use of this source code is governed by a BSD-style
5100
+ // license that can be found in the LICENSE file.
5101
+ const FileUploadField = (props) => {
5102
+ const { label, id, uploadUrl, performUpload, validationText, required, hasError, onChange, onUploadSuccess, } = props;
5103
+ const d2 = window?.d2Web;
5104
+ const [file, setFile] = useState(null);
5105
+ const [uploading, setUploading] = useState();
5106
+ const valid = useMemo(() => {
5107
+ if (isUndefined(hasError)) {
5108
+ return undefined;
5109
+ }
5110
+ return !hasError;
5111
+ }, [hasError]);
5112
+ const uploadFile = async (fileItem) => {
5113
+ if (fileItem) {
5114
+ setUploading(true);
5115
+ const formData = new FormData();
5116
+ formData.append('file', fileItem);
5117
+ try {
5118
+ const fileUploadResponse = await d2.httpInstance.post(uploadUrl, formData);
5119
+ setUploading(false);
5120
+ const fileId = fileUploadResponse?.data?.response?.fileResource
5121
+ ?.id;
5122
+ if (fileId) {
5123
+ onUploadSuccess(fileId);
5124
+ }
5125
+ }
5126
+ catch (error) {
5127
+ setUploading(false);
5128
+ }
5129
+ }
5130
+ };
5131
+ return (React__default.createElement(FileInputField, { accept: "*", buttonLabel: "Upload a file", label: label, name: id, valid: valid, required: required, validationText: validationText, onChange: (event) => {
5132
+ const fileItem = (event?.files || [])[0];
5133
+ setFile(fileItem);
5134
+ if (performUpload) {
5135
+ uploadFile(fileItem);
5136
+ }
5137
+ else {
5138
+ onChange(file);
5139
+ }
5140
+ // this.ngZone.run(() => {
5141
+ // this.form().get(this.field().id) ||
5142
+ // this.form()
5143
+ // .get(this.field().key)
5144
+ // ?.setValue((event?.files || [])[0]);
5145
+ // this.update.emit({
5146
+ // form: this.form(),
5147
+ // value: (event?.files || [])[0],
5148
+ // });
5149
+ // });
5150
+ // setValue((event?.files || [])[0]);
5151
+ // setTouched(true);
5152
+ } }, file && (React__default.createElement(FileListItem, { cancelText: "Cancel", label: file.name, loading: uploading, onRemove: () => {
5153
+ setFile(null);
5154
+ }, removeText: "Remove" }))));
5155
+ };
5156
+
4804
5157
  // Copyright 2024 UDSM DHIS2 Lab. All rights reserved.
4805
5158
  // Use of this source code is governed by a BSD-style
4806
5159
  // license that can be found in the LICENSE file.
@@ -4890,45 +5243,15 @@ class BaseFormFieldComponent extends ReactWrapperModule {
4890
5243
  }
4891
5244
  return [];
4892
5245
  }, [value]);
4893
- const hasError = useMemo(() => {
4894
- return (touched &&
4895
- (this.form().get(this.field().id) ||
4896
- this.form().get(this.field().key))?.invalid);
4897
- }, [value, touched]);
4898
- const validationError = useMemo(() => {
4899
- if (!touched) {
4900
- return undefined;
4901
- }
4902
- const field = this.field();
4903
- const error = (this.form().get(field.id) || this.form().get(field.key))
4904
- ?.errors;
4905
- if (!error) {
4906
- return undefined;
4907
- }
4908
- if (error['min']) {
4909
- return `${this.label() || 'Value'} should not be less than ${error['min'].min}!`;
4910
- }
4911
- else if (error['max']) {
4912
- return `${this.label() || 'Value'} should not be greater than ${error['max'].max}!`;
4913
- }
4914
- else if (error['pattern']) {
4915
- if (field.type === 'email') {
4916
- return `The email address provided is not valid`;
4917
- }
4918
- else if (field.type === 'tel') {
4919
- return 'Phone number provided is not valid';
4920
- }
4921
- else {
4922
- return `${this.label() || 'Value'} should have appropriate format`;
4923
- }
4924
- }
4925
- return error['required']
4926
- ? `${this.label() || 'Value'} is required`
4927
- : undefined;
4928
- }, [hasError]);
5246
+ const { hasError, validationError } = useFieldValidation({
5247
+ field: this.field(),
5248
+ form: this.form(),
5249
+ value,
5250
+ touched,
5251
+ });
4929
5252
  switch (this.field().controlType) {
4930
5253
  case 'textarea':
4931
- return (React__default.createElement(TextAreaField, { error: hasError, validationText: validationError, type: this.field().type, inputWidth: this.fieldConfig()?.inputWidth, required: this.field().required, name: this.field().id, label: this.label(), min: this.field().min?.toString(), max: this.field().max?.toString(), disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, placeholder: this.placeholder(), value: value, onChange: (event) => {
5254
+ return (React__default.createElement(TextAreaField, { error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, required: this.field().required, name: this.field().id, label: this.label(), rows: 5, placeholder: this.placeholder(), value: value, onChange: (event) => {
4932
5255
  this.ngZone.run(() => {
4933
5256
  (this.form().get(this.field().id) ||
4934
5257
  this.form().get(this.field().key))?.setValue(event.value);
@@ -4945,34 +5268,14 @@ class BaseFormFieldComponent extends ReactWrapperModule {
4945
5268
  });
4946
5269
  } }));
4947
5270
  case 'org-unit':
4948
- return (React__default.createElement(React__default.Fragment, null,
4949
- React__default.createElement(InputField$1, { error: hasError, validationText: validationError, type: this.field().type, prefixIcon: React__default.createElement(IconDimensionOrgUnit16, null), required: this.field().required, name: this.field().id, label: this.label(), placeholder: this.placeholder(), value: displayValue, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, onFocus: () => {
4950
- setShowOrgUnit(true);
4951
- }, onBlur: () => {
4952
- this.ngZone.run(() => {
4953
- this.update.emit({ form: this.form(), value });
4954
- });
4955
- setTouched(true);
4956
- } }),
4957
- showOrgUnit && (React__default.createElement(this.FieldOrgUnitSelector, { selectedOrgUnits: [], onSelectOrgUnit: (selectedOrgUnits) => {
4958
- if ((selectedOrgUnits || [])[0]) {
4959
- const selectedOrgUnit = selectedOrgUnits[0];
4960
- setDisplayValue(selectedOrgUnit.name);
4961
- setValue(selectedOrgUnit.id);
4962
- setShowOrgUnit(false);
4963
- setTouched(true);
4964
- this.ngZone.run(() => {
4965
- (this.form().get(this.field().id) ||
4966
- this.form().get(this.field().key))?.setValue(selectedOrgUnit.id);
4967
- this.immediateUpdate.emit({
4968
- form: this.form(),
4969
- value: selectedOrgUnit.id,
4970
- });
4971
- });
4972
- }
4973
- } }))));
5271
+ return (React__default.createElement(OrgUnitFormField, { label: this.label(), key: this.field().id, required: this.field().required, onSelectOrgUnit: (selectedOrgUnit) => {
5272
+ setValue(selectedOrgUnit);
5273
+ this.ngZone.run(() => {
5274
+ this.update.emit({ form: this.form(), value });
5275
+ });
5276
+ }, selected: value }));
4974
5277
  case 'transfer':
4975
- return (React__default.createElement(Transfer, { filterable: true, filterPlaceholder: "Search", selected: selected, error: hasError, validationText: validationError, type: this.field().type, inputWidth: this.fieldConfig()?.inputWidth, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, required: this.field().required, name: this.field().id, label: this.label(), placeholder: this.placeholder(), value: selected, leftHeader: React__default.createElement("div", { style: {
5278
+ return (React__default.createElement(Transfer, { filterable: true, filterPlaceholder: "Search", selected: selected, leftHeader: React__default.createElement("div", { style: {
4976
5279
  fontSize: 14,
4977
5280
  padding: '8px 4px',
4978
5281
  } }, this.field().availableOptionsLabel), rightHeader: React__default.createElement("div", { style: {
@@ -4989,7 +5292,10 @@ class BaseFormFieldComponent extends ReactWrapperModule {
4989
5292
  this.update.emit({ form: this.form(), value });
4990
5293
  } }));
4991
5294
  case 'checkbox':
4992
- return (React__default.createElement(Checkbox, { checked: Boolean(value), error: hasError, label: this.label(), name: this.field().id, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, onChange: (event) => {
5295
+ return (React__default.createElement(Checkbox, { checked: Boolean(value), error: hasError, label: this.label(), name: this.field().id, disabled: this.field()?.disabled ||
5296
+ this.field()?.generated ||
5297
+ this.field()?.unique ||
5298
+ false, onChange: (event) => {
4993
5299
  this.ngZone.run(() => {
4994
5300
  (this.form().get(this.field().id) ||
4995
5301
  this.form().get(this.field().key))?.setValue(event.checked);
@@ -5002,7 +5308,10 @@ class BaseFormFieldComponent extends ReactWrapperModule {
5002
5308
  });
5003
5309
  } }));
5004
5310
  case 'dropdown':
5005
- return (React__default.createElement(SingleSelectField, { filterable: (this.field().options || []).length > 5, clearable: true, error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, required: this.field().required, name: this.field().id, className: "select", label: this.label(), selected: value, onChange: (event) => {
5311
+ return (React__default.createElement(SingleSelectField, { filterable: (this.field().options || []).length > 5, clearable: true, error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, disabled: this.field()?.disabled ||
5312
+ this.field()?.generated ||
5313
+ this.field()?.unique ||
5314
+ false, required: this.field().required, className: "select", label: this.label(), selected: value, onChange: (event) => {
5006
5315
  this.ngZone.run(() => {
5007
5316
  (this.form().get(this.field().id) ||
5008
5317
  this.form().get(this.field().key))?.setValue(event.selected);
@@ -5015,7 +5324,10 @@ class BaseFormFieldComponent extends ReactWrapperModule {
5015
5324
  setTouched(true);
5016
5325
  } }, (this.field().options || []).map((option) => (React__default.createElement(SingleSelectOption, { key: crypto.randomUUID() || option.key, label: option.label, value: option.value })))));
5017
5326
  case 'multi-dropdown':
5018
- return (React__default.createElement(MultiSelectField, { clearText: "Clear", clearable: true, empty: "No data found", filterable: (this.field().options || []).length > 5, filterPlaceholder: "Type to filter options", error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, label: this.label(), name: this.field().id, required: this.field().required, loadingText: "Loading options", noMatchText: "No options found", onChange: (event) => {
5327
+ return (React__default.createElement(MultiSelectField, { clearText: "Clear", clearable: true, empty: "No data found", filterable: (this.field().options || []).length > 5, filterPlaceholder: "Type to filter options", error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, disabled: this.field()?.disabled ||
5328
+ this.field()?.generated ||
5329
+ this.field()?.unique ||
5330
+ false, label: this.label(), required: this.field().required, loadingText: "Loading options", noMatchText: "No options found", onChange: (event) => {
5019
5331
  const selectedValue = (event.selected || []).join(',');
5020
5332
  this.ngZone.run(() => {
5021
5333
  (this.form().get(this.field().id) ||
@@ -5029,29 +5341,27 @@ class BaseFormFieldComponent extends ReactWrapperModule {
5029
5341
  setTouched(true);
5030
5342
  }, selected: arrayValue }, (this.field().options || []).map((option) => (React__default.createElement(MultiSelectOption, { key: option.key, label: option.label, value: option.value })))));
5031
5343
  case 'file':
5032
- return (React__default.createElement(FileInputField, { accept: "*", buttonLabel: "Upload a file", label: this.label(), name: this.field().id, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, files: this.uploadedFiles, onChange: (event) => {
5033
- this.uploadedFiles.push((event?.files || [])[0]);
5034
- this.ngZone.run(() => {
5035
- this.form().get(this.field().id) ||
5036
- this.form()
5037
- .get(this.field().key)
5038
- ?.setValue((event?.files || [])[0]);
5039
- this.update.emit({
5040
- form: this.form(),
5041
- value: (event?.files || [])[0],
5344
+ return (React__default.createElement(React__default.Fragment, null,
5345
+ React__default.createElement(FileUploadField, { label: this.label(), id: this.field().id, hasError: hasError, required: this.field().required, validationText: validationError, performUpload: true, uploadUrl: 'fileResources', onUploadSuccess: (fileId) => {
5346
+ this.ngZone.run(() => {
5347
+ this.form().get(this.field().id) ||
5348
+ this.form().get(this.field().key)?.setValue(fileId);
5349
+ this.update.emit({
5350
+ form: this.form(),
5351
+ value: fileId,
5352
+ });
5042
5353
  });
5043
- });
5044
- setValue((event?.files || [])[0]);
5045
- setTouched(true);
5046
- } }, this.uploadedFiles.map((file, index) => (React__default.createElement(FileListItem, { key: index, cancelText: "Cancel", label: file.name, onCancel: () => { }, onRemove: () => {
5047
- this.ngZone.run(() => {
5048
- this.uploadedFiles.splice(index, 1);
5049
- setValue(this.uploadedFiles);
5354
+ setValue(fileId);
5050
5355
  setTouched(true);
5051
- });
5052
- }, removeText: "Remove" })))));
5356
+ } })));
5053
5357
  default:
5054
- return (React__default.createElement(InputField$1, { error: hasError, validationText: validationError, type: this.field().type, inputWidth: this.fieldConfig()?.inputWidth, required: this.field().required, name: this.field().id, label: this.label(), min: this.field().min?.toString(), max: this.field().max?.toString(), placeholder: this.placeholder(), value: value, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, onChange: (event) => {
5358
+ return (React__default.createElement(InputField$1, { error: hasError, validationText: validationError, type: this.field().type, inputWidth: this.fieldConfig()?.inputWidth, required: this.field().required, name: this.field().id, label: this.label(), min: this.field().min?.toString(), max: this.field().max?.toString(), placeholder: this.placeholder(), value: value, disabled: this.field()?.disabled ||
5359
+ this.field()?.generated ||
5360
+ this.field()?.unique ||
5361
+ false, readOnly: this.field()?.disabled ||
5362
+ this.field()?.generated ||
5363
+ this.field()?.unique ||
5364
+ false, onChange: (event) => {
5055
5365
  this.ngZone.run(() => {
5056
5366
  (this.form().get(this.field().id) ||
5057
5367
  this.form().get(this.field().key))?.setValue(event.value);
@@ -5881,8 +6191,8 @@ class SelectionFiltersComponent extends ReactWrapperModule {
5881
6191
  setTouched(true);
5882
6192
  }
5883
6193
  } }))),
5884
- React__default.createElement(InputField$1, { className: "input-field", label: "Start Date", type: "DATE", value: filters.startDate, onChange: (event) => setFilters({ ...filters, startDate: event.value }), style: { width: '100%', height: '100%' } }),
5885
- React__default.createElement(InputField$1, { className: "input-field", label: "End Date", type: "DATE", value: filters.endDate, onChange: (event) => setFilters({ ...filters, endDate: event.value }), style: { width: '100%', height: '100%' } }),
6194
+ React__default.createElement(InputField$1, { className: "input-field", label: "Start Date", type: "date", value: filters.startDate, onChange: (event) => setFilters({ ...filters, startDate: event.value }) }),
6195
+ React__default.createElement(InputField$1, { className: "input-field", label: "End Date", type: "date", value: filters.endDate, onChange: (event) => setFilters({ ...filters, endDate: event.value }) }),
5886
6196
  React__default.createElement("div", { style: {
5887
6197
  display: 'flex',
5888
6198
  flexDirection: 'column',
@@ -5901,12 +6211,12 @@ class SelectionFiltersComponent extends ReactWrapperModule {
5901
6211
  display: 'grid',
5902
6212
  gridTemplateColumns: 'repeat(4, 1fr)',
5903
6213
  gap: '16px',
5904
- } }, filters.programAttributesFilters.map((filter, index) => (React__default.createElement("div", { key: index, style: { gridColumn: getAttributeColumnSpan(index) } }, filter.hasOptions && filter.options.length > 0 ? (React__default.createElement(SingleSelectField, { className: "single-select", label: filter.name || 'Select Option', type: filter.valueType, selected: filter.value, onChange: (event) => handleAttributeChange(index, event.selected) }, filter.options.map((option, i) => (React__default.createElement(SingleSelectOption, { key: crypto.randomUUID() || i, label: option.name, value: option.code }))))) : (React__default.createElement(InputField$1, { className: "input-field", label: filter.name || 'Program name', type: filter.valueType, value: filter.value, onChange: (event) => handleAttributeChange(index, event.value) })))))),
6214
+ } }, filters.programAttributesFilters.map((filter, index) => (React__default.createElement("div", { key: index, style: { gridColumn: getAttributeColumnSpan(index) } }, filter.hasOptions && filter.options.length > 0 ? (React__default.createElement(SingleSelectField, { className: "single-select", label: filter.name || 'Select Option', selected: filter.value, onChange: (event) => handleAttributeChange(index, event.selected) }, filter.options.map((option, i) => (React__default.createElement(SingleSelectOption, { key: crypto.randomUUID() || i, label: option.name, value: option.code }))))) : (React__default.createElement(InputField$1, { className: "input-field", label: filter.name || 'Program name', type: filter.valueType, value: filter.value, onChange: (event) => handleAttributeChange(index, event.value) })))))),
5905
6215
  React__default.createElement("div", { style: {
5906
6216
  display: 'grid',
5907
6217
  gridTemplateColumns: 'repeat(4, 1fr)',
5908
6218
  gap: '16px',
5909
- } }, filters.programStageDataElementFilters.map((filter, index) => (React__default.createElement("div", { key: index, style: { gridColumn: getDataElementColumnSpan(index) } }, filter.hasOptions && filter.options.length > 0 ? (React__default.createElement(SingleSelectField, { className: "single-select", label: filter.name || 'Aggregation type', type: filter.valueType, selected: filter.value, onChange: (event) => handleDataElementChange(index, event.selected) }, filter.options.map((option, i) => (React__default.createElement(SingleSelectOption, { key: crypto.randomUUID() || i, label: option.name, value: option.code }))))) : (React__default.createElement(InputField$1, { className: "input-field", type: filter.valueType, label: filter.name || 'Program name', value: filter.value, onChange: (event) => handleDataElementChange(index, event.value) })))))),
6219
+ } }, filters.programStageDataElementFilters.map((filter, index) => (React__default.createElement("div", { key: index, style: { gridColumn: getDataElementColumnSpan(index) } }, filter.hasOptions && filter.options.length > 0 ? (React__default.createElement(SingleSelectField, { className: "single-select", label: filter.name || 'Aggregation type', selected: filter.value, onChange: (event) => handleDataElementChange(index, event.selected) }, filter.options.map((option, i) => (React__default.createElement(SingleSelectOption, { key: crypto.randomUUID() || i, label: option.name, value: option.code }))))) : (React__default.createElement(InputField$1, { className: "input-field", type: filter.valueType, label: filter.name || 'Program name', value: filter.value, onChange: (event) => handleDataElementChange(index, event.value) })))))),
5910
6220
  React__default.createElement("div", { style: { textAlign: 'right', marginTop: '16px' } },
5911
6221
  React__default.createElement(Button$1, { onClick: handleSearch }, "Search"))))));
5912
6222
  };