Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -35,8 +36,31 @@ export const FormStringInput: FC<TFormStringInputProps> = ({
onRemoveByMinus,
}) => {
const designNewLayout = useDesignNewLayout()
const [isMultiline, setIsMultiline] = useState(false)
const [currentValue, setCurrentValue] = useState<string>('')

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 = (
<>
Expand Down Expand Up @@ -72,7 +96,50 @@ export const FormStringInput: FC<TFormStringInputProps> = ({
validateTrigger="onBlur"
hasFeedback={designNewLayout ? { icons: feedbackIcons } : true}
>
<Input placeholder={getStringByName(name)} />
<Input.TextArea
placeholder={getStringByName(name)}
rows={isMultiline ? 4 : 1}
autoSize={!isMultiline ? { minRows: 1, maxRows: 1 } : { minRows: 2, maxRows: 10 }}
onChange={(e) => {
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)
}
}}
/>
</ResetedFormItem>
</HiddenContainer>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -11,11 +12,36 @@ type TYamlEditProps = {
onChange: (values: Record<string, unknown>) => 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<TYamlEditProps> = ({ theme, currentValues, onChange }) => {
const [yamlData, setYamlData] = useState<string>('')

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 (
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './createContextFactory'
export * from './prepareUrlsToFetchForDynamicRenderer'
export * from './deepMerge'
export * from './getSortedKinds'
export * from './isMultilineString'
1 change: 1 addition & 0 deletions src/utils/isMultilineString/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { isMultilineString, isMultilineFromYaml } from './isMultilineString'
60 changes: 60 additions & 0 deletions src/utils/isMultilineString/isMultilineString.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
70 changes: 70 additions & 0 deletions src/utils/isMultilineString/isMultilineString.ts
Original file line number Diff line number Diff line change
@@ -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
}
}