@openmrs/esm-reports-app 4.0.6-pre.161

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.
Files changed (103) hide show
  1. package/.turbo/turbo-build.log +36 -0
  2. package/README.md +23 -0
  3. package/dist/126.js +1 -0
  4. package/dist/126.js.map +1 -0
  5. package/dist/144.js +2 -0
  6. package/dist/144.js.LICENSE.txt +19 -0
  7. package/dist/144.js.map +1 -0
  8. package/dist/156.js +1 -0
  9. package/dist/156.js.map +1 -0
  10. package/dist/202.js +2 -0
  11. package/dist/202.js.LICENSE.txt +35 -0
  12. package/dist/202.js.map +1 -0
  13. package/dist/216.js +1 -0
  14. package/dist/216.js.map +1 -0
  15. package/dist/297.js +2 -0
  16. package/dist/297.js.LICENSE.txt +32 -0
  17. package/dist/297.js.map +1 -0
  18. package/dist/300.js +1 -0
  19. package/dist/304.js +1 -0
  20. package/dist/304.js.map +1 -0
  21. package/dist/322.js +1 -0
  22. package/dist/322.js.map +1 -0
  23. package/dist/336.js +1 -0
  24. package/dist/336.js.map +1 -0
  25. package/dist/353.js +1 -0
  26. package/dist/353.js.map +1 -0
  27. package/dist/367.js +2 -0
  28. package/dist/367.js.LICENSE.txt +5 -0
  29. package/dist/367.js.map +1 -0
  30. package/dist/41.js +2 -0
  31. package/dist/41.js.LICENSE.txt +9 -0
  32. package/dist/41.js.map +1 -0
  33. package/dist/549.js +1 -0
  34. package/dist/549.js.map +1 -0
  35. package/dist/553.js +2 -0
  36. package/dist/553.js.LICENSE.txt +14 -0
  37. package/dist/553.js.map +1 -0
  38. package/dist/640.js +1 -0
  39. package/dist/640.js.map +1 -0
  40. package/dist/699.js +1 -0
  41. package/dist/699.js.map +1 -0
  42. package/dist/855.js +2 -0
  43. package/dist/855.js.LICENSE.txt +9 -0
  44. package/dist/855.js.map +1 -0
  45. package/dist/926.js +1 -0
  46. package/dist/926.js.map +1 -0
  47. package/dist/947.js +1 -0
  48. package/dist/947.js.map +1 -0
  49. package/dist/972.js +2 -0
  50. package/dist/972.js.LICENSE.txt +14 -0
  51. package/dist/972.js.map +1 -0
  52. package/dist/main.js +1 -0
  53. package/dist/main.js.map +1 -0
  54. package/dist/openmrs-esm-reports-app.js +1 -0
  55. package/dist/openmrs-esm-reports-app.js.buildmanifest.json +657 -0
  56. package/dist/openmrs-esm-reports-app.js.map +1 -0
  57. package/dist/routes.json +1 -0
  58. package/jest.config.js +3 -0
  59. package/package.json +57 -0
  60. package/src/components/edit-scheduled-report/edit-scheduled-report-form.component.tsx +149 -0
  61. package/src/components/edit-scheduled-report/edit-scheduled-report-form.scss +60 -0
  62. package/src/components/next-report-execution.component.tsx +31 -0
  63. package/src/components/overlay.component.tsx +53 -0
  64. package/src/components/overlay.scss +95 -0
  65. package/src/components/overview.component.tsx +356 -0
  66. package/src/components/pagination-constants.ts +3 -0
  67. package/src/components/report-overview-button.component.tsx +27 -0
  68. package/src/components/report-parameter-input.component.tsx +121 -0
  69. package/src/components/report-schedule-description.component.tsx +21 -0
  70. package/src/components/report-status.component.tsx +68 -0
  71. package/src/components/report-statuses-constants.ts +11 -0
  72. package/src/components/reports.resource.tsx +224 -0
  73. package/src/components/reports.scss +90 -0
  74. package/src/components/run-report/cancel-report-modal.component.tsx +129 -0
  75. package/src/components/run-report/run-report-form.component.tsx +241 -0
  76. package/src/components/run-report/run-report-form.scss +87 -0
  77. package/src/components/scheduled-overview-cell-content.component.tsx +85 -0
  78. package/src/components/scheduled-overview.component.tsx +118 -0
  79. package/src/components/scheduled-report-status.component.tsx +25 -0
  80. package/src/components/simple-cron-editor/commons.ts +47 -0
  81. package/src/components/simple-cron-editor/cron-date-picker.component.tsx +67 -0
  82. package/src/components/simple-cron-editor/cron-day-of-month-select.component.tsx +74 -0
  83. package/src/components/simple-cron-editor/cron-day-of-week-select.component.tsx +67 -0
  84. package/src/components/simple-cron-editor/cron-time-picker.component.tsx +60 -0
  85. package/src/components/simple-cron-editor/simple-cron-editor.component.tsx +342 -0
  86. package/src/components/simple-cron-editor/simple-cron-editor.scss +34 -0
  87. package/src/constants.ts +3 -0
  88. package/src/declarations.d.ts +6 -0
  89. package/src/hooks/useOverlay.tsx +45 -0
  90. package/src/index.ts +73 -0
  91. package/src/reports-link.tsx +22 -0
  92. package/src/reports.component.tsx +21 -0
  93. package/src/root.scss +15 -0
  94. package/src/routes.json +40 -0
  95. package/src/setup-tests.ts +1 -0
  96. package/src/types/report-definition.ts +12 -0
  97. package/src/types/report-design.ts +4 -0
  98. package/src/types/report-request.ts +8 -0
  99. package/src/utils/openmrs-cron-utils.ts +26 -0
  100. package/src/utils/time-utils.ts +43 -0
  101. package/translations/en.json +86 -0
  102. package/tsconfig.json +5 -0
  103. package/webpack.config.js +1 -0
