ui-soxo-bootstrap-core 2.4.26 → 2.5.0

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.
@@ -0,0 +1,225 @@
1
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { message } from 'antd';
4
+
5
+ /**
6
+ * Renders children in a separate browser window
7
+ * @param {ReactNode} children - Content to render in new window
8
+ * @param {Function} onClose - Callback when window closes
9
+ * @param {string} title - Window title
10
+ * @param {number} width - Window width
11
+ * @param {number} height - Window height
12
+ * @param {number} left - Window left position
13
+ * @param {number} top - Window top position
14
+ * @param {boolean} copyStyles - Whether to copy parent styles (default: true)
15
+ * @param {string} centerScreen - Center window on screen ('horizontal', 'vertical', 'both', or false)
16
+ * @param {Object} shortcuts - Keyboard shortcuts config { close: 'Escape', focus: 'Ctrl+Shift+F', ... }
17
+ * @param {Function} onMinimize - Callback when window is minimized
18
+ * @param {Function} onMaximize - Callback when window is maximized
19
+ */
20
+ export function ExternalWindow({
21
+ children,
22
+ onClose,
23
+ title = 'New Window',
24
+ width = 600,
25
+ height = 400,
26
+ left,
27
+ top,
28
+ copyStyles = true,
29
+ centerScreen = false,
30
+ shortcuts = {},
31
+ onMinimize,
32
+ onMaximize
33
+ }) {
34
+ const [container, setContainer] = useState(null);
35
+ const windowRef = useRef(null);
36
+
37
+ // Default shortcuts
38
+ const defaultShortcuts = useMemo(() => ({
39
+ close: 'Escape',
40
+ focus: 'Ctrl+Shift+F',
41
+ ...shortcuts
42
+ }), [shortcuts]);
43
+
44
+ // Calculate window position
45
+ const { posX, posY } = useMemo(() => {
46
+ let posX = left;
47
+ let posY = top;
48
+
49
+ if (centerScreen) {
50
+ const screenWidth = window.screen.availWidth;
51
+ const screenHeight = window.screen.availHeight;
52
+
53
+ if (centerScreen === 'both' || centerScreen === 'horizontal') {
54
+ posX = (screenWidth - width) / 2;
55
+ }
56
+ if (centerScreen === 'both' || centerScreen === 'vertical') {
57
+ posY = (screenHeight - height) / 2;
58
+ }
59
+ }
60
+
61
+ return {
62
+ posX: posX ?? 200,
63
+ posY: posY ?? 200
64
+ };
65
+ }, [left, top, width, height, centerScreen]);
66
+
67
+ const windowFeatures = useMemo(() =>
68
+ `width=${width},height=${height},left=${posX},top=${posY},resizable=yes,scrollbars=yes`,
69
+ [width, height, posX, posY]
70
+ );
71
+
72
+ // Initialize window document
73
+ const initializeWindow = useCallback((win) => {
74
+ const doc = win.document;
75
+
76
+ // Write minimal HTML structure (like your working example)
77
+ doc.open();
78
+ doc.write(`
79
+ <!DOCTYPE html>
80
+ <html>
81
+ <head>
82
+ <meta charset="UTF-8">
83
+ <title>${title}</title>
84
+ <style>
85
+ * {
86
+ box-sizing: border-box;
87
+ }
88
+ html, body {
89
+ width: 100%;
90
+ height: 100%;
91
+ margin: 0 !important;
92
+ padding: 0 !important;
93
+ overflow: auto;
94
+ background: ${getComputedStyle(document.body).backgroundColor || '#ffffff'};
95
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
96
+ color: ${getComputedStyle(document.body).color || '#000000'};
97
+ }
98
+ body {
99
+ display: block;
100
+ min-height: 100vh;
101
+ }
102
+ #root {
103
+ width: 100%;
104
+ min-height: 100%;
105
+ margin: 0;
106
+ padding: 0;
107
+ }
108
+ </style>
109
+ </head>
110
+ <body>
111
+ <div id="root"></div>
112
+ </body>
113
+ </html>
114
+ `);
115
+ doc.close();
116
+
117
+ return doc.getElementById('root');
118
+ }, [title]);
119
+
120
+ // Copy styles from parent window
121
+ const copyStylesToWindow = useCallback((targetWindow) => {
122
+ if (!copyStyles) return;
123
+
124
+ const fragment = targetWindow.document.createDocumentFragment();
125
+
126
+ // Copy all stylesheets from parent
127
+ Array.from(document.styleSheets).forEach(sheet => {
128
+ try {
129
+ if (sheet.href) {
130
+ // External stylesheet
131
+ const link = targetWindow.document.createElement('link');
132
+ link.rel = 'stylesheet';
133
+ link.href = sheet.href;
134
+ fragment.appendChild(link);
135
+ } else if (sheet.cssRules) {
136
+ // Inline stylesheet
137
+ const style = targetWindow.document.createElement('style');
138
+ const cssText = Array.from(sheet.cssRules).map(r => r.cssText).join('\n');
139
+ style.textContent = cssText;
140
+ fragment.appendChild(style);
141
+ }
142
+ } catch (e) {
143
+ // Silently ignore cross-origin stylesheet errors
144
+ console.warn('Could not copy stylesheet:', e);
145
+ }
146
+ });
147
+
148
+ targetWindow.document.head.appendChild(fragment);
149
+ }, [copyStyles]);
150
+
151
+ // Keyboard shortcut handler
152
+ const createKeyDownHandler = useCallback((win) => (e) => {
153
+ const key = [
154
+ e.ctrlKey && 'Ctrl',
155
+ e.shiftKey && 'Shift',
156
+ e.altKey && 'Alt',
157
+ e.metaKey && 'Meta',
158
+ e.key
159
+ ].filter(Boolean).join('+');
160
+
161
+ if (defaultShortcuts.close && key === defaultShortcuts.close) {
162
+ e.preventDefault();
163
+ onClose();
164
+ } else if (defaultShortcuts.focus && key === defaultShortcuts.focus) {
165
+ e.preventDefault();
166
+ win.focus();
167
+ } else if (defaultShortcuts.minimize && key === defaultShortcuts.minimize) {
168
+ e.preventDefault();
169
+ win.blur();
170
+ onMinimize?.();
171
+ } else if (defaultShortcuts.maximize && key === defaultShortcuts.maximize) {
172
+ e.preventDefault();
173
+ win.moveTo(0, 0);
174
+ win.resizeTo(screen.availWidth, screen.availHeight);
175
+ onMaximize?.();
176
+ }
177
+ }, [defaultShortcuts, onClose, onMinimize, onMaximize]);
178
+
179
+ useEffect(() => {
180
+ const win = window.open('', '', windowFeatures);
181
+
182
+ if (!win) {
183
+ message.error('Please allow popups for this site');
184
+ onClose();
185
+ return;
186
+ }
187
+
188
+ windowRef.current = win;
189
+
190
+ // Initialize window document structure
191
+ const root = initializeWindow(win);
192
+
193
+ // Copy styles from parent (async to not block)
194
+ requestAnimationFrame(() => {
195
+ copyStylesToWindow(win);
196
+ });
197
+
198
+ // Set container for portal immediately
199
+ setContainer(root);
200
+
201
+ // Setup keyboard shortcuts
202
+ const handleKeyDown = createKeyDownHandler(win);
203
+ window.addEventListener('keydown', handleKeyDown);
204
+ win.addEventListener('keydown', handleKeyDown);
205
+
206
+ // Handle window close
207
+ const handleClose = () => {
208
+ setContainer(null);
209
+ onClose();
210
+ };
211
+ win.addEventListener('beforeunload', handleClose);
212
+
213
+ // Cleanup
214
+ return () => {
215
+ window.removeEventListener('keydown', handleKeyDown);
216
+ win.removeEventListener('keydown', handleKeyDown);
217
+ win.removeEventListener('beforeunload', handleClose);
218
+ if (!win.closed) win.close();
219
+ };
220
+ // Only re-run if window features change, not on every render
221
+ // eslint-disable-next-line react-hooks/exhaustive-deps
222
+ }, [windowFeatures]);
223
+
224
+ return container ? createPortal(children, container) : null;
225
+ }
@@ -0,0 +1,80 @@
1
+ import React from 'react';
2
+ import { render, screen, act } from '@testing-library/react';
3
+ import { ExternalWindow } from './external-window';
4
+
5
+ // Mock window.open
6
+ global.open = jest.fn(() => {
7
+ const newWindow = {
8
+ document: {
9
+ open: jest.fn(),
10
+ write: jest.fn(),
11
+ close: jest.fn(),
12
+ getElementById: jest.fn().mockReturnValue(document.createElement('div')),
13
+ head: {
14
+ appendChild: jest.fn(),
15
+ },
16
+ createDocumentFragment: jest.fn().mockReturnThis(),
17
+ createElement: jest.fn().mockReturnThis(),
18
+ body: document.createElement('body'),
19
+ },
20
+ addEventListener: jest.fn(),
21
+ removeEventListener: jest.fn(),
22
+ close: jest.fn(),
23
+ screen: {
24
+ availWidth: 1920,
25
+ availHeight: 1080,
26
+ },
27
+ getComputedStyle: jest.fn().mockReturnValue({}),
28
+ };
29
+ return newWindow;
30
+ });
31
+
32
+ // Mock getComputedStyle
33
+ global.getComputedStyle = jest.fn(() => ({
34
+ backgroundColor: '#ffffff',
35
+ color: '#000000'
36
+ }));
37
+
38
+ // Mock message.error
39
+ jest.mock('antd', () => ({
40
+ ...jest.requireActual('antd'),
41
+ message: {
42
+ error: jest.fn(),
43
+ },
44
+ }));
45
+
46
+ describe('ExternalWindow', () => {
47
+ it('renders children in a portal', () => {
48
+ const handleClose = jest.fn();
49
+ render(
50
+ <ExternalWindow onClose={handleClose}>
51
+ <div>External Content</div>
52
+ </ExternalWindow>
53
+ );
54
+
55
+ // The content is rendered via a portal, so it won't be in the main container.
56
+ // Instead, we can check if window.open was called, which is the primary
57
+ // effect of this component.
58
+ expect(global.open).toHaveBeenCalled();
59
+ });
60
+
61
+ it('calls window.close on unmount', () => {
62
+ const handleClose = jest.fn();
63
+ const { unmount } = render(<ExternalWindow onClose={handleClose} />);
64
+
65
+ const newWindow = global.open.mock.results[0].value;
66
+
67
+ unmount(); // cleanup
68
+
69
+ expect(newWindow.close).toHaveBeenCalledTimes(1);
70
+ });
71
+
72
+ it('should not open a window if popups are blocked', () => {
73
+ // block popups
74
+ global.open.mockReturnValueOnce(null);
75
+ const handleClose = jest.fn();
76
+
77
+ render(<ExternalWindow onClose={handleClose} />);
78
+ expect(handleClose).toHaveBeenCalled();
79
+ });
80
+ });
@@ -10,9 +10,12 @@ import RootApplicationAPI from './root-application-api/root-application-api';
10
10
 
