Skip to content

Commit 5dc39b7

Browse files
[ui-storagebrowser] refactor browser actions (#3915)
* [ui-storagebrowser] refactor browser actions * moves create file and folder actions to separate folder * fix fileChooser unintentional render * revert extra merge changes
1 parent 2331571 commit 5dc39b7

File tree

14 files changed

+561
-367
lines changed

14 files changed

+561
-367
lines changed

desktop/core/src/desktop/js/apps/storageBrowser/FileChooserModal/FileChooserModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import Table, { ColumnProps } from 'cuix/dist/components/Table';
2121
import classNames from 'classnames';
2222

2323
import FolderIcon from '@cloudera/cuix-core/icons/react/ProjectIcon';
24-
import { FileOutlined } from '@ant-design/icons';
24+
import FileIcon from '@cloudera/cuix-core/icons/react/DocumentationIcon';
2525

2626
import { i18nReact } from '../../../utils/i18nReact';
2727
import useDebounce from '../../../utils/useDebounce';
@@ -105,7 +105,7 @@ const FileChooserModal = ({
105105
column.render = (_, record: FileChooserTableData) => (
106106
<Tooltip title={record.name} mouseEnterDelay={1.5}>
107107
<span className="hue-filechooser-modal__table-cell-icon">
108-
{record.type === 'dir' ? <FolderIcon /> : <FileOutlined />}
108+
{record.type === 'dir' ? <FolderIcon /> : <FileIcon />}
109109
</span>
110110
<span className="hue-filechooser-modal__table-cell-name">{record.name}</span>
111111
</Tooltip>

desktop/core/src/desktop/js/apps/storageBrowser/InputModal/InputModal.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import './InputModal.scss';
2323

2424
interface InputModalProps {
2525
cancelText?: string;
26-
initialValue: string | number;
26+
initialValue?: string | number;
2727
inputLabel: string;
28-
inputType: 'text' | 'number';
28+
inputType?: 'text' | 'number';
2929
onClose: () => void;
3030
onSubmit: (value: string | number) => void;
3131
submitText?: string;
@@ -35,8 +35,8 @@ interface InputModalProps {
3535

3636
const InputModal = ({
3737
inputLabel,
38-
inputType,
39-
initialValue,
38+
inputType = 'text',
39+
initialValue = '',
4040
onClose,
4141
onSubmit,
4242
showModal,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
@use 'mixins';
17+
18+
$action-dropdown-width: 214px;
19+
20+
.hue-storage-browser__actions-dropdown {
21+
width: $action-dropdown-width;
22+
@include mixins.hue-svg-icon__d3-conflict;
23+
}
24+
25+
.hue-file-upload-modal {
26+
height: 200px;
27+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import CreateAndUploadAction from './CreateAndUploadAction';
5+
import {
6+
CREATE_DIRECTORY_API_URL,
7+
CREATE_FILE_API_URL
8+
} from '../../../../reactComponents/FileChooser/api';
9+
10+
const mockSave = jest.fn();
11+
jest.mock('../../../../utils/hooks/useSaveData', () => ({
12+
__esModule: true,
13+
default: jest.fn(() => ({
14+
save: mockSave
15+
}))
16+
}));
17+
18+
jest.mock('../../../../utils/huePubSub', () => ({
19+
__esModule: true,
20+
publish: jest.fn()
21+
}));
22+
23+
describe('CreateAndUploadAction', () => {
24+
const currentPath = '/some/path';
25+
const onSuccessfulAction = jest.fn();
26+
const setLoadingFiles = jest.fn();
27+
28+
beforeEach(() => {
29+
render(
30+
<CreateAndUploadAction
31+
currentPath={currentPath}
32+
onSuccessfulAction={onSuccessfulAction}
33+
setLoadingFiles={setLoadingFiles}
34+
/>
35+
);
36+
});
37+
38+
it('should render the dropdown with actions', async () => {
39+
const newButton = screen.getByText('New');
40+
expect(newButton).toBeInTheDocument();
41+
42+
await act(async () => fireEvent.click(newButton));
43+
44+
// Check that the "Create" and "Upload" groups are in the dropdown
45+
expect(screen.getByText('CREATE')).toBeInTheDocument();
46+
expect(screen.getByText('UPLOAD')).toBeInTheDocument();
47+
});
48+
49+
it('should open the folder creation modal when "New Folder" is clicked', async () => {
50+
const newButton = screen.getByText('New');
51+
await act(async () => fireEvent.click(newButton));
52+
53+
const newFolderButton = screen.getByText('New Folder');
54+
await act(async () => fireEvent.click(newFolderButton));
55+
56+
expect(screen.getByText('Create New Folder')).toBeInTheDocument();
57+
});
58+
59+
it('should open the file creation modal when "New File" is clicked', async () => {
60+
const newButton = screen.getByText('New');
61+
await act(async () => fireEvent.click(newButton));
62+
63+
const newFileButton = screen.getByText('New File');
64+
await act(async () => fireEvent.click(newFileButton));
65+
66+
expect(screen.getByText('Create New File')).toBeInTheDocument();
67+
});
68+
69+
it('should open the upload file modal when "New Upload" is clicked', async () => {
70+
const newButton = screen.getByText('New');
71+
await act(async () => fireEvent.click(newButton));
72+
73+
const newUploadButton = screen.getByText('New Upload');
74+
fireEvent.click(newUploadButton);
75+
76+
// Check if the upload modal is opened
77+
expect(screen.getByText('Upload A File')).toBeInTheDocument();
78+
});
79+
80+
it('should call the correct API for creating a folder', async () => {
81+
const newButton = screen.getByText('New');
82+
await act(async () => fireEvent.click(newButton));
83+
84+
const newFolderButton = screen.getByText('New Folder');
85+
await act(async () => fireEvent.click(newFolderButton));
86+
87+
const input = screen.getByRole('textbox');
88+
fireEvent.change(input, { target: { value: 'Test Folder' } });
89+
90+
const createButton = screen.getByText('Create');
91+
fireEvent.click(createButton);
92+
93+
await waitFor(() => {
94+
expect(mockSave).toHaveBeenCalledWith(
95+
{ path: currentPath, name: 'Test Folder' },
96+
{ url: CREATE_DIRECTORY_API_URL } // This URL is assumed from the code.
97+
);
98+
});
99+
});
100+
101+
it('should call the correct API for creating a file', async () => {
102+
const newButton = screen.getByText('New');
103+
await act(async () => fireEvent.click(newButton));
104+
105+
const newFileButton = screen.getByText('New File');
106+
await act(async () => fireEvent.click(newFileButton));
107+
108+
// Simulate file name submission
109+
const input = screen.getByRole('textbox');
110+
fireEvent.change(input, { target: { value: 'Test File' } });
111+
112+
const createButton = screen.getByText('Create');
113+
fireEvent.click(createButton);
114+
115+
await waitFor(() => {
116+
expect(mockSave).toHaveBeenCalledWith(
117+
{ path: currentPath, name: 'Test File' },
118+
{ url: CREATE_FILE_API_URL } // This URL is assumed from the code.
119+
);
120+
});
121+
});
122+
});
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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, { useState } from 'react';
18+
import { Dropdown } from 'antd';
19+
import { MenuItemGroupType } from 'antd/lib/menu/hooks/useItems';
20+
import Modal from 'cuix/dist/components/Modal';
21+
import FolderIcon from '@cloudera/cuix-core/icons/react/ProjectIcon';
22+
import FileIcon from '@cloudera/cuix-core/icons/react/DocumentationIcon';
23+
import DropDownIcon from '@cloudera/cuix-core/icons/react/DropdownIcon';
24+
import ImportIcon from '@cloudera/cuix-core/icons/react/ImportIcon';
25+
import { PrimaryButton } from 'cuix/dist/components/Button';
26+
27+
import { i18nReact } from '../../../../utils/i18nReact';
28+
import huePubSub from '../../../../utils/huePubSub';
29+
import {
30+
CREATE_DIRECTORY_API_URL,
31+
CREATE_FILE_API_URL
32+
} from '../../../../reactComponents/FileChooser/api';
33+
import { FileStats } from '../../../../reactComponents/FileChooser/types';
34+
import useSaveData from '../../../../utils/hooks/useSaveData';
35+
import InputModal from '../../InputModal/InputModal';
36+
import './CreateAndUploadAction.scss';
37+
import DragAndDrop from '../../../../reactComponents/DragAndDrop/DragAndDrop';
38+
39+
interface CreateAndUploadActionProps {
40+
currentPath: FileStats['path'];
41+
onSuccessfulAction: () => void;
42+
setLoadingFiles: (value: boolean) => void;
43+
}
44+
45+
enum ActionType {
46+
createFile = 'createFile',
47+
createFolder = 'createFolder',
48+
uploadFile = 'uploadFile'
49+
}
50+
51+
const CreateAndUploadAction = ({
52+
currentPath,
53+
onSuccessfulAction,
54+
setLoadingFiles
55+
}: CreateAndUploadActionProps): JSX.Element => {
56+
const { t } = i18nReact.useTranslation();
57+
58+
const [selectedAction, setSelectedAction] = useState<ActionType>();
59+
60+
const onApiSuccess = () => {
61+
setLoadingFiles(false);
62+
onSuccessfulAction();
63+
};
64+
65+
const onApiError = (error: Error) => {
66+
setLoadingFiles(false);
67+
huePubSub.publish('hue.error', error);
68+
};
69+
70+
const { save } = useSaveData(undefined, {
71+
onSuccess: onApiSuccess,
72+
onError: onApiError
73+
});
74+
75+
const onActionClick = (action: ActionType) => () => {
76+
setSelectedAction(action);
77+
};
78+
79+
const onModalClose = () => {
80+
setSelectedAction(undefined);
81+
};
82+
83+
const newActionsMenuItems: MenuItemGroupType[] = [
84+
{
85+
key: 'create',
86+
type: 'group',
87+
label: t('CREATE'),
88+
children: [
89+
{
90+
icon: <FileIcon />,
91+
key: ActionType.createFile,
92+
label: t('New File'),
93+
onClick: onActionClick(ActionType.createFile)
94+
},
95+
{
96+
icon: <FolderIcon />,
97+
key: ActionType.createFolder,
98+
label: t('New Folder'),
99+
onClick: onActionClick(ActionType.createFolder)
100+
}
101+
]
102+
},
103+
{
104+
key: 'upload',
105+
type: 'group',
106+
label: t('UPLOAD'),
107+
children: [
108+
{
109+
icon: <ImportIcon />,
110+
key: ActionType.uploadFile,
111+
label: t('New Upload'),
112+
onClick: onActionClick(ActionType.uploadFile)
113+
}
114+
]
115+
}
116+
];
117+
118+
const handleCreate = (name: string | number) => {
119+
const url = {
120+
[ActionType.createFile]: CREATE_FILE_API_URL,
121+
[ActionType.createFolder]: CREATE_DIRECTORY_API_URL
122+
}[selectedAction ?? ''];
123+
124+
if (!url) {
125+
return;
126+
}
127+
setLoadingFiles(true);
128+
save({ path: currentPath, name }, { url });
129+
};
130+
131+
return (
132+
<>
133+
<Dropdown
134+
overlayClassName="hue-storage-browser__actions-dropdown"
135+
menu={{
136+
items: newActionsMenuItems,
137+
className: 'hue-storage-browser__action-menu'
138+
}}
139+
trigger={['click']}
140+
>
141+
<PrimaryButton data-event="">
142+
{t('New')}
143+
<DropDownIcon />
144+
</PrimaryButton>
145+
</Dropdown>
146+
<InputModal
147+
title={t('Create New Folder')}
148+
inputLabel={t('Enter New Folder Name')}
149+
submitText={t('Create')}
150+
showModal={selectedAction === ActionType.createFolder}
151+
onSubmit={handleCreate}
152+
onClose={onModalClose}
153+
/>
154+
<InputModal
155+
title={t('Create New File')}
156+
inputLabel={t('Enter New File Name')}
157+
submitText={t('Create')}
158+
showModal={selectedAction === ActionType.createFile}
159+
onSubmit={handleCreate}
160+
onClose={onModalClose}
161+
/>
162+
<Modal
163+
onCancel={onModalClose}
164+
className="cuix antd"
165+
open={selectedAction === ActionType.uploadFile}
166+
title={t('Upload A File')}
167+
>
168+
<div className="hue-file-upload-modal">
169+
<DragAndDrop onDrop={() => {}} />
170+
</div>
171+
</Modal>
172+
</>
173+
);
174+
};
175+
176+
export default CreateAndUploadAction;

0 commit comments

Comments
 (0)