Skip to content

Commit 96baf7e

Browse files
[ui-importer] fix: settings tab for max width in input fields (#4288)
1 parent 272ef7a commit 96baf7e

6 files changed

Lines changed: 391 additions & 6 deletions

File tree

desktop/core/src/desktop/js/apps/newimporter/SettingsTab/SettingsTab.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
display: flex;
2929
flex-direction: column;
3030
gap: vars.$cdl-spacing-m;
31+
max-width: 1024px;
32+
33+
@media (width <= 1024px) {
34+
width: 100%;
35+
}
3136
}
3237

3338
&__section {
@@ -47,6 +52,10 @@
4752
flex: 1;
4853
width: 100%;
4954
gap: vars.$cdl-spacing-m;
55+
56+
@media (width <= 640px) {
57+
flex-direction: column;
58+
}
5059
}
5160

5261
&__fields {

desktop/core/src/desktop/js/apps/newimporter/SettingsTab/SettingsTabConfig.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const ADVANCED_SETTINGS_CONFIG: Record<string, SettingsFieldConfig[]> = {
5151
description: [
5252
{
5353
name: 'description',
54-
type: FieldType.INPUT,
54+
type: FieldType.TEXTAREA,
5555
label: 'Description',
5656
placeholder: "A table to store customer data imported from the marketing team's CRM.",
5757
tooltip:
@@ -118,7 +118,7 @@ export const ADVANCED_SETTINGS_CONFIG: Record<string, SettingsFieldConfig[]> = {
118118
},
119119
{
120120
name: 'externalLocation',
121-
type: FieldType.INPUT,
121+
type: FieldType.FILECHOOSER,
122122
placeholder: 'External location',
123123
isHidden: (context: SettingsContext) => !context.useExternalLocation
124124
}
@@ -139,7 +139,6 @@ export const ADVANCED_SETTINGS_CONFIG: Record<string, SettingsFieldConfig[]> = {
139139
label: 'Field',
140140
placeholder: 'Choose an option',
141141
options: DELIMITER_OPTIONS,
142-
tooltip: 'Field delimiter',
143142
isHidden: (context: SettingsContext) => !context.customCharDelimiters
144143
},
145144
{
@@ -148,7 +147,6 @@ export const ADVANCED_SETTINGS_CONFIG: Record<string, SettingsFieldConfig[]> = {
148147
label: 'Array Map',
149148
placeholder: 'Choose an option',
150149
options: DELIMITER_OPTIONS,
151-
tooltip: 'Array map delimiter',
152150
isHidden: (context: SettingsContext) => !context.customCharDelimiters
153151
},
154152
{
@@ -157,7 +155,6 @@ export const ADVANCED_SETTINGS_CONFIG: Record<string, SettingsFieldConfig[]> = {
157155
label: 'Struct',
158156
placeholder: 'Choose an option',
159157
options: DELIMITER_OPTIONS,
160-
tooltip: 'Struct delimiter',
161158
isHidden: (context: SettingsContext) => !context.customCharDelimiters
162159
}
163160
]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to Cloudera, Inc. under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. Cloudera, Inc. licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
@use 'variables' as vars;
18+
19+
.antd.cuix {
20+
.hue-form-input__file-chooser {
21+
display: flex;
22+
align-items: stretch;
23+
position: relative;
24+
25+
input {
26+
flex: 1;
27+
border-top-right-radius: 0;
28+
border-bottom-right-radius: 0;
29+
}
30+
31+
&__button {
32+
white-space: nowrap;
33+
flex-shrink: 0;
34+
border-top-left-radius: 0;
35+
border-bottom-left-radius: 0;
36+
margin-left: -1px;
37+
}
38+
}
39+
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// Licensed to Cloudera, Inc. under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. Cloudera, Inc. licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
import React from 'react';
18+
import { render, screen, waitFor } from '@testing-library/react';
19+
import userEvent from '@testing-library/user-event';
20+
import '@testing-library/jest-dom';
21+
import FileChooserInput from './FileChooserInput';
22+
23+
jest.mock('../../../utils/hooks/useLoadData/useLoadData', () => ({
24+
__esModule: true,
25+
default: jest.fn()
26+
}));
27+
28+
import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
29+
30+
const mockUseLoadData = useLoadData as jest.MockedFunction<typeof useLoadData>;
31+
32+
describe('FileChooserInput', () => {
33+
const mockOnChange = jest.fn();
34+
35+
const mockDirectoryData = {
36+
files: [
37+
{
38+
path: '/test/directory/file1.txt',
39+
type: 'file',
40+
size: 1024,
41+
mtime: Date.now(),
42+
user: 'testuser',
43+
group: 'testgroup',
44+
rwx: 'rw-r--r--',
45+
mode: 644,
46+
atime: Date.now(),
47+
blockSize: 4096,
48+
replication: 3
49+
},
50+
{
51+
path: '/test/directory/file2.txt',
52+
type: 'file',
53+
size: 2048,
54+
mtime: Date.now(),
55+
user: 'testuser',
56+
group: 'testgroup',
57+
rwx: 'rw-r--r--',
58+
mode: 644,
59+
atime: Date.now(),
60+
blockSize: 4096,
61+
replication: 3
62+
},
63+
{
64+
path: '/test/directory/subfolder',
65+
type: 'dir',
66+
size: 0,
67+
mtime: Date.now(),
68+
user: 'testuser',
69+
group: 'testgroup',
70+
rwx: 'rwxr-xr-x',
71+
mode: 755,
72+
atime: Date.now(),
73+
blockSize: 0,
74+
replication: 0
75+
}
76+
],
77+
page: {
78+
number: 1,
79+
numPages: 1,
80+
startIndex: 0,
81+
endIndex: 3,
82+
totalRecords: 3,
83+
pageSize: 1000,
84+
previousPage: 1,
85+
nextPage: 1
86+
}
87+
};
88+
89+
beforeEach(() => {
90+
jest.clearAllMocks();
91+
mockUseLoadData.mockReturnValue({
92+
data: mockDirectoryData,
93+
loading: false,
94+
error: undefined,
95+
reloadData: jest.fn()
96+
});
97+
});
98+
99+
it('should render input field with value', () => {
100+
render(<FileChooserInput value="/test/path" onChange={mockOnChange} />);
101+
102+
const input = screen.getByRole('textbox');
103+
expect(input).toBeInTheDocument();
104+
expect(input).toHaveValue('/test/path');
105+
});
106+
107+
it('should render Choose button', () => {
108+
render(<FileChooserInput value="" onChange={mockOnChange} />);
109+
110+
const button = screen.getByRole('button', { name: 'Choose' });
111+
expect(button).toBeInTheDocument();
112+
expect(button).toHaveTextContent('Choose');
113+
});
114+
115+
it('should render with placeholder', () => {
116+
render(<FileChooserInput value="" onChange={mockOnChange} placeholder="Enter file path" />);
117+
118+
const input = screen.getByRole('textbox');
119+
expect(input).toHaveAttribute('placeholder', 'Enter file path');
120+
});
121+
122+
it('should call onChange when typing in input field', async () => {
123+
const user = userEvent.setup();
124+
render(<FileChooserInput value="" onChange={mockOnChange} />);
125+
126+
const input = screen.getByRole('textbox');
127+
await user.type(input, '/new/path');
128+
129+
expect(mockOnChange).toHaveBeenCalled();
130+
});
131+
132+
it('should show error status when error prop is true', () => {
133+
render(<FileChooserInput value="" onChange={mockOnChange} error={true} />);
134+
135+
const input = screen.getByRole('textbox');
136+
expect(input).toHaveClass('ant-input-status-error');
137+
});
138+
139+
it('should open FileChooserModal when Choose button is clicked', async () => {
140+
const user = userEvent.setup();
141+
render(<FileChooserInput value="/initial/path" onChange={mockOnChange} />);
142+
143+
const button = screen.getByRole('button', { name: 'Choose' });
144+
await user.click(button);
145+
146+
await waitFor(() => {
147+
expect(screen.getByText('Choose a file')).toBeInTheDocument();
148+
});
149+
});
150+
151+
it('should update value and close modal when file is selected', async () => {
152+
const user = userEvent.setup();
153+
render(<FileChooserInput value="" onChange={mockOnChange} />);
154+
155+
const button = screen.getByRole('button', { name: 'Choose' });
156+
await user.click(button);
157+
158+
await waitFor(() => {
159+
expect(screen.getByText('Choose a file')).toBeInTheDocument();
160+
});
161+
162+
await waitFor(() => {
163+
expect(screen.getByText('file1.txt')).toBeInTheDocument();
164+
});
165+
166+
const fileRow = screen.getByText('file1.txt');
167+
await user.click(fileRow);
168+
169+
await waitFor(() => {
170+
expect(mockOnChange).toHaveBeenCalledWith('/test/directory/file1.txt');
171+
});
172+
173+
await waitFor(() => {
174+
expect(screen.queryByText('Choose a file')).not.toBeInTheDocument();
175+
});
176+
});
177+
178+
it('should close modal when cancel is clicked', async () => {
179+
const user = userEvent.setup();
180+
render(<FileChooserInput value="" onChange={mockOnChange} />);
181+
182+
const button = screen.getByRole('button', { name: 'Choose' });
183+
await user.click(button);
184+
185+
await waitFor(() => {
186+
expect(screen.getByText('Choose a file')).toBeInTheDocument();
187+
});
188+
189+
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
190+
await user.click(cancelButton);
191+
192+
await waitFor(() => {
193+
expect(screen.queryByText('Choose a file')).not.toBeInTheDocument();
194+
});
195+
});
196+
197+
it('should use root path "/" as default sourcePath when value is empty', async () => {
198+
const user = userEvent.setup();
199+
render(<FileChooserInput value="" onChange={mockOnChange} />);
200+
201+
const button = screen.getByRole('button', { name: 'Choose' });
202+
await user.click(button);
203+
204+
await waitFor(() => {
205+
expect(screen.getByText('Choose a file')).toBeInTheDocument();
206+
});
207+
208+
await waitFor(() => {
209+
expect(mockUseLoadData).toHaveBeenCalledWith(
210+
expect.any(String),
211+
expect.objectContaining({
212+
params: expect.objectContaining({
213+
path: '/'
214+
})
215+
})
216+
);
217+
});
218+
});
219+
220+
it('should use existing value as sourcePath when available', async () => {
221+
const user = userEvent.setup();
222+
render(<FileChooserInput value="/existing/path" onChange={mockOnChange} />);
223+
224+
const button = screen.getByRole('button', { name: 'Choose' });
225+
await user.click(button);
226+
227+
await waitFor(() => {
228+
expect(screen.getByText('Choose a file')).toBeInTheDocument();
229+
});
230+
231+
await waitFor(() => {
232+
expect(mockUseLoadData).toHaveBeenCalledWith(
233+
expect.any(String),
234+
expect.objectContaining({
235+
params: expect.objectContaining({
236+
path: '/existing/path'
237+
})
238+
})
239+
);
240+
});
241+
});
242+
});

0 commit comments

Comments
 (0)