11
11
  import { HomePageAPI } from '../modules';
12
12
 
13
+ import { ExternalWindow } from './external-window/external-window';
14
+
13
15
  export {
14
16
  LandingAPI,
15
17
  RootApplicationAPI,
16
18
  ExtraInfoDetail,
17
- HomePageAPI
19
+ HomePageAPI,
20
+ ExternalWindow
18
21
  }
@@ -153,30 +153,30 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
153
153
 
154
154
  /**If there is roles assigned to the user */
155
155
  // for matria
156
- const useCoreMenus= process.env.REACT_APP_USE_CORE_MENUS === 'true';
157
- if(useCoreMenus){
158
- if (result && result.result && reportMenus) {
159
- setAllModules([...result.result, ...reportMenus, ...coreModules]);
160
- } else {
161
- //If there is no roles assigned to the user
162
- setAllModules([...coreModules]);
163
- }
156
+ const useCoreMenus = process.env.REACT_APP_USE_CORE_MENUS === 'true';
157
+ if (useCoreMenus) {
158
+ if (result && result.result && reportMenus) {
159
+ setAllModules([...result.result, ...reportMenus, ...coreModules]);
160
+ } else {
161
+ //If there is no roles assigned to the user
162
+ setAllModules([...coreModules]);
163
+ }
164
164
 
165
- } else{
166
- // for nura
167
- if (result && result.result.menus && reportMenus) {
168
- setAllModules([...result.result.menus, ...reportMenus, ...coreModules]);
169
165
  } else {
170
- //If there is no roles assigned to the user
171
- setAllModules([...coreModules]);
166
+ // for nura
167
+ if (result && result.result.menus && reportMenus) {
168
+ setAllModules([...result.result.menus, ...reportMenus, ...coreModules]);
169
+ } else {
170
+ //If there is no roles assigned to the user
171
+ setAllModules([...coreModules]);
172
+ }
173
+
174
+
172
175
  }
176
+ setLoader(false);
173
177
 
174
-
175
178
  }
176
- setLoader(false);
177
179
 
178
- }
179
-
180
180
 
