Skip to content

Commit 5b6d74e

Browse files
feat(ui): add support for textarea config option (#2447)
* add support for textarea config option * add new line * remove default placeholders for text and textarea
1 parent f0d9f28 commit 5b6d74e

File tree

4 files changed

+166
-5
lines changed

4 files changed

+166
-5
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { ChangeEvent, CSSProperties } from 'react';
2+
import { useSettings } from '../../contexts/SettingsContext';
3+
4+
interface TextareaProps {
5+
id: string;
6+
label: string;
7+
value: string;
8+
onChange: (e: ChangeEvent<HTMLTextAreaElement>) => void;
9+
rows?: number;
10+
placeholder?: string;
11+
required?: boolean;
12+
disabled?: boolean;
13+
error?: string;
14+
helpText?: string;
15+
className?: string;
16+
labelClassName?: string;
17+
dataTestId?: string;
18+
}
19+
20+
const Textarea = ({
21+
id,
22+
label,
23+
value,
24+
onChange,
25+
rows = 4,
26+
placeholder = '',
27+
required = false,
28+
disabled = false,
29+
error,
30+
helpText,
31+
className = '',
32+
labelClassName = '',
33+
dataTestId,
34+
}: TextareaProps) => {
35+
const { settings } = useSettings();
36+
const themeColor = settings.themeColor;
37+
38+
return (
39+
<div className="mb-4">
40+
<label htmlFor={id} className={`block text-sm font-medium text-gray-700 mb-1 ${labelClassName}`}>
41+
{label}
42+
{required && <span className="text-red-500 ml-1">*</span>}
43+
</label>
44+
<textarea
45+
id={id}
46+
value={value}
47+
onChange={onChange}
48+
rows={rows}
49+
placeholder={placeholder}
50+
disabled={disabled}
51+
required={required}
52+
className={`w-full px-3 py-2 border ${
53+
error ? 'border-red-500' : 'border-gray-300'
54+
} rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 ${
55+
disabled ? 'bg-gray-100 text-gray-500' : 'bg-white'
56+
} ${className}`}
57+
style={{
58+
'--tw-ring-color': themeColor,
59+
'--tw-ring-offset-color': themeColor,
60+
} as CSSProperties}
61+
data-testid={dataTestId}
62+
/>
63+
{error && <p className="mt-1 text-sm text-red-500">{error}</p>}
64+
{helpText && !error && <p className="mt-1 text-sm text-gray-500">{helpText}</p>}
65+
</div>
66+
);
67+
};
68+
69+
export default Textarea;

web/src/components/wizard/config/ConfigurationStep.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useQuery, useMutation } from '@tanstack/react-query';
33
import Card from '../../common/Card';
44
import Button from '../../common/Button';
55
import Input from '../../common/Input';
6+
import Textarea from '../../common/Textarea';
67
import { useWizard } from '../../../contexts/WizardModeContext';
78
import { useAuth } from '../../../contexts/AuthContext';
89
import { useSettings } from '../../../contexts/SettingsContext';
@@ -141,12 +142,22 @@ const ConfigurationStep: React.FC<ConfigurationStepProps> = ({ onNext }) => {
141142
id={item.name}
142143
label={item.title}
143144
value={item.value || ''}
144-
placeholder={item.default}
145145
onChange={handleInputChange}
146146
dataTestId={`text-input-${item.name}`}
147147
/>
148148
);
149149

