From 0783cbc1c5679c88ea6973a4a0dd831dcae7b8ba Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:17:50 +0530 Subject: [PATCH 01/16] feat: add date selection handler --- .../workflow-management/classes/Generator.ts | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 213a0e86..06eac494 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -1,4 +1,4 @@ -import { Action, ActionType, Coordinates, TagName } from "../../types"; +import { Action, ActionType, Coordinates, TagName, DatePickerEventData } from "../../types"; import { WhereWhatPair, WorkflowFile } from 'maxun-core'; import logger from "../../logger"; import { Socket } from "socket.io"; @@ -255,6 +255,25 @@ export class WorkflowGenerator { logger.log('info', `Workflow emitted`); }; + public onDateSelection = async (page: Page, data: DatePickerEventData) => { + const { selector, value } = data; + + try { + await page.fill(selector, value); + } catch (error) { + console.error("Failed to fill date value:", error); + } + + const pair: WhereWhatPair = { + where: { url: this.getBestUrl(page.url()) }, + what: [{ + action: 'fill', + args: [selector, value], + }], + }; + + await this.addPairToWorkflowAndNotifyClient(pair, page); + }; /** * Generates a pair for the click event. @@ -266,6 +285,22 @@ export class WorkflowGenerator { let where: WhereWhatPair["where"] = { url: this.getBestUrl(page.url()) }; const selector = await this.generateSelector(page, coordinates, ActionType.Click); logger.log('debug', `Element's selector: ${selector}`); + + // Check if clicked element is a date input + const isDateInput = await page.evaluate(({x, y}) => { + const element = document.elementFromPoint(x, y); + return element instanceof HTMLInputElement && element.type === 'date'; + }, coordinates); + + if (isDateInput) { + // Notify client to show datepicker overlay + this.socket.emit('showDatePicker', { + coordinates, + selector + }); + return; + } + //const element = await getElementMouseIsOver(page, coordinates); //logger.log('debug', `Element: ${JSON.stringify(element, null, 2)}`); if (selector) { From 7ac79dc31c7ffa26d94d4b58f9d50088e238fca7 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:19:05 +0530 Subject: [PATCH 02/16] feat: add date selection event handlers --- .../src/browser-management/inputHandlers.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index d6902b3f..2e722e9d 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -6,7 +6,7 @@ import { Socket } from 'socket.io'; import logger from "../logger"; -import { Coordinates, ScrollDeltas, KeyboardInput } from '../types'; +import { Coordinates, ScrollDeltas, KeyboardInput, DatePickerEventData } from '../types'; import { browserPool } from "../server"; import { WorkflowGenerator } from "../workflow-management/classes/Generator"; import { Page } from "playwright"; @@ -223,6 +223,23 @@ const handleKeydown = async (generator: WorkflowGenerator, page: Page, { key, co logger.log('debug', `Key ${key} pressed`); }; +/** + * Handles the date selection event. + * @param generator - the workflow generator {@link Generator} + * @param page - the active page of the remote browser + * @param data - the data of the date selection event {@link DatePickerEventData} + * @category BrowserManagement + */ +const handleDateSelection = async (generator: WorkflowGenerator, page: Page, data: DatePickerEventData) => { + await generator.onDateSelection(page, data); + logger.log('debug', `Date ${data.value} selected`); +} + +const onDateSelection = async (data: DatePickerEventData) => { + logger.log('debug', 'Handling date selection event emitted from client'); + await handleWrapper(handleDateSelection, data); +} + /** * A wrapper function for handling the keyup event. * @param keyboardInput - the keyboard input of the keyup event @@ -378,6 +395,7 @@ const registerInputHandlers = (socket: Socket) => { socket.on("input:refresh", onRefresh); socket.on("input:back", onGoBack); socket.on("input:forward", onGoForward); + socket.on("input:date", onDateSelection); socket.on("action", onGenerateAction); }; From 7eea077e70124cc9a24faaa768d7752305158685 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:21:05 +0530 Subject: [PATCH 03/16] feat: add interface to hanle date picker event data --- server/src/types/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 4fe761f1..f2e327ef 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -20,6 +20,16 @@ export interface Coordinates { y: number; } +/** + * interface to handle date picker events. + * @category Types + */ +export interface DatePickerEventData { + coordinates: Coordinates; + selector: string; + value: string; +} + /** * Holds the deltas of a wheel/scroll event. * @category Types From ec4d1acfa27c9c0ff5ef1fa6ae5415ecad6ebf15 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:22:51 +0530 Subject: [PATCH 04/16] feat: trigger socket event to display date picker --- src/components/atoms/canvas.tsx | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index 1dd88e19..84d6a620 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -3,6 +3,7 @@ import { useSocketStore } from '../../context/socket'; import { getMappedCoordinates } from "../../helpers/inputHelpers"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { useActionContext } from '../../context/browserActions'; +import DatePicker from './DatePicker'; interface CreateRefCallback { (ref: React.RefObject): void; @@ -31,6 +32,11 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { const getTextRef = useRef(getText); const getListRef = useRef(getList); + const [datePickerInfo, setDatePickerInfo] = React.useState<{ + coordinates: Coordinates; + selector: string; + } | null>(null); + const notifyLastAction = (action: string) => { if (lastAction !== action) { setLastAction(action); @@ -44,6 +50,28 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { getListRef.current = getList; }, [getText, getList]); + useEffect(() => { + if (socket) { + socket.on('showDatePicker', (info: {coordinates: Coordinates, selector: string}) => { + setDatePickerInfo(info); + }); + + return () => { + socket.off('showDatePicker'); + }; + } + }, [socket]); + + const handleDateSelect = (value: string) => { + if (socket && datePickerInfo) { + socket.emit('input:date', { + selector: datePickerInfo.selector, + value + }); + setDatePickerInfo(null); + } + }; + const onMouseEvent = useCallback((event: MouseEvent) => { if (socket && canvasRef.current) { // Get the canvas bounding rectangle @@ -146,6 +174,13 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { width={900} style={{ display: 'block' }} /> + {datePickerInfo && ( + setDatePickerInfo(null)} + /> + )} ); From 9ee54d118b522d87c7e37791e69f3a6496f53489 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:23:56 +0530 Subject: [PATCH 05/16] feat: rm onHandleSelct callback function --- src/components/atoms/canvas.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index 84d6a620..13966abd 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -62,16 +62,6 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { } }, [socket]); - const handleDateSelect = (value: string) => { - if (socket && datePickerInfo) { - socket.emit('input:date', { - selector: datePickerInfo.selector, - value - }); - setDatePickerInfo(null); - } - }; - const onMouseEvent = useCallback((event: MouseEvent) => { if (socket && canvasRef.current) { // Get the canvas bounding rectangle From 2e301924226fda12ce8fea82828b3eaa4c2976c6 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:24:51 +0530 Subject: [PATCH 06/16] feat: add date picker component to input date --- src/components/atoms/DatePicker.tsx | 74 +++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/components/atoms/DatePicker.tsx diff --git a/src/components/atoms/DatePicker.tsx b/src/components/atoms/DatePicker.tsx new file mode 100644 index 00000000..30d3b869 --- /dev/null +++ b/src/components/atoms/DatePicker.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { useSocketStore } from '../../context/socket'; +import { Coordinates } from './canvas'; + +interface DatePickerProps { + coordinates: Coordinates; + selector: string; + onClose: () => void; +} + +const DatePicker: React.FC = ({ coordinates, selector, onClose }) => { + const { socket } = useSocketStore(); + const [selectedDate, setSelectedDate] = useState(''); + + const handleDateChange = (e: React.ChangeEvent) => { + setSelectedDate(e.target.value); + }; + + const handleConfirm = () => { + if (socket && selectedDate) { + socket.emit('input:date', { + selector, + value: selectedDate + }); + onClose(); + } + }; + + return ( +
+
+ +
+ + +
+
+
+ ); +}; + +export default DatePicker; \ No newline at end of file From 14079fa0f89029502e6c3671aab0d9232e17e5a9 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 12:13:17 +0530 Subject: [PATCH 07/16] feat: date input check using element information --- server/src/workflow-management/classes/Generator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 06eac494..96645de0 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -286,11 +286,11 @@ export class WorkflowGenerator { const selector = await this.generateSelector(page, coordinates, ActionType.Click); logger.log('debug', `Element's selector: ${selector}`); + const elementInfo = await getElementInformation(page, coordinates, '', false); + console.log("Element info: ", elementInfo); + // Check if clicked element is a date input - const isDateInput = await page.evaluate(({x, y}) => { - const element = document.elementFromPoint(x, y); - return element instanceof HTMLInputElement && element.type === 'date'; - }, coordinates); + const isDateInput = elementInfo?.tagName === 'INPUT' && elementInfo?.attributes?.type === 'date'; if (isDateInput) { // Notify client to show datepicker overlay From a6ed8c67b8b19e7d684e89f4e2985947a48d4522 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 12:14:08 +0530 Subject: [PATCH 08/16] feat: check for select type and emit dropdown socket event --- .../workflow-management/classes/Generator.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 96645de0..a221a47b 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -289,6 +289,45 @@ export class WorkflowGenerator { const elementInfo = await getElementInformation(page, coordinates, '', false); console.log("Element info: ", elementInfo); + // Check if clicked element is a select dropdown + const isDropdown = elementInfo?.tagName === 'SELECT'; + + if (isDropdown && elementInfo.innerHTML) { + // Parse options from innerHTML + const options = elementInfo.innerHTML + .split(' { + const valueMatch = optionHtml.match(/value="([^"]*)"/); + const disabledMatch = optionHtml.includes('disabled="disabled"'); + const selectedMatch = optionHtml.includes('selected="selected"'); + + // Extract text content between > and + const textMatch = optionHtml.match(/>([^<]*) Date: Thu, 19 Dec 2024 12:23:13 +0530 Subject: [PATCH 09/16] feat: add dropdown selection action pair to workflow --- .../workflow-management/classes/Generator.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index a221a47b..3395e9a1 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -275,6 +275,26 @@ export class WorkflowGenerator { await this.addPairToWorkflowAndNotifyClient(pair, page); }; + public onDropdownSelection = async (page: Page, data: { selector: string, value: string }) => { + const { selector, value } = data; + + try { + await page.selectOption(selector, value); + } catch (error) { + console.error("Failed to fill date value:", error); + } + + const pair: WhereWhatPair = { + where: { url: this.getBestUrl(page.url()) }, + what: [{ + action: 'selectOption', + args: [selector, value], + }], + }; + + await this.addPairToWorkflowAndNotifyClient(pair, page); + }; + /** * Generates a pair for the click event. * @param coordinates The coordinates of the click event. From d8b5ae4113d5a4201680e4e23ff6721708ec0199 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 12:24:19 +0530 Subject: [PATCH 10/16] feat: add dropdown selection handler functions and register socket event --- server/src/browser-management/inputHandlers.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index 2e722e9d..4b37480f 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -240,6 +240,16 @@ const onDateSelection = async (data: DatePickerEventData) => { await handleWrapper(handleDateSelection, data); } +const handleDropdownSelection = async (generator: WorkflowGenerator, page: Page, data: { selector: string, value: string }) => { + await generator.onDropdownSelection(page, data); + logger.log('debug', `Dropdown value ${data.value} selected`); +} + +const onDropdownSelection = async (data: { selector: string, value: string }) => { + logger.log('debug', 'Handling dropdown selection event emitted from client'); + await handleWrapper(handleDropdownSelection, data); +} + /** * A wrapper function for handling the keyup event. * @param keyboardInput - the keyboard input of the keyup event @@ -396,6 +406,7 @@ const registerInputHandlers = (socket: Socket) => { socket.on("input:back", onGoBack); socket.on("input:forward", onGoForward); socket.on("input:date", onDateSelection); + socket.on("input:dropdown", onDropdownSelection); socket.on("action", onGenerateAction); }; From 7bd7a84173d47be16a0f828a1067e7fca7a2ed8d Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 14:04:05 +0530 Subject: [PATCH 11/16] feat: tigger socket event to display dropdown --- src/components/atoms/canvas.tsx | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index 13966abd..fc778963 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -4,6 +4,7 @@ import { getMappedCoordinates } from "../../helpers/inputHelpers"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { useActionContext } from '../../context/browserActions'; import DatePicker from './DatePicker'; +import Dropdown from './Dropdown'; interface CreateRefCallback { (ref: React.RefObject): void; @@ -37,6 +38,17 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { selector: string; } | null>(null); + const [dropdownInfo, setDropdownInfo] = React.useState<{ + coordinates: Coordinates; + selector: string; + options: Array<{ + value: string; + text: string; + disabled: boolean; + selected: boolean; + }>; + } | null>(null); + const notifyLastAction = (action: string) => { if (lastAction !== action) { setLastAction(action); @@ -56,8 +68,22 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { setDatePickerInfo(info); }); + socket.on('showDropdown', (info: { + coordinates: Coordinates, + selector: string, + options: Array<{ + value: string; + text: string; + disabled: boolean; + selected: boolean; + }>; + }) => { + setDropdownInfo(info); + }); + return () => { socket.off('showDatePicker'); + socket.off('showDropdown'); }; } }, [socket]); @@ -171,6 +197,14 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { onClose={() => setDatePickerInfo(null)} /> )} + {dropdownInfo && ( + setDropdownInfo(null)} + /> + )} ); From 13b92ee5dce7d7e33e3e0f60c608e202187f01fb Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 14:05:02 +0530 Subject: [PATCH 12/16] feat: add dropdown component to input dropdown --- src/components/atoms/Dropdown.tsx | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/components/atoms/Dropdown.tsx diff --git a/src/components/atoms/Dropdown.tsx b/src/components/atoms/Dropdown.tsx new file mode 100644 index 00000000..c7ead64b --- /dev/null +++ b/src/components/atoms/Dropdown.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { useSocketStore } from '../../context/socket'; +import { Coordinates } from './canvas'; + +interface DropdownProps { + coordinates: Coordinates; + selector: string; + options: Array<{ + value: string; + text: string; + disabled: boolean; + selected: boolean; + }>; + onClose: () => void; +} + +const Dropdown = ({ coordinates, selector, options, onClose }: DropdownProps) => { + const { socket } = useSocketStore(); + const [hoveredIndex, setHoveredIndex] = useState(null); + + const handleSelect = (value: string) => { + if (socket) { + socket.emit('input:dropdown', { selector, value }); + } + onClose(); + }; + + const containerStyle: React.CSSProperties = { + position: 'absolute', + left: coordinates.x, + top: coordinates.y, + zIndex: 1000, + width: '200px', + backgroundColor: 'white', + border: '1px solid rgb(169, 169, 169)', + boxShadow: '0 2px 4px rgba(0,0,0,0.15)', + }; + + const scrollContainerStyle: React.CSSProperties = { + maxHeight: '180px', + overflowY: 'auto', + overflowX: 'hidden', + }; + + const getOptionStyle = (option: any, index: number): React.CSSProperties => ({ + fontSize: '13.333px', + lineHeight: '18px', + padding: '0 3px', + cursor: option.disabled ? 'default' : 'default', + backgroundColor: hoveredIndex === index ? '#0078D7' : + option.selected ? '#0078D7' : + option.disabled ? '#f8f8f8' : 'white', + color: (hoveredIndex === index || option.selected) ? 'white' : + option.disabled ? '#a0a0a0' : 'black', + userSelect: 'none', + }); + + return ( +
+
e.stopPropagation()} + > +
+ {options.map((option, index) => ( +
!option.disabled && setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)} + onClick={() => !option.disabled && handleSelect(option.value)} + > + {option.text} +
+ ))} +
+
+
+ ); +}; + +export default Dropdown; \ No newline at end of file From 947a6b75cb9d2d431d23ecfd98ec20cc1a27e855 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 16:16:47 +0530 Subject: [PATCH 13/16] feat: check for time input field and emit socket event --- .../workflow-management/classes/Generator.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 3395e9a1..9ff4922e 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -295,6 +295,26 @@ export class WorkflowGenerator { await this.addPairToWorkflowAndNotifyClient(pair, page); }; + public onTimeSelection = async (page: Page, data: { selector: string, value: string }) => { + const { selector, value } = data; + + try { + await page.fill(selector, value); + } catch (error) { + console.error("Failed to set time value:", error); + } + + const pair: WhereWhatPair = { + where: { url: this.getBestUrl(page.url()) }, + what: [{ + action: 'fill', + args: [selector, value], + }], + }; + + await this.addPairToWorkflowAndNotifyClient(pair, page); + }; + /** * Generates a pair for the click event. * @param coordinates The coordinates of the click event. @@ -360,6 +380,16 @@ export class WorkflowGenerator { return; } + const isTimeInput = elementInfo?.tagName === 'INPUT' && elementInfo?.attributes?.type === 'time'; + + if (isTimeInput) { + this.socket.emit('showTimePicker', { + coordinates, + selector + }); + return; + } + //const element = await getElementMouseIsOver(page, coordinates); //logger.log('debug', `Element: ${JSON.stringify(element, null, 2)}`); if (selector) { From 0b2d099dc0348af577762aa2588250e2c8c6202a Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 16:25:48 +0530 Subject: [PATCH 14/16] feat: add time selection event handlers --- server/src/browser-management/inputHandlers.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index 4b37480f..982e18de 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -250,6 +250,16 @@ const onDropdownSelection = async (data: { selector: string, value: string }) => await handleWrapper(handleDropdownSelection, data); } +const handleTimeSelection = async (generator: WorkflowGenerator, page: Page, data: { selector: string, value: string }) => { + await generator.onTimeSelection(page, data); + logger.log('debug', `Time value ${data.value} selected`); +} + +const onTimeSelection = async (data: { selector: string, value: string }) => { + logger.log('debug', 'Handling time selection event emitted from client'); + await handleWrapper(handleTimeSelection, data); +} + /** * A wrapper function for handling the keyup event. * @param keyboardInput - the keyboard input of the keyup event @@ -407,6 +417,7 @@ const registerInputHandlers = (socket: Socket) => { socket.on("input:forward", onGoForward); socket.on("input:date", onDateSelection); socket.on("input:dropdown", onDropdownSelection); + socket.on("input:time", onTimeSelection); socket.on("action", onGenerateAction); }; From 66f3ccd34fa8c06da8dd35b5277e093b318109aa Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 16:26:23 +0530 Subject: [PATCH 15/16] trigger socket event to display time picker --- src/components/atoms/canvas.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index fc778963..77128a65 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -5,6 +5,7 @@ import { useGlobalInfoStore } from "../../context/globalInfo"; import { useActionContext } from '../../context/browserActions'; import DatePicker from './DatePicker'; import Dropdown from './Dropdown'; +import TimePicker from './TimePicker'; interface CreateRefCallback { (ref: React.RefObject): void; @@ -49,6 +50,11 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { }>; } | null>(null); + const [timePickerInfo, setTimePickerInfo] = React.useState<{ + coordinates: Coordinates; + selector: string; + } | null>(null); + const notifyLastAction = (action: string) => { if (lastAction !== action) { setLastAction(action); @@ -81,6 +87,10 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { setDropdownInfo(info); }); + socket.on('showTimePicker', (info: {coordinates: Coordinates, selector: string}) => { + setTimePickerInfo(info); + }); + return () => { socket.off('showDatePicker'); socket.off('showDropdown'); @@ -205,6 +215,13 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { onClose={() => setDropdownInfo(null)} /> )} + {timePickerInfo && ( + setTimePickerInfo(null)} + /> + )} ); From a97837d8b8cc20fcc943362d2745fff9d21c662d Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 16:26:57 +0530 Subject: [PATCH 16/16] feat: add time picker component to input time --- src/components/atoms/TimePicker.tsx | 130 ++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/components/atoms/TimePicker.tsx diff --git a/src/components/atoms/TimePicker.tsx b/src/components/atoms/TimePicker.tsx new file mode 100644 index 00000000..31353c7a --- /dev/null +++ b/src/components/atoms/TimePicker.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import { useSocketStore } from '../../context/socket'; +import { Coordinates } from './canvas'; + +interface TimePickerProps { + coordinates: Coordinates; + selector: string; + onClose: () => void; +} + +const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => { + const { socket } = useSocketStore(); + const [hoveredHour, setHoveredHour] = useState(null); + const [hoveredMinute, setHoveredMinute] = useState(null); + const [selectedHour, setSelectedHour] = useState(null); + const [selectedMinute, setSelectedMinute] = useState(null); + + const handleHourSelect = (hour: number) => { + setSelectedHour(hour); + // If minute is already selected, complete the selection + if (selectedMinute !== null) { + const formattedHour = hour.toString().padStart(2, '0'); + const formattedMinute = selectedMinute.toString().padStart(2, '0'); + if (socket) { + socket.emit('input:time', { + selector, + value: `${formattedHour}:${formattedMinute}` + }); + } + onClose(); + } + }; + + const handleMinuteSelect = (minute: number) => { + setSelectedMinute(minute); + // If hour is already selected, complete the selection + if (selectedHour !== null) { + const formattedHour = selectedHour.toString().padStart(2, '0'); + const formattedMinute = minute.toString().padStart(2, '0'); + if (socket) { + socket.emit('input:time', { + selector, + value: `${formattedHour}:${formattedMinute}` + }); + } + onClose(); + } + }; + + const containerStyle: React.CSSProperties = { + position: 'absolute', + left: coordinates.x, + top: coordinates.y, + zIndex: 1000, + display: 'flex', + backgroundColor: 'white', + border: '1px solid rgb(169, 169, 169)', + boxShadow: '0 2px 4px rgba(0,0,0,0.15)', + }; + + const columnStyle: React.CSSProperties = { + width: '60px', + maxHeight: '180px', + overflowY: 'auto', + overflowX: 'hidden', + borderRight: '1px solid rgb(169, 169, 169)', + }; + + const getOptionStyle = (value: number, isHour: boolean): React.CSSProperties => { + const isHovered = isHour ? hoveredHour === value : hoveredMinute === value; + const isSelected = isHour ? selectedHour === value : selectedMinute === value; + + return { + fontSize: '13.333px', + lineHeight: '18px', + padding: '0 3px', + cursor: 'default', + backgroundColor: isSelected ? '#0078D7' : isHovered ? '#0078D7' : 'white', + color: (isSelected || isHovered) ? 'white' : 'black', + userSelect: 'none', + }; + }; + + const hours = Array.from({ length: 24 }, (_, i) => i); + const minutes = Array.from({ length: 60 }, (_, i) => i); + + return ( +
+
e.stopPropagation()} + > + {/* Hours column */} +
+ {hours.map((hour) => ( +
setHoveredHour(hour)} + onMouseLeave={() => setHoveredHour(null)} + onClick={() => handleHourSelect(hour)} + > + {hour.toString().padStart(2, '0')} +
+ ))} +
+ + {/* Minutes column */} +
+ {minutes.map((minute) => ( +
setHoveredMinute(minute)} + onMouseLeave={() => setHoveredMinute(null)} + onClick={() => handleMinuteSelect(minute)} + > + {minute.toString().padStart(2, '0')} +
+ ))} +
+
+
+ ); +}; + +export default TimePicker; \ No newline at end of file