@@ -0,0 +1,87 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .tablet {
6
+ padding: layout.$spacing-06 layout.$spacing-05;
7
+ background-color: $ui-02;
8
+ }
9
+
10
+ .desktop {
11
+ padding: 0rem;
12
+ }
13
+
14
+ .desktopRunReport {
15
+ background-color: $ui-background;
16
+ display: flex;
17
+ flex-direction: column;
18
+ justify-content: space-between;
19
+ }
20
+
21
+ .runReportInnerDivElement {
22
+ display: flex;
23
+ padding: layout.$spacing-05;
24
+ flex-direction: column;
25
+ align-items: flex-start;
26
+ gap: layout.$spacing-05;
27
+ }
28
+
29
+ .basicInputElement {
30
+ width: 300px;
31
+ height: 30px;
32
+ margin-bottom: 30px;
33
+ }
34
+
35
+ .outputFormatDiv {
36
+ margin-bottom: 50px;
37
+ display: flex;
38
+ padding: layout.$spacing-07 layout.$spacing-05 layout.$spacing-05 layout.$spacing-05;
39
+ flex-direction: column;
40
+ align-items: flex-start;
41
+ gap: layout.$spacing-05;
42
+ }
43
+
44
+ .buttonsDiv {
45
+ margin-top: 50px;
46
+ }
47
+
48
+ .parameterElement {
49
+ margin-bottom: 30px;
50
+ margin-top: 30px;
51
+ }
52
+
53
+ .datePicker {
54
+ text-align: left;
55
+ }
56
+
57
+ .cancelActionButton {
58
+ text-decoration: underline;
59
+ color: #525252;
60
+ padding-left: 0px;
61
+ }
62
+
63
+ .unknownParameterTypeSpan {
64
+ text-align: left;
65
+ color: red;
66
+ }
67
+
68
+ .tableIcon {
69
+ display: inline-block;
70
+ margin-left: 10px;
71
+ }
72
+
73
+ .reportButton {
74
+ height: layout.$spacing-10;
75
+ display: flex;
76
+ align-content: flex-start;
77
+ align-items: baseline;
78
+ min-width: 50%;
79
+ }
80
+
81
+ .rowCellEven {
82
+ background-color: #FFFFFF !important;
83
+ }
84
+
85
+ .rowCellOdd {
86
+ background-color: #F4F4F4 !important;
87
+ }
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { Edit, TrashCan } from '@carbon/react/icons';
3
+ import { showModal, userHasAccess, useSession } from '@openmrs/esm-framework';
4
+ import ReportScheduleDescription from './report-schedule-description.component';
5
+ import NextReportExecution from './next-report-execution.component';
6
+ import ReportOverviewButton from './report-overview-button.component';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { closeOverlay, launchOverlay } from '../hooks/useOverlay';
9
+ import styles from './reports.scss';
10
+ import { PRIVILEGE_SYSTEM_DEVELOPER } from '../constants';
11
+ import EditScheduledReportForm from './edit-scheduled-report/edit-scheduled-report-form.component';
12
+ import ScheduledReportStatus from './scheduled-report-status.component';
13
+
14
+ interface ScheduledOverviewCellContentProps {
15
+ cell: { info: { header: string }; value: any };
16
+ mutate: () => void;
17
+ }
18
+
19
+ const ScheduledOverviewCellContent: React.FC<ScheduledOverviewCellContentProps> = ({ cell, mutate }) => {
20
+ const { t } = useTranslation();
21
+ const session = useSession();
22
+
23
+ const renderContent = () => {
24
+ switch (cell.info.header) {
25
+ case 'name':
26
+ return <div>{cell.value?.content ?? cell.value}</div>;
27
+ case 'status':
28
+ return <ScheduledReportStatus hasSchedule={cell.value} />;
29
+ case 'schedule':
30
+ return <ReportScheduleDescription schedule={cell.value} />;
31
+ case 'nextRun':
32
+ return <NextReportExecution schedule={cell.value} currentDate={new Date()} />;
33
+ case 'actions':
34
+ return (
35
+ <div>
36
+ <ReportOverviewButton
37
+ shouldBeDisplayed={userHasAccess(PRIVILEGE_SYSTEM_DEVELOPER, session.user)}
38
+ label={t('edit', 'Edit')}
39
+ icon={() => <Edit size={16} className={styles.actionButtonIcon} />}
40
+ reportRequestUuid={null}
41
+ onClick={() => {
42
+ launchOverlay(
43
+ t('editScheduledReport', 'Edit Scheduled Report'),
44
+ <EditScheduledReportForm
45
+ reportDefinitionUuid={cell.value.reportDefinitionUuid}
46
+ reportRequestUuid={cell.value.reportRequestUuid}
47
+ closePanel={() => {
48
+ closeOverlay();
49
+ mutate();
50
+ }}
51
+ />,
52
+ );
53
+ }}
54
+ />
55
+ <ReportOverviewButton
56
+ shouldBeDisplayed={
57
+ !!cell.value.reportRequestUuid && userHasAccess(PRIVILEGE_SYSTEM_DEVELOPER, session.user)
58
+ }
59
+ label={t('deleteSchedule', 'Delete Schedule')}
60
+ icon={() => <TrashCan size={16} className={styles.actionButtonIcon} />}
61
+ reportRequestUuid={cell.value.reportRequestUuid}
62
+ onClick={() => launchDeleteReportScheduleDialog(cell.value.reportRequestUuid)}
63
+ />
64
+ </div>
65
+ );
66
+ default:
67
+ return <span>{cell.value?.content ?? cell.value}</span>;
68
+ }
69
+ };
70
+
71
+ const launchDeleteReportScheduleDialog = (reportRequestUuid: string) => {
72
+ const dispose = showModal('cancel-report-modal', {
73
+ closeModal: () => {
74
+ dispose();
75
+ mutate();
76
+ },
77
+ reportRequestUuid,
78
+ modalType: 'schedule',
79
+ });
80
+ };
81
+
82
+ return renderContent();
83
+ };
84
+
85
+ export default ScheduledOverviewCellContent;
@@ -0,0 +1,118 @@
1
+ import React, { useState } from 'react';
2
+ import { ExtensionSlot, isDesktop, useLayoutType, usePagination } from '@openmrs/esm-framework';
3
+ import styles from './reports.scss';
4
+ import { useTranslation } from 'react-i18next';
5
+ import {
6
+ DataTable,
7
+ Pagination,
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableContainer,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow,
15
+ } from '@carbon/react';
16
+ import { useScheduledReports } from './reports.resource';
17
+ import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES } from './pagination-constants';
18
+ import ScheduledOverviewCellContent from './scheduled-overview-cell-content.component';
19
+ import Overlay from './overlay.component';
20
+ import classNames from 'classnames';
21
+
22
+ const ScheduledOverviewComponent: React.FC = () => {
23
+ const { t } = useTranslation();
24
+ const layout = useLayoutType();
25
+
26
+ const { scheduledReports, mutateScheduledReports } = useScheduledReports('name');
27
+ const scheduledReportRows = scheduledReports
28
+ ? scheduledReports.map((report) => ({
29
+ id: report.reportRequestUuid ? report.reportRequestUuid : report.reportDefinitionUuid,
30
+ name: report.name,
31
+ status: !!report.reportRequestUuid,
32
+ schedule: report.schedule,
33
+ nextRun: report.schedule,
34
+ actions: {
35
+ reportDefinitionUuid: report.reportDefinitionUuid,
36
+ reportRequestUuid: report.reportRequestUuid,
37
+ },
38
+ }))
39
+ : [];
40
+
41
+ const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
42
+ const { currentPage, results, goTo } = usePagination(scheduledReportRows, pageSize);
43
+
44
+ const tableHeaders = [
45
+ { key: 'name', header: t('reportName', 'Report Name') },
46
+ { key: 'status', header: t('status', 'Status') },
47
+ { key: 'schedule', header: t('schedule', 'Schedule') },
48
+ { key: 'nextRun', header: t('nextRun', 'Next run') },
49
+ { key: 'actions', header: t('actions', 'Actions') },
50
+ ];
51
+
52
+ return (
53
+ <div>
54
+ <ExtensionSlot name="breadcrumbs-slot" className={styles.breadcrumb} />
55
+ <div className={styles.mainPanelDiv}>
56
+ <div className={styles.reportsLabelDiv}>
57
+ <h3>{t('scheduledReports', 'Scheduld Reports')}</h3>
58
+ </div>
59
+ <div className={styles.mainActionButtonsDiv}></div>
60
+ <DataTable rows={results} headers={tableHeaders} isSortable={false}>
61
+ {({ rows, headers }) => (
62
+ <TableContainer>
63
+ <Table>
64
+ <TableHead>
65
+ <TableRow>
66
+ {headers.map((header, index) => (
67
+ <TableHeader key={header.key ?? `header-${index}`}>
68
+ {header.header?.content ?? header.header}
69
+ </TableHeader>
70
+ ))}
71
+ </TableRow>
72
+ </TableHead>
73
+ <TableBody>
74
+ {rows.map((row, index) => (
75
+ <TableRow key={row.id ?? `row-${index}`} className={styles.tableRow}>
76
+ {row.cells.map((cell) => (
77
+ <TableCell
78
+ key={cell.id}
79
+ className={classNames({
80
+ [styles.rowCellEven]: index % 2 === 0,
81
+ [styles.rowCellOdd]: index % 2 !== 0,
82
+ })}
83
+ >
84
+ <ScheduledOverviewCellContent cell={cell} mutate={mutateScheduledReports} />
85
+ </TableCell>
86
+ ))}
87
+ </TableRow>
88
+ ))}
89
+ </TableBody>
90
+ </Table>
91
+ </TableContainer>
92
+ )}
93
+ </DataTable>
94
+ <Pagination
95
+ backwardText={t('previousPage', 'Previous page')}
96
+ forwardText={t('nextPage', 'Next page')}
97
+ page={currentPage}
98
+ pageSize={pageSize}
99
+ pageSizes={DEFAULT_PAGE_SIZES}
100
+ totalItems={scheduledReports?.length}
101
+ size={isDesktop(layout) ? 'sm' : 'lg'}
102
+ onChange={({ pageSize: newPageSize, page: newPage }) => {
103
+ if (newPageSize !== pageSize) {
104
+ setPageSize(newPageSize);
105
+ }
106
+
107
+ if (newPage !== currentPage) {
108
+ goTo(newPage);
109
+ }
110
+ }}
111
+ />
112
+ </div>
113
+ <Overlay />
114
+ </div>
115
+ );
116
+ };
117
+
118
+ export default ScheduledOverviewComponent;
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import styles from './reports.scss';
3
+ import { useTranslation } from 'react-i18next';
4
+ import classNames from 'classnames';
5
+
6
+ interface ReportStatusProps {
7
+ hasSchedule: string;
8
+ }
9
+
10
+ const ScheduledReportStatus: React.FC<ReportStatusProps> = ({ hasSchedule }) => {
11
+ const { t } = useTranslation();
12
+
13
+ return (
14
+ <span
15
+ className={classNames({
16
+ [styles.scheduledStatusText]: hasSchedule,
17
+ [styles.notScheduledStatusText]: !hasSchedule,
18
+ })}
19
+ >
20
+ {hasSchedule ? t('scheduled', 'Scheduled') : t('notScheduled', 'Not Scheduled')}
21
+ </span>
22
+ );
23
+ };
24
+
25
+ export default ScheduledReportStatus;
@@ -0,0 +1,47 @@
1
+ export interface CronField {
2
+ name: string;
3
+ value: number | string;
4
+ }
5
+
6
+ export const ST_ONCE = 'once';
7
+ export const ST_EVERY_DAY = 'everyDay';
8
+ export const ST_EVERY_WEEK = 'everyWeek';
9
+ export const ST_EVERY_MONTH = 'everyMonth';
10
+ export const ST_ADVANCED = 'advanced';
11
+ export const SCHEDULE_TYPES = [ST_ONCE, ST_EVERY_DAY, ST_EVERY_WEEK, ST_EVERY_MONTH, ST_ADVANCED];
12
+ export const SCHEDULE_TYPE_DEFAULT_LABELS: Record<string, string> = {
13
+ once: 'Once',
14
+ everyDay: 'Every day',
15
+ everyWeek: 'Every week',
16
+ everyMonth: 'Every month',
17
+ };
18
+ export const DAYS_OF_WEEK: CronField[] = [
19
+ 'sunday',
20
+ 'monday',
21
+ 'tuesday',
22
+ 'wednesday',
23
+ 'thursday',
24
+ 'friday',
25
+ 'saturday',
26
+ ].map((name, idx) => ({
27
+ name,
28
+ value: idx,
29
+ }));
30
+ export const DAYS_OF_WEEK_DEFAULT_LABELS: Record<string, string> = {
31
+ sunday: 'Sunday',
32
+ monday: 'Monday',
33
+ tuesday: 'Tuesday',
34
+ wednesday: 'Wednesday',
35
+ thursday: 'Thursday',
36
+ friday: 'Friday',
37
+ saturday: 'Saturday',
38
+ };
39
+ export const DAYS_OF_MONTH: CronField[] = [
40
+ { name: '', value: '' },
41
+ { name: 'firstDay', value: '1' },
42
+ { name: 'lastDay', value: 'L' },
43
+ ];
44
+ export const DAYS_OF_MONTH_DEFAULT_LABELS: Record<string, string> = {
45
+ firstDay: 'First day',
46
+ lastDay: 'Last day',
47
+ };
@@ -0,0 +1,67 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { DatePicker, DatePickerInput } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import styles from './simple-cron-editor.scss';
5
+ import { isEqual } from 'lodash-es';
6
+
7
+ interface CronDatePickerProps {
8
+ value: Date;
9
+ onChange: (date: Date) => void;
10
+ }
11
+
12
+ interface ValidationState {
13
+ invalid: boolean;
14
+ invalidText: string;
15
+ }
16
+
17
+ const CronDatePicker: React.FC<CronDatePickerProps> = ({ value, onChange }) => {
18
+ const { t } = useTranslation();
19
+ const [valueInternal, setValueInternal] = useState(value);
20
+ const [validationState, setValidationState] = useState<ValidationState>({
21
+ invalid: false,
22
+ invalidText: null,
23
+ });
24
+
25
+ useEffect(() => {
26
+ if (!isEqual(value, valueInternal)) {
27
+ setValueInternal(value);
28
+ }
29
+ }, [value]);
30
+
31
+ useEffect(() => {
32
+ validate();
33
+ }, [valueInternal]);
34
+
35
+ useEffect(() => {
36
+ onChange(validationState.invalid ? null : valueInternal);
37
+ }, [validationState]);
38
+
39
+ const validate = () => {
40
+ if (!(valueInternal instanceof Date)) {
41
+ setValidationState({ invalid: true, invalidText: t('dateRequired', 'Required') });
42
+ } else {
43
+ setValidationState({ invalid: false, invalidText: null });
44
+ }
45
+ };
46
+
47
+ return (
48
+ <div>
49
+ <DatePicker
50
+ datePickerType="single"
51
+ value={valueInternal}
52
+ invalid={validationState.invalid}
53
+ invalidText={t(validationState.invalidText)}
54
+ onChange={([selectedDate]) => {
55
+ setValueInternal(selectedDate);
56
+ }}
57
+ >
58
+ <DatePickerInput hideLabel={true} />
59
+ </DatePicker>
60
+ {validationState.invalid && (
61
+ <span className={styles.dangerLabel01}>{validationState.invalidText && t(validationState.invalidText)}</span>
62
+ )}
63
+ </div>
64
+ );
65
+ };
66
+
67
+ export default CronDatePicker;
@@ -0,0 +1,74 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { Select, SelectItem } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { type CronField, DAYS_OF_MONTH, DAYS_OF_MONTH_DEFAULT_LABELS } from './commons';
5
+ import { isEqual } from 'lodash-es';
6
+
7
+ interface CronDayOfMonthSelectProps {
8
+ value: CronField;
9
+ onChange: (selectedDayOfMonth: CronField) => void;
10
+ }
11
+
12
+ interface ValidationState {
13
+ invalid: boolean;
14
+ invalidText: string;
15
+ }
16
+
17
+ const CronDayOfMonthSelect: React.FC<CronDayOfMonthSelectProps> = ({ value, onChange }) => {
18
+ const { t } = useTranslation();
19
+
20
+ const [valueInternal, setValueInternal] = useState<number | string>(value?.value);
21
+ const [validationState, setValidationState] = useState<ValidationState>({
22
+ invalid: false,
23
+ invalidText: null,
24
+ });
25
+
26
+ useEffect(() => {
27
+ if (!isEqual(value, valueInternal)) {
28
+ setValueInternal(value?.value);
29
+ }
30
+ }, [value]);
31
+
32
+ useEffect(() => {
33
+ validate();
34
+ }, [valueInternal]);
35
+
36
+ useEffect(() => {
37
+ onChange(validationState.invalid ? null : DAYS_OF_MONTH.find((dayOfMonth) => dayOfMonth.value == valueInternal));
38
+ }, [validationState]);
39
+
40
+ const validate = () => {
41
+ if (!!valueInternal) {
42
+ setValidationState({ invalid: false, invalidText: null });
43
+ } else {
44
+ setValidationState({ invalid: true, invalidText: t('dayOfMonthRequired', 'Required') });
45
+ }
46
+ };
47
+
48
+ const translatedOptions = useMemo(
49
+ () =>
50
+ DAYS_OF_MONTH.map((day) => ({
51
+ ...day,
52
+ label: t(`dayOfMonth_${day.name}`, DAYS_OF_MONTH_DEFAULT_LABELS[day.name] || day.name),
53
+ })),
54
+ [t],
55
+ );
56
+
57
+ return (
58
+ <Select
59
+ hideLabel={true}
60
+ onChange={(event) => {
61
+ setValueInternal(event.target.value);
62
+ }}
63
+ value={valueInternal}
64
+ invalid={validationState.invalid}
65
+ invalidText={t(validationState.invalidText)}
66
+ >
67
+ {translatedOptions.map((dayOfMonth) => (
68
+ <SelectItem key={dayOfMonth.value} text={dayOfMonth.label} value={dayOfMonth.value.toString()} />
69
+ ))}
70
+ </Select>
71
+ );
72
+ };
73
+
74
+ export default CronDayOfMonthSelect;
@@ -0,0 +1,67 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { FilterableMultiSelect } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { type CronField, DAYS_OF_WEEK, DAYS_OF_WEEK_DEFAULT_LABELS } from './commons';
5
+ import { isEqual } from 'lodash-es';
6
+
7
+ interface CronDayOfWeekSelectProps {
8
+ value: CronField[];
9
+ onChange: (selectedDaysOfWeek: CronField[]) => void;
10
+ }
11
+
12
+ interface ValidationState {
13
+ invalid: boolean;
14
+ invalidText: string;
15
+ }
16
+
17
+ const CronDayOfWeekSelect: React.FC<CronDayOfWeekSelectProps> = ({ value, onChange }) => {
18
+ const { t } = useTranslation();
19
+
20
+ const [valueInternal, setValueInternal] = useState<CronField[]>(value);
21
+ const [validationState, setValidationState] = useState<ValidationState>({
22
+ invalid: false,
23
+ invalidText: null,
24
+ });
25
+
26
+ useEffect(() => {
27
+ if (!isEqual(value, valueInternal)) {
28
+ setValueInternal(value);
29
+ }
30
+ }, [value]);
31
+
32
+ useEffect(() => {
33
+ validate();
34
+ }, [valueInternal]);
35
+
36
+ useEffect(() => {
37
+ onChange(validationState.invalid ? null : valueInternal);
38
+ }, [validationState]);
39
+
40
+ const validate = () => {
41
+ if (!!valueInternal && valueInternal.length > 0) {
42
+ setValidationState({ invalid: false, invalidText: null });
43
+ } else {
44
+ setValidationState({ invalid: true, invalidText: t('dayOfWeekRequired', 'Required') });
45
+ }
46
+ };
47
+
48
+ return (
49
+ <FilterableMultiSelect
50
+ hideLabel={true}
51
+ items={DAYS_OF_WEEK}
52
+ compareItems={(item1, item2) => item1.value < item2.value}
53
+ itemToString={(item) =>
54
+ item ? t('dayOfWeek_' + item.name, DAYS_OF_WEEK_DEFAULT_LABELS[item.name] || item.name) : ''
55
+ }
56
+ selectionFeedback="fixed"
57
+ initialSelectedItems={valueInternal ? valueInternal : []}
58
+ onChange={(event) => {
59
+ setValueInternal(event.selectedItems);
60
+ }}
61
+ invalid={validationState.invalid}
62
+ invalidText={t(validationState.invalidText)}
63
+ />
64
+ );
65
+ };
66
+
67
+ export default CronDayOfWeekSelect;
@@ -0,0 +1,60 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { TimePicker } from '@carbon/react';
3
+ import { parseTime, TIME_PATTERN, TIME_PATTERN_REG_EXP, to24HTime } from '../../utils/time-utils';
4
+ import type { Time } from '../../utils/time-utils';
5
+ import { isEqual } from 'lodash-es';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ interface CronTimePickerProps {
9
+ value: Time;
10
+ onChange: (time: Time) => void;
11
+ }
12
+
13
+ interface ValidationState {
14
+ invalid: boolean;
15
+ invalidText: string;
16
+ }
17
+
18
+ const CronTimePicker: React.FC<CronTimePickerProps> = ({ value, onChange }) => {
19
+ const { t } = useTranslation();
20
+ const [valueInternal, setValueInternal] = useState(to24HTime(value));
21
+ const [validationState, setValidationState] = useState<ValidationState>({
22
+ invalid: false,
23
+ invalidText: null,
24
+ });
25
+
26
+ useEffect(() => {
27
+ const newInternalValue = to24HTime(value);
28
+ if (!isEqual(value, newInternalValue)) {
29
+ setValueInternal(newInternalValue);
30
+ }
31
+ }, [value]);
32
+
33
+ useEffect(() => {
34
+ onChange(validationState.invalid ? null : parseTime(valueInternal));
35
+ }, [validationState]);
36
+
37
+ const validate = () => {
38
+ if (TIME_PATTERN_REG_EXP.test(valueInternal)) {
39
+ setValidationState({ invalid: false, invalidText: null });
40
+ } else {
41
+ setValidationState({ invalid: true, invalidText: t('notATimeText', 'hh:mm 24-hr pattern required') });
42
+ }
43
+ };
44
+
45
+ return (
46
+ <TimePicker
47
+ hideLabel={true}
48
+ pattern={TIME_PATTERN}
49
+ value={valueInternal}
50
+ invalid={validationState.invalid}
51
+ invalidText={t(validationState.invalidText)}
52
+ onChange={(event) => {
53
+ setValueInternal(event.target.value);
54
+ }}
55
+ onBlur={validate}
56
+ />
57
+ );
58
+ };
59
+
60
+ export default CronTimePicker;