150+
case 'textarea':
151+
return (
152+
<Textarea
153+
id={item.name}
154+
label={item.title}
155+
value={item.value || ''}
156+
onChange={handleInputChange}
157+
dataTestId={`textarea-input-${item.name}`}
158+
/>
159+
);
160+
150161
case 'bool':
151162
return (
152163
<div className="flex items-center space-x-3">

web/src/components/wizard/tests/ConfigurationStep.test.tsx

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ const MOCK_APP_CONFIG: AppConfig = {
2020
value: "My App",
2121
default: "Default App"
2222
},
23+
{
24+
name: "description",
25+
title: "Application Description",
26+
type: "textarea",
27+
value: "This is my application\nIt does amazing things",
28+
default: "Enter description here..."
29+
},
2330
{
2431
name: "enable_feature",
2532
title: "Enable Feature",
@@ -57,6 +64,13 @@ const MOCK_APP_CONFIG: AppConfig = {
5764
type: "text",
5865
value: "localhost",
5966
default: "localhost"
67+
},
68+
{
69+
name: "db_config",
70+
title: "Database Configuration",
71+
type: "textarea",
72+
value: "",
73+
default: "# Database configuration\nhost: localhost\nport: 5432"
6074
}
6175
]
6276
}
@@ -143,11 +157,13 @@ describe.each([
143157

144158
// Check that form fields are rendered for the active tab
145159
expect(screen.getByTestId("config-item-app_name")).toBeInTheDocument();
160+
expect(screen.getByTestId("config-item-description")).toBeInTheDocument();
146161
expect(screen.getByTestId("config-item-enable_feature")).toBeInTheDocument();
147162
expect(screen.getByTestId("config-item-auth_type")).toBeInTheDocument();
148163

149164
// Check that the database tab is not rendered
150165
expect(screen.queryByTestId("config-item-db_host")).not.toBeInTheDocument();
166+
expect(screen.queryByTestId("config-item-db_config")).not.toBeInTheDocument();
151167

152168
// Check next button
153169
const nextButton = screen.getByTestId("config-next-button");
@@ -224,20 +240,24 @@ describe.each([
224240

225241
// Initially, Settings tab should be active
226242
expect(screen.getByTestId("config-item-app_name")).toBeInTheDocument();
243+
expect(screen.getByTestId("config-item-description")).toBeInTheDocument();
227244
expect(screen.getByTestId("config-item-enable_feature")).toBeInTheDocument();
228245
expect(screen.getByTestId("config-item-auth_type")).toBeInTheDocument();
229246

230247
// Check that the database tab is not rendered
231248
expect(screen.queryByTestId("config-item-db_host")).not.toBeInTheDocument();
249+
expect(screen.queryByTestId("config-item-db_config")).not.toBeInTheDocument();
232250

233251
// Click on Database tab
234252
fireEvent.click(screen.getByTestId("config-tab-database"));
235253

236254
// Database tab content should be visible
237255
expect(screen.getByTestId("config-item-db_host")).toBeInTheDocument();
256+
expect(screen.getByTestId("config-item-db_config")).toBeInTheDocument();
238257

239258
// Settings tab content should not be visible
240259
expect(screen.queryByTestId("config-item-app_name")).not.toBeInTheDocument();
260+
expect(screen.queryByTestId("config-item-description")).not.toBeInTheDocument();
241261
expect(screen.queryByTestId("config-item-enable_feature")).not.toBeInTheDocument();
242262
expect(screen.queryByTestId("config-item-auth_type")).not.toBeInTheDocument();
243263
});
@@ -263,6 +283,52 @@ describe.each([
263283
expect(appNameInput).toHaveValue("New App Name");
264284
});
265285

286+
it("handles textarea input changes correctly", async () => {
287+
renderWithProviders(<ConfigurationStep onNext={mockOnNext} />, {
288+
wrapperProps: {
289+
authenticated: true,
290+
target: target,
291+
},
292+
});
293+
294+
// Wait for loading to complete
295+
await waitFor(() => {
296+
expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument();
297+
});
298+
299+
// Find and update textarea input
300+
const descriptionTextarea = screen.getByTestId("textarea-input-description");
301+
fireEvent.change(descriptionTextarea, { target: { value: "New multi-line\ndescription text" } });
302+
303+
// Verify the value was updated
304+
expect(descriptionTextarea).toHaveValue("New multi-line\ndescription text");
305+
});
306+
307+
it("renders textarea with correct initial values", async () => {
308+
renderWithProviders(<ConfigurationStep onNext={mockOnNext} />, {
309+
wrapperProps: {
310+
authenticated: true,
311+
target: target,
312+
},
313+
});
314+
315+
// Wait for loading to complete
316+
await waitFor(() => {
317+
expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument();
318+
});
319+
320+
// Check textarea with value
321+
const descriptionTextarea = screen.getByTestId("textarea-input-description");
322+
expect(descriptionTextarea).toHaveValue("This is my application\nIt does amazing things");
323+
324+
// Switch to database tab
325+
fireEvent.click(screen.getByTestId("config-tab-database"));
326+
327+
// Check textarea with empty value
328+
const dbConfigTextarea = screen.getByTestId("textarea-input-db_config");
329+
expect(dbConfigTextarea).toHaveValue("");
330+
});
331+
266332
it("handles checkbox changes correctly", async () => {
267333
renderWithProviders(<ConfigurationStep onNext={mockOnNext} />, {
268334
wrapperProps: {
@@ -350,6 +416,9 @@ describe.each([
350416
const appNameInput = screen.getByTestId("text-input-app_name");
351417
fireEvent.change(appNameInput, { target: { value: "Updated App Name" } });
352418

419+
const descriptionTextarea = screen.getByTestId("textarea-input-description");
420+
fireEvent.change(descriptionTextarea, { target: { value: "Updated multi-line\ndescription text" } });
421+
353422
const enableFeatureCheckbox = screen.getByTestId("bool-input-enable_feature");
354423
fireEvent.click(enableFeatureCheckbox);
355424

@@ -364,6 +433,10 @@ describe.each([
364433
const dbHostInput = screen.getByTestId("text-input-db_host");
365434
fireEvent.change(dbHostInput, { target: { value: "Updated DB Host" } });
366435

436+
// Change textarea input
437+
const dbConfigTextarea = screen.getByTestId("textarea-input-db_config");
438+
fireEvent.change(dbConfigTextarea, { target: { value: "# Updated config\nhost: updated-host\nport: 5432" } });
439+
367440
// Submit form
368441
const nextButton = screen.getByTestId("config-next-button");
369442
fireEvent.click(nextButton);
@@ -381,9 +454,11 @@ describe.each([
381454
expect(submittedValues!).toMatchObject({
382455
values: {
383456
app_name: "Updated App Name",
457+
description: "Updated multi-line\ndescription text",
384458
enable_feature: "1",
385459
auth_type: "auth_type_anonymous",
386-
db_host: "Updated DB Host"
460+
db_host: "Updated DB Host",
461+
db_config: "# Updated config\nhost: updated-host\nport: 5432"
387462
}
388463
});
389464
});
@@ -438,6 +513,10 @@ describe.each([
438513
const appNameInput = screen.getByTestId("text-input-app_name");
439514
fireEvent.change(appNameInput, { target: { value: "Only Changed Field" } });
440515

516+
// Change the description textarea
517+
const descriptionTextarea = screen.getByTestId("textarea-input-description");
518+
fireEvent.change(descriptionTextarea, { target: { value: "Only changed description" } });
519+
441520
// Change the auth type
442521
const anonymousRadio = screen.getByTestId("radio-input-auth_type_anonymous");
443522
fireEvent.click(anonymousRadio);
@@ -459,11 +538,13 @@ describe.each([
459538
expect(submittedValues!).toMatchObject({
460539
values: {
461540
app_name: "Only Changed Field",
541+
description: "Only changed description",
462542
auth_type: "auth_type_anonymous"
463543
}
464544
});
465545
expect(submittedValues!.values).not.toHaveProperty("enable_feature");
466-
expect(submittedValues!.values).not.toHaveProperty("database_type");
546+
expect(submittedValues!.values).not.toHaveProperty("db_host");
547+
expect(submittedValues!.values).not.toHaveProperty("db_config");
467548
});
468549

469550
describe("Radio button behavior", () => {

web/src/vite.setup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { expect, vi } from "vitest";
2-
import matchers from "@testing-library/jest-dom/matchers";
1+
import { expect, vi, beforeEach, afterEach } from "vitest";
2+
import * as matchers from "@testing-library/jest-dom/matchers";
33
import { act } from "react";
44
import { faker } from "@faker-js/faker";
55

0 commit comments

Comments
 (0)