diff --git a/src/components/UploadButton/UploadButton.tsx b/src/components/UploadButton/UploadButton.tsx index 70994f98b..0823bf2ae 100644 --- a/src/components/UploadButton/UploadButton.tsx +++ b/src/components/UploadButton/UploadButton.tsx @@ -159,15 +159,15 @@ const FileUploadButton = ({ // Try parsing with full options first workbook = window.XLSX.read(arrayBuffer, { type: 'array', - cellFormula: false, // Disable formula parsing to avoid potential issues - cellNF: false, // Disable number format parsing - cellHTML: false, // Disable HTML parsing - cellText: true, // Force text output - cellDates: false, // Disable date parsing to avoid errors + cellFormula: true, + cellNF: true, + cellHTML: true, + cellText: true, + cellDates: true, error: (e: any) => { console.warn('Non-fatal XLSX error:', e); - }, // Log non-fatal errors - cellStyles: false, // Disable style parsing + }, + cellStyles: true, }); } catch (initialError) { console.warn( @@ -179,15 +179,15 @@ const FileUploadButton = ({ try { workbook = window.XLSX.read(arrayBuffer, { type: 'array', - sheetRows: 1000, // Limit number of rows to parse - cellFormula: false, - cellStyles: false, - bookDeps: false, // Don't parse external dependencies - bookFiles: false, // Don't parse embedded files - bookProps: false, // Don't parse document properties - bookSheets: false, // Don't parse sheet properties - bookVBA: false, // Don't parse VBA - WTF: true, // "What the Formula" mode - ignores errors when possible + sheetRows: 1000, + cellFormula: true, + cellStyles: true, + bookDeps: true, + bookFiles: true, + bookProps: true, + bookSheets: true, + bookVBA: true, + WTF: true, }); } catch (recoveryError) { setErrors(prev => [ @@ -247,27 +247,62 @@ const FileUploadButton = ({ ); } - // Try to convert sheet to CSV - let csv; + // Try to convert sheet to formatted text with columns + let formattedText; try { - csv = window.XLSX.utils.sheet_to_csv(worksheet); - } catch (csvError) { - // If CSV conversion fails, try a more basic cell-by-cell approach - csv = ''; + // Get array of arrays representation + const data = window.XLSX.utils.sheet_to_json(worksheet, { + header: 1, + raw: false, + }); + + // Find the maximum width for each column + const colWidths = data.reduce((widths: number[], row: any[]) => { + row.forEach((cell, i) => { + const cellWidth = (cell || '').toString().length; + widths[i] = Math.max(widths[i] || 0, cellWidth); + }); + return widths; + }, []); + + // Format each row with proper column spacing + formattedText = data.map((row: any[]) => { + return row + .map((cell, i) => { + const cellStr = (cell || '').toString(); + return cellStr.padEnd(colWidths[i] + 2); // Add 2 spaces padding + }) + .join('|') + .trim(); + }); + + // Add separator line after header + if (formattedText.length > 0) { + const separator = colWidths + .map((w: number) => '-'.repeat(w + 2)) + .join('+'); + formattedText.splice(1, 0, separator); + } + + formattedText = formattedText.join('\n'); + + } catch (formatError) { + // Fallback to basic formatting if advanced fails + formattedText = ''; for (let r = range.s.r; r <= Math.min(range.e.r, 1000); ++r) { let row = ''; for (let c = range.s.c; c <= Math.min(range.e.c, 100); ++c) { const cell = worksheet[window.XLSX.utils.encode_cell({ r: r, c: c })]; - row += (cell ? String(cell.v || '') : '') + ','; + row += (cell ? String(cell.v || '').padEnd(15) : ' '.repeat(15)) + '|'; } - csv += row + '\n'; + formattedText += row + '\n'; } - csv += '...(truncated due to potential corruption)'; + formattedText += '...(truncated due to potential corruption)'; } - // Add sheet name and content to final text - text += `Sheet: ${sheetName}\n${csv}\n\n`; + // Add sheet name and formatted content to final text + text += `Sheet: ${sheetName}\n${formattedText}\n\n`; successfulSheets++; } catch (sheetError) { // Log sheet-specific error but continue with other sheets @@ -291,7 +326,7 @@ const FileUploadButton = ({ text; } - return text; // Return the extracted text from all sheets + return text; } catch (error) { setErrors(prev => [ ...prev, @@ -303,7 +338,6 @@ const FileUploadButton = ({ fileId: file.name, }, ]); - // If any error occurs during processing, throw with descriptive message throw new Error( `XLSX extraction failed: ${ error instanceof Error ? error.message : 'Unknown error' diff --git a/src/components/ui/Switch.css b/src/components/ui/Switch.css new file mode 100644 index 000000000..18f202850 --- /dev/null +++ b/src/components/ui/Switch.css @@ -0,0 +1,133 @@ +.memori-switch-wrapper { + display: inline-flex; + align-items: center; + gap: 0.5rem; + } + + .memori-switch--label { + color: #333; + font-size: 14px; + font-weight: 500; + } + + .memori-switch { + position: relative; + display: inline-flex; + width: 44px; + height: 22px; + align-items: center; + border: none; + border-radius: 100px; + background-color: rgba(0, 0, 0, 0.25); + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1); + cursor: pointer; + outline: none; + transition: all 0.2s; + } + + /* Size variants */ + .memori-switch--small { + width: 28px; + min-width: 28px; + height: 16px; + } + + .memori-switch--large { + width: 56px; + min-width: 56px; + height: 28px; + } + + .memori-switch:focus { + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + + .memori-switch--checked { + background-color: #1890ff; + } + + .memori-switch--disabled { + background-color: rgba(0, 0, 0, 0.25); /* Stay gray when disabled */ + cursor: not-allowed; + opacity: 0.4; + } + + .memori-switch--checked.memori-switch--disabled { + background-color: rgba(24, 144, 255, 0.6); /* Light blue when checked and disabled */ + } + + .memori-switch--handle { + position: absolute; + top: 2px; + left: 2px; + display: flex; + width: 18px; + height: 18px; + align-items: center; + justify-content: center; + border-radius: 50%; + background-color: #fff; + box-shadow: 0 2px 4px 0 rgba(0, 35, 11, 0.2); + transform: translateX(0%); + transition: all 0.2s; + } + + .memori-switch--small .memori-switch--handle { + width: 12px; + height: 12px; + } + + .memori-switch--large .memori-switch--handle { + width: 24px; + height: 24px; + } + + .memori-switch--handle-checked { + transform: translateX(22px); + } + + .memori-switch--small .memori-switch--handle-checked { + transform: translateX(12px); + } + + .memori-switch--large .memori-switch--handle-checked { + transform: translateX(28px); + } + + .memori-switch--inner { + margin-right: 6px; + margin-left: 24px; + color: #fff; + font-size: 12px; + } + + .memori-switch--checked .memori-switch--inner { + margin-right: 24px; + margin-left: 6px; + } + + /* Loading spinner animation */ + .memori-switch--loading-spinner { + width: 12px; + height: 12px; + animation: switch-spinner 1s linear infinite; + } + + .memori-switch--small .memori-switch--loading-spinner { + width: 8px; + height: 8px; + } + + .memori-switch--large .memori-switch--loading-spinner { + width: 16px; + height: 16px; + } + + @keyframes switch-spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } \ No newline at end of file diff --git a/src/components/ui/Switch.stories.tsx b/src/components/ui/Switch.stories.tsx new file mode 100644 index 000000000..46229deac --- /dev/null +++ b/src/components/ui/Switch.stories.tsx @@ -0,0 +1,188 @@ +import React, { useState } from 'react'; +import { Meta, Story } from '@storybook/react'; +import Switch, { Props } from './Switch'; +import './Switch.css'; + +const meta: Meta = { + title: 'UI/Switch', + component: Switch, + argTypes: { + checked: { + control: { + type: 'boolean', + }, + }, + defaultChecked: { + control: { + type: 'boolean', + }, + }, + disabled: { + control: { + type: 'boolean', + }, + }, + size: { + control: { + type: 'select', + options: ['small', 'default', 'large'], + }, + }, + loading: { + control: { + type: 'boolean', + }, + }, + label: { + control: { + type: 'text', + }, + }, + className: { + control: { + type: 'text', + }, + }, + }, + parameters: { + controls: { expanded: true }, + }, +}; + +export default meta; + +const Template: Story = args => { + const [checked, setChecked] = useState(args.checked || args.defaultChecked || false); + + const handleChange = (newChecked: boolean) => { + setChecked(newChecked); + if (args.onChange) { + args.onChange(newChecked); + } + }; + + return ; +}; + +export const Default = Template.bind({}); +Default.args = {}; + +export const Checked = Template.bind({}); +Checked.args = { + defaultChecked: true, +}; + +export const WithLabel = Template.bind({}); +WithLabel.args = { + label: 'Toggle feature', +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + disabled: true, +}; + +export const DisabledAndChecked = Template.bind({}); +DisabledAndChecked.args = { + disabled: true, + defaultChecked: true, +}; + +export const Small = Template.bind({}); +Small.args = { + size: 'small', +}; + +export const Large = Template.bind({}); +Large.args = { + size: 'large', +}; + +export const Loading = Template.bind({}); +Loading.args = { + loading: true, +}; + +export const LoadingAndChecked = Template.bind({}); +LoadingAndChecked.args = { + loading: true, + defaultChecked: true, +}; + +export const WithTestId = Template.bind({}); +WithTestId.args = { + 'data-testid': 'feature-toggle', +}; + +export const WithChildren = Template.bind({}); +WithChildren.args = { + checkedChildren: 'ON', + unCheckedChildren: 'OFF', +}; + +export const SmallWithChildren = Template.bind({}); +SmallWithChildren.args = { + size: 'small', + checkedChildren: '✓', + unCheckedChildren: '✗', +}; + +export const LargeWithChildren = Template.bind({}); +LargeWithChildren.args = { + size: 'large', + checkedChildren: 'ENABLED', + unCheckedChildren: 'DISABLED', +}; + +// Interactive example with form integration +export const InteractiveForm: Story = () => { + const [formState, setFormState] = useState({ + disableR2R3Loop: false, + enableNotifications: true, + darkMode: false, + }); + + const handleFieldChange = (field: string) => (checked: boolean) => { + setFormState({ + ...formState, + [field]: checked, + }); + }; + + return ( +
+

Settings

+ +
+ Disable R2R3 Loop + +
+ +
+ Enable Notifications + +
+ +
+ Dark Mode + +
+ +
+
{JSON.stringify(formState, null, 2)}
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/Switch.test.tsx b/src/components/ui/Switch.test.tsx new file mode 100644 index 000000000..9f3d11dd0 --- /dev/null +++ b/src/components/ui/Switch.test.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Switch from './Switch'; + +describe('Switch Component', () => { + it('renders Switch unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders Switch with label unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders Switch checked unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders Switch with defaultChecked unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders Switch disabled unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders Switch disabled and checked unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders Switch with size small unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders Switch with size large unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders Switch with loading state unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders Switch with checkedChildren and unCheckedChildren unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders Switch with data-testid unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('calls onChange when clicked', () => { + const handleChange = jest.fn(); + render(); + + const switchElement = screen.getByTestId('test-switch'); + fireEvent.click(switchElement); + + expect(handleChange).toHaveBeenCalledWith(true); + }); + + it('does not call onChange when disabled', () => { + const handleChange = jest.fn(); + render(); + + const switchElement = screen.getByTestId('test-switch'); + fireEvent.click(switchElement); + + expect(handleChange).not.toHaveBeenCalled(); + }); + + it('does not call onChange when loading', () => { + const handleChange = jest.fn(); + render(); + + const switchElement = screen.getByTestId('test-switch'); + fireEvent.click(switchElement); + + expect(handleChange).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/src/components/ui/Switch.tsx b/src/components/ui/Switch.tsx new file mode 100644 index 000000000..4858a6982 --- /dev/null +++ b/src/components/ui/Switch.tsx @@ -0,0 +1,115 @@ +import React, { useState, useEffect } from 'react'; +import { Switch as HeadlessSwitch } from '@headlessui/react'; +import cx from 'classnames'; + +export interface Props { + className?: string; + checked?: boolean; + defaultChecked?: boolean; + onChange?: (checked: boolean) => void; + disabled?: boolean; + size?: 'small' | 'default' | 'large'; + checkedChildren?: React.ReactNode; + unCheckedChildren?: React.ReactNode; + loading?: boolean; + 'data-testid'?: string; + label?: string; + id?: string; +} + +const Switch = ({ + className, + checked, + defaultChecked = false, + onChange, + disabled = false, + size = 'default', + checkedChildren, + unCheckedChildren, + loading = false, + 'data-testid': testId, + label, + id, +}: Props) => { + // Handle controlled vs uncontrolled + const [innerChecked, setInnerChecked] = useState(defaultChecked); + const isControlled = checked !== undefined; + const isChecked = isControlled ? checked : innerChecked; + + // Sync with external value changes + useEffect(() => { + if (isControlled && checked !== undefined) { + setInnerChecked(checked); + } + }, [isControlled, checked]); + + const handleChange = (newChecked: boolean) => { + if (!disabled && !loading) { + if (!isControlled) { + setInnerChecked(newChecked); + } + + if (onChange) { + onChange(newChecked); + } + } + }; + + return ( +
+ {label && ( + + )} + + + + + {(checkedChildren || unCheckedChildren) && ( + + {isChecked ? checkedChildren : unCheckedChildren} + + )} + +
+ ); +}; + +export default Switch; \ No newline at end of file diff --git a/src/components/ui/__snapshots__/Switch.test.tsx.snap b/src/components/ui/__snapshots__/Switch.test.tsx.snap new file mode 100644 index 000000000..962f74100 --- /dev/null +++ b/src/components/ui/__snapshots__/Switch.test.tsx.snap @@ -0,0 +1,292 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Switch Component renders Switch checked unchanged 1`] = ` +
+
+ +
+
+`; + +exports[`Switch Component renders Switch disabled and checked unchanged 1`] = ` +
+
+ +
+
+`; + +exports[`Switch Component renders Switch disabled unchanged 1`] = ` +
+
+ +
+
+`; + +exports[`Switch Component renders Switch unchanged 1`] = ` +
+
+ +
+
+`; + +exports[`Switch Component renders Switch with checkedChildren and unCheckedChildren unchanged 1`] = ` +
+
+ +
+
+`; + +exports[`Switch Component renders Switch with data-testid unchanged 1`] = ` +
+
+ +
+
+`; + +exports[`Switch Component renders Switch with defaultChecked unchanged 1`] = ` +
+
+ +
+
+`; + +exports[`Switch Component renders Switch with label unchanged 1`] = ` +
+
+ + +
+
+`; + +exports[`Switch Component renders Switch with loading state unchanged 1`] = ` +
+
+ +
+
+`; + +exports[`Switch Component renders Switch with size large unchanged 1`] = ` +
+
+ +
+
+`; + +exports[`Switch Component renders Switch with size small unchanged 1`] = ` +
+
+ +
+
+`; diff --git a/src/styles.css b/src/styles.css index 2fc0dc73d..c41d8e231 100644 --- a/src/styles.css +++ b/src/styles.css @@ -12,6 +12,7 @@ @import url('./components/ui/Expandable.css'); @import url('./components/ui/Slider.css'); @import url('./components/ui/Alert.css'); +@import url('./components/ui/Switch.css'); @import url('./components/CompletionProviderStatus/CompletionProviderStatus.css'); @import url('./components/PoweredBy/PoweredBy.css');