diff --git a/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx b/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx index 2dbbc92..32aedcf 100644 --- a/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx +++ b/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx @@ -1,7 +1,8 @@ /* eslint-disable no-nested-ternary */ -import React, { FC } from 'react' -import { Flex, Input, Typography, Tooltip, Button } from 'antd' +import React, { FC, useState, useEffect } from 'react' +import { Flex, Input, Typography, Tooltip, Button, Form } from 'antd' import { getStringByName } from 'utils/getStringByName' +import { isMultilineString } from 'utils/isMultilineString' import { TFormName, TPersistedControls } from 'localTypes/form' import { MinusIcon, feedbackIcons } from 'components/atoms' import { PersistedCheckbox, HiddenContainer, ResetedFormItem, CustomSizeTitle } from '../../atoms' @@ -35,8 +36,31 @@ export const FormStringInput: FC = ({ onRemoveByMinus, }) => { const designNewLayout = useDesignNewLayout() + const [isMultiline, setIsMultiline] = useState(false) + const [currentValue, setCurrentValue] = useState('') const fixedName = name === 'nodeName' ? 'nodeNameBecauseOfSuddenBug' : name + const formFieldName = arrName || fixedName + + // Watch the form field value + const formValue = Form.useWatch(formFieldName) + + // Initialize multiline state based on form value + useEffect(() => { + if (formValue && typeof formValue === 'string') { + setCurrentValue(formValue) + if (isMultilineString(formValue)) { + setIsMultiline(true) + } + } + }, [formValue]) + + // Check if the current value should be multiline + useEffect(() => { + if (currentValue && isMultilineString(currentValue)) { + setIsMultiline(true) + } + }, [currentValue]) const title = ( <> @@ -72,7 +96,50 @@ export const FormStringInput: FC = ({ validateTrigger="onBlur" hasFeedback={designNewLayout ? { icons: feedbackIcons } : true} > - + { + const value = e.target.value + setCurrentValue(value) + }} + onKeyPress={(e) => { + // If user presses Enter in single-line mode, switch to multiline + if (!isMultiline && e.key === 'Enter' && !e.shiftKey) { + // Don't prevent default - let the newline be added + setIsMultiline(true) + } + }} + onInput={(e) => { + // Handle input changes and check for newlines + const value = (e.target as HTMLTextAreaElement).value + setCurrentValue(value) + + // If we detect a newline and we're in single-line mode, switch to multiline + if (!isMultiline && value.includes('\n')) { + setIsMultiline(true) + } + }} + onBlur={(e) => { + // If the value becomes single line, switch back to single-line mode + if (isMultilineString(e.target.value)) { + setIsMultiline(true) + } else { + setIsMultiline(false) + } + }} + onPaste={(e) => { + // Handle paste of multiline content + const pastedText = e.clipboardData.getData('text') + if (pastedText && isMultilineString(pastedText)) { + // Let the default paste behavior happen, but ensure multiline mode + setTimeout(() => { + setIsMultiline(true) + }, 0) + } + }} + /> ) diff --git a/src/components/molecules/BlackholeForm/molecules/YamlEditor/YamlEditor.tsx b/src/components/molecules/BlackholeForm/molecules/YamlEditor/YamlEditor.tsx index 83cf3f8..ddb9d14 100644 --- a/src/components/molecules/BlackholeForm/molecules/YamlEditor/YamlEditor.tsx +++ b/src/components/molecules/BlackholeForm/molecules/YamlEditor/YamlEditor.tsx @@ -3,6 +3,7 @@ import React, { FC, useEffect, useState } from 'react' import Editor from '@monaco-editor/react' import * as yaml from 'yaml' +import { isMultilineString } from 'utils/isMultilineString' import { Styled } from './styled' type TYamlEditProps = { @@ -11,11 +12,36 @@ type TYamlEditProps = { onChange: (values: Record) => void } +// Function to process values and format multiline strings properly +const processValuesForYaml = (values: any): any => { + if (Array.isArray(values)) { + return values.map(processValuesForYaml) + } + + if (values && typeof values === 'object') { + const processed: any = {} + for (const [key, value] of Object.entries(values)) { + processed[key] = processValuesForYaml(value) + } + return processed + } + + return values +} + export const YamlEditor: FC = ({ theme, currentValues, onChange }) => { const [yamlData, setYamlData] = useState('') useEffect(() => { - setYamlData(yaml.stringify(currentValues)) + const yamlString = yaml.stringify(currentValues, { + // Use literal block scalar for multiline strings + blockQuote: 'literal', + // Preserve line breaks + lineWidth: 0, + // Use double quotes for strings that need escaping + doubleQuotedAsJSON: false, + }) + setYamlData(yamlString) }, [currentValues]) return ( diff --git a/src/utils/index.ts b/src/utils/index.ts index 5132ea6..be02e5c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,3 +12,4 @@ export * from './createContextFactory' export * from './prepareUrlsToFetchForDynamicRenderer' export * from './deepMerge' export * from './getSortedKinds' +export * from './isMultilineString' diff --git a/src/utils/isMultilineString/index.ts b/src/utils/isMultilineString/index.ts new file mode 100644 index 0000000..0a5f3e4 --- /dev/null +++ b/src/utils/isMultilineString/index.ts @@ -0,0 +1 @@ +export { isMultilineString, isMultilineFromYaml } from './isMultilineString' diff --git a/src/utils/isMultilineString/isMultilineString.test.ts b/src/utils/isMultilineString/isMultilineString.test.ts new file mode 100644 index 0000000..9855f34 --- /dev/null +++ b/src/utils/isMultilineString/isMultilineString.test.ts @@ -0,0 +1,60 @@ +import { isMultilineString, isMultilineFromYaml } from './isMultilineString' + +describe('isMultilineString', () => { + it('should return false for empty string', () => { + expect(isMultilineString('')).toBe(false) + }) + + it('should return false for null or undefined', () => { + expect(isMultilineString(null as any)).toBe(false) + expect(isMultilineString(undefined as any)).toBe(false) + }) + + it('should return true for strings with newlines', () => { + expect(isMultilineString('line1\nline2')).toBe(true) + expect(isMultilineString('line1\r\nline2')).toBe(true) + }) + + it('should return true for long strings', () => { + const longString = 'a'.repeat(81) + expect(isMultilineString(longString)).toBe(true) + }) + + it('should return true for strings with multiline indicators', () => { + expect(isMultilineString('#cloud-config\npassword: atomic')).toBe(true) + expect(isMultilineString('#!/bin/bash\necho "hello"')).toBe(true) + expect(isMultilineString('-----BEGIN CERTIFICATE-----')).toBe(true) + expect(isMultilineString('-----END CERTIFICATE-----')).toBe(true) + }) + + it('should return false for short single-line strings', () => { + expect(isMultilineString('hello')).toBe(false) + expect(isMultilineString('short string')).toBe(false) + }) +}) + +describe('isMultilineFromYaml', () => { + it('should return false for empty content', () => { + expect(isMultilineFromYaml('', ['field'])).toBe(false) + }) + + it('should return true for YAML with literal block scalar', () => { + const yamlContent = `field: | + #cloud-config + password: atomic + chpasswd: { expire: False }` + expect(isMultilineFromYaml(yamlContent, ['field'])).toBe(true) + }) + + it('should return true for YAML with folded block scalar', () => { + const yamlContent = `field: > + This is a long string + that should be folded` + expect(isMultilineFromYaml(yamlContent, ['field'])).toBe(true) + }) + + it('should return false for YAML with regular string', () => { + const yamlContent = `field: "regular string"` + expect(isMultilineFromYaml(yamlContent, ['field'])).toBe(false) + }) +}) diff --git a/src/utils/isMultilineString/isMultilineString.ts b/src/utils/isMultilineString/isMultilineString.ts new file mode 100644 index 0000000..b28e3f9 --- /dev/null +++ b/src/utils/isMultilineString/isMultilineString.ts @@ -0,0 +1,70 @@ +/** + * Determines if a string should be treated as multiline in YAML + * @param value - The string value to check + * @returns true if the string should be multiline, false otherwise + */ +export const isMultilineString = (value: string): boolean => { + if (!value || typeof value !== 'string') { + return false + } + + // Check if string contains newlines + if (value.includes('\n')) { + return true + } + + // Check if string is very long (more than 80 characters) + if (value.length > 80) { + return true + } + + // Check if string contains special characters that suggest multiline content + const multilineIndicators = [ + '#cloud-config', + '#!/', + '---', + '```', + 'BEGIN', + 'END', + '-----BEGIN', + '-----END', + ] + + return multilineIndicators.some(indicator => value.includes(indicator)) +} + +/** + * Determines if a string should be treated as multiline based on YAML content + * @param yamlContent - The YAML content to analyze + * @param fieldPath - The path to the field in the YAML + * @returns true if the field should be multiline, false otherwise + */ +export const isMultilineFromYaml = (yamlContent: string, fieldPath: string[]): boolean => { + if (!yamlContent || !fieldPath.length) { + return false + } + + try { + // Look for multiline indicators in the YAML content + const lines = yamlContent.split('\n') + const fieldName = fieldPath[fieldPath.length - 1] + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + // Check if this line contains our field + if (line.includes(`${fieldName}:`) && line.includes('|')) { + return true + } + + // Check if this line contains our field with literal block scalar + if (line.includes(`${fieldName}:`) && line.includes('>')) { + return true + } + } + + return false + } catch { + return false + } +}