181
181
  /**
182
182
  * Load the scripts
package/core/lib/Store.js CHANGED
@@ -48,25 +48,25 @@ import { ConfigProvider, theme } from 'antd';
48
48
  const initialTheme = () => {
49
49
  try {
50
50
  // manage theme with env
51
- const isEnvThemeTrue = process.env.REACT_APP_THEME;
51
+ const isEnvThemeTrue = process.env.REACT_APP_THEME;
52
52
 
53
- console.log('REACT_APP_THEME:', isEnvThemeTrue);
54
- const matchedTheme = themes.find(t => t.name === isEnvThemeTrue);
53
+ console.log('REACT_APP_THEME:', isEnvThemeTrue);
54
+ const matchedTheme = themes.find((t) => t.name === isEnvThemeTrue);
55
55
 
56
56
  if (matchedTheme) return matchedTheme;
57
-
58
- // Check saved theme
59
- const savedTheme = localStorage.getItem('selectedTheme');
60
- if (savedTheme) {
61
- return JSON.parse(savedTheme);
62
- }
63
-
64
- // Fallback to default
65
- return themes[0];
66
- } catch (e) {
67
- console.error('Error loading theme:', e);
68
- return themes[0];
57
+
58
+ // Check saved theme
59
+ const savedTheme = localStorage.getItem('selectedTheme');
60
+ if (savedTheme) {
61
+ return JSON.parse(savedTheme);
69
62
  }
63
+
64
+ // Fallback to default
65
+ return themes[0];
66
+ } catch (e) {
67
+ console.error('Error loading theme:', e);
68
+ return themes[0];
69
+ }
70
70
  };
71
71
 
72
72
  const initialState = {
@@ -87,7 +87,7 @@ let app = {};
87
87
  * @param {*} param0
88
88
  * @returns
89
89
  */
90
- export const GlobalProvider = ({ children, CustomModels, appSettings: settings }) => {
90
+ export const GlobalProvider = ({ children, CustomModels,CustomComponents, appSettings: settings }) => {
91
91
  // console.log("Setting up store");
92
92
 
93
93
  const [branches, setBranches] = useState([]);
@@ -226,13 +226,14 @@ export const GlobalProvider = ({ children, CustomModels, appSettings: settings }
226
226
  let store = {
227
227
  app: app,
228
228
  user: state.user,
229
- settings:state.settings,
229
+ settings: state.settings,
230
230
  dispatch: dispatch,
231
231
  twilio: state.twilio,
232
232
  isMobile,
233
233
  branches,
234
234
  defaultBranch: state.defaultBranch,
235
235
  CustomModels: CustomModels,
236
+ CustomComponents:CustomComponents,
236
237
  selectedBranch: state.selectedBranch,
237
238
  kiosk: state.kiosk,
238
239
  state,
@@ -314,7 +315,8 @@ export const AppReducer = (state, action) => {
314
315
 
315
316
  case 'CustomModels':
316
317
  return { ...state, CustomModels: action.CustomModels };
317
-
318
+ case 'CustomComponents':
319
+ return { ...state, CustomComponents: action.CustomComponents };
318
320
  // # TODO Below Variable might be removed
319
321
  case 'selectedLocation':
320
322
  localStorage.selectedLocation = JSON.stringify(action.payload);
@@ -109,6 +109,8 @@ import ConsentComponent from './consent/consent'
109
109
  import TaskOverviewLegacy from './../models/process/components/task-overview-legacy/task-overview-legacy'
110
110
 
111
111
  import ReportingDashboard from '../../modules/reporting/components/reporting-dashboard/reporting-dashboard';
112
+
113
+ import ProcessStepsPage from '../../modules/steps/steps'
112
114
  export {
113
115
 
114
116
  // Bootstrap Components
@@ -197,7 +199,8 @@ export {
197
199
  // WebCamera,
198
200
  ConsentComponent,
199
201
 
200
- ReportingDashboard
202
+ ReportingDashboard,
203
+ ProcessStepsPage
201
204
 
202
205
 
203
206
  }
@@ -1,46 +1,135 @@
1
- import { DatePicker } from 'antd';
1
+ /**
2
+ * @file rangepicker.js
3
+ * @description A reusable, enhanced Ant Design RangePicker component.
4
+ *
5
+ * Features:
6
+ * - Manual selections require "Apply" button confirmation
7
+ * - Preset ranges apply immediately without confirmation
8
+ * - Optimized state management and event handling
9
+ */
10
+
11
+ import { useCallback, useRef, useState } from 'react';
12
+ import { DatePicker, Space } from 'antd';
2
13
  import moment from 'moment-timezone';
3
14
  import PropTypes from 'prop-types';
15
+ import { useTranslation } from 'react-i18next';
16
+ import Button from '../button/button';
4
17
  import './rangepicker.scss';
5
18
 
19
+ /**
20
+ * Enhanced RangePicker with Apply/Cancel controls for manual selections.
21
+ * @param {object} props
22
+ * @param {moment.Moment[]} props.value - Current applied date range
23
+ * @param {function(moment.Moment[]): void} props.onChange - Callback when range is applied
24
+ * @param {string} [props.format='DD/MM/YYYY'] - Date display format
25
+ * @param {object} [props.ranges] - Preset date ranges
26
+ * @param {boolean} [props.allowClear=false] - Whether to show clear button
27
+ * @param {boolean} [props.inputReadOnly=true] - Whether input is read-only
28
+ */
29
+
6
30
  const { RangePicker } = DatePicker;
7
31
 
8
- export default function RangePickerComponent({ onChange, value, ranges }) {
9
- // value is still: [moment, moment]
10
- const startDate = value[0];
11
- const endDate = value[1];
32
+ export default function RangePickerComponent({
33
+ value,
34
+ onChange,
35
+ format = 'DD/MM/YYYY',
36
+ ranges,
37
+ allowClear = false,
38
+ inputReadOnly = true,
39
+ ...restProps
40
+ }) {
41
+ const { t } = useTranslation();
42
+
43
+ // Temporary range during selection (before Apply is clicked)
44
+ const [tempRange, setTempRange] = useState(null);
45
+ const [isOpen, setIsOpen] = useState(false);
46
+
47
+ // Ref to track if a selection just happened, to prevent the picker from closing.
48
+ const selectionJustHappened = useRef(false);
12
49
 
13
- function handleChange(dates) {
14
- if (!dates) {
15
- onChange(null);
50
+ /**
51
+ * Handles any completed date selection (both manual and preset).
52
+ * It updates the temporary range and sets a flag to keep the picker open.
53
+ */
54
+ const handleChange = useCallback((dates) => {
55
+ setTempRange(dates || []);
56
+ selectionJustHappened.current = true;
57
+ }, []);
58
+
59
+ /**
60
+ * Handles picker open/close events.
61
+ * Prevents closing after a selection, requiring user to click Apply/Cancel.
62
+ */
63
+ const handleOpenChange = useCallback((open) => {
64
+ // If the picker is trying to close, check if it was due to a selection.
65
+ if (!open && selectionJustHappened.current) {
66
+ // It was a selection, so ignore the close request and reset the flag.
67
+ selectionJustHappened.current = false;
16
68
  return;
17
69
  }
18
70
 
19
- // Convert dayjs moment so your external code does NOT break
20
- const converted = [moment(dates[0].toDate()), moment(dates[1].toDate())];
71
+ // For all other cases (opening, or closing by clicking outside), proceed as normal.
72
+ if (open) {
73
+ // Reset temp range when opening for a fresh selection.
74
+ setTempRange(null);
75
+ }
76
+ setIsOpen(open);
77
+ }, []);
78
+
79
+ /**
80
+ * Applies the temporary range and closes picker
81
+ */
82
+ const handleApply = useCallback(() => {
83
+ if (tempRange?.length === 2 && onChange) {
84
+ onChange(tempRange);
85
+ }
86
+ selectionJustHappened.current = false; // Clear the flag
87
+ setIsOpen(false);
88
+ }, [tempRange, onChange]);
21
89
 
22
- onChange(converted);
23
- }
90
+ /**
91
+ * Cancels selection and closes picker
92
+ */
93
+ const handleCancel = useCallback(() => {
94
+ selectionJustHappened.current = false; // Clear the flag
95
+ setTempRange(null);
96
+ setIsOpen(false);
97
+ }, []);
24
98
 
25
- // Convert your old "ranges" object into AntD presets array
26
- const presets = Object.entries(ranges).map(([label, range]) => ({
27
- label,
28
- value: range,
29
- }));
99
+ // Memoized preset ranges - use provided ranges or default set
100
+ const defaultRanges = useRef({
101
+ Today: [moment(), moment()],
102
+ Yesterday: [moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').endOf('day')],
103
+ 'This Week': [moment().startOf('week'), moment().endOf('week')],
104
+ 'Last Week': [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')],
105
+ 'This Month': [moment().startOf('month'), moment().endOf('month')],
106
+ 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')],
107
+ }).current;
30
108
 
31
109
  return (
32
- <div className="rangepicker">
33
- <RangePicker
34
- allowClear={false}
35
- inputReadOnly
36
- format="DD/MM/YYYY"
37
- // moment → allowed
38
- value={[startDate, endDate]}
39
- onChange={handleChange}
40
- presets={presets}
41
- ranges={ranges}
42
- />
43
- </div>
110
+ <RangePicker
111
+ allowClear={allowClear}
112
+ inputReadOnly={inputReadOnly}
113
+ format={format}
114
+ value={tempRange || value}
115
+ open={isOpen}
116
+ onOpenChange={handleOpenChange}
117
+ onChange={handleChange}
118
+ ranges={ranges !== undefined ? ranges : defaultRanges}
119
+ {...restProps}
120
+ renderExtraFooter={() => (
121
+ <div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end', order: 1 }}>
122
+ <Space>
123
+ <Button size="small" onClick={handleCancel}>
124
+ {t('Cancel')}
125
+ </Button>
126
+ <Button type="primary" size="small" onClick={handleApply}>
127
+ {t('Apply')}
128
+ </Button>
129
+ </Space>
130
+ </div>
131
+ )}
132
+ />
44
133
  );
45
134
  }
46
135