소스 검색

[ui-core] adds drag and drop component (#3912)

* [ui-core] adds drag and drop component

* adds the npm package react-dropzone

* fix the spacing

* fix lint issue
Ram Prasad Agarwal 11 달 전
부모
커밋
470b515654

+ 49 - 0
desktop/core/src/desktop/js/reactComponents/DragAndDrop/DragAndDrop.scss

@@ -0,0 +1,49 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@use 'variables' as vars;
+
+.antd.cuix {
+  .drag-drop {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+
+    &__dropzone {
+      display: flex;
+      flex-direction: column;
+      flex: 1;
+      transition: background-color 0.3s ease;
+    }
+
+    &__message {
+      display: flex;
+      flex-direction: column;
+      flex: 1;
+      margin: 0;
+      font-size: 16px;
+      justify-content: center;
+      align-items: center;
+      border: 3px dashed vars.$fluidx-gray-300;
+      border-radius: 3px;
+      background-color: vars.$fluidx-gray-040;
+    }
+
+    &__input {
+      display: none; // Hide the default input
+    }
+  }
+}

+ 95 - 0
desktop/core/src/desktop/js/reactComponents/DragAndDrop/DragAndDrop.test.tsx

@@ -0,0 +1,95 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import { render, fireEvent, waitFor, act } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import DragAndDrop from './DragAndDrop';
+
+describe('DragAndDrop', () => {
+  const mockOnFilesDrop = jest.fn();
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should render the initial message when not dragging and children not present', () => {
+    const { getByText } = render(<DragAndDrop onDrop={mockOnFilesDrop} />);
+
+    expect(
+      getByText("Drag 'n' drop some files here, or click to select files")
+    ).toBeInTheDocument();
+  });
+
+  it('should render children when provided and not dragging', () => {
+    const { getByText } = render(
+      <DragAndDrop onDrop={mockOnFilesDrop}>
+        <div>Custom Child Element</div>
+      </DragAndDrop>
+    );
+
+    expect(getByText('Custom Child Element')).toBeInTheDocument();
+  });
+
+  it('should not display the default message when children are passed', () => {
+    const { queryByText, getByText } = render(
+      <DragAndDrop onDrop={mockOnFilesDrop}>
+        <div>Custom Child Element</div>
+      </DragAndDrop>
+    );
+
+    expect(
+      queryByText("Drag 'n' drop some files here, or click to select files")
+    ).not.toBeInTheDocument();
+    expect(getByText('Custom Child Element')).toBeInTheDocument();
+  });
+
+  it('should display the dragging message when dragging files', async () => {
+    const { getByText, getByTestId } = render(<DragAndDrop onDrop={mockOnFilesDrop} />);
+
+    await act(async () => fireEvent.dragEnter(getByTestId('drag-drop__input')));
+
+    await waitFor(() => {
+      expect(getByText('Drop files here to upload')).toBeInTheDocument();
+    });
+  });
+
+  it('should call onDrop when files are dropped', async () => {
+    const files = [new File(['fileContents'], 'test.txt', { type: 'text/plain' })];
+
+    const { getByTestId } = render(<DragAndDrop onDrop={mockOnFilesDrop} />);
+
+    const dropzone = getByTestId('drag-drop__input');
+
+    const dropEvent = {
+      dataTransfer: {
+        files,
+        items: files.map(file => ({
+          kind: 'file',
+          type: file.type,
+          getAsFile: () => file
+        })),
+        types: ['Files']
+      }
+    };
+
+    await act(async () => fireEvent.drop(dropzone, dropEvent));
+
+    await waitFor(() => {
+      expect(mockOnFilesDrop).toHaveBeenCalledTimes(1);
+    });
+  });
+});

+ 51 - 0
desktop/core/src/desktop/js/reactComponents/DragAndDrop/DragAndDrop.tsx

@@ -0,0 +1,51 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import { useDropzone } from 'react-dropzone';
+import './DragAndDrop.scss';
+import { i18nReact } from '../../utils/i18nReact';
+
+interface DragAndDropProps {
+  onDrop: (files: File[]) => void;
+  children?: JSX.Element;
+}
+
+const DragAndDrop = ({ children, onDrop }: DragAndDropProps): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+
+  const { getRootProps, getInputProps, isDragActive } = useDropzone({
+    onDrop,
+    noClick: !!children
+  });
+
+  return (
+    <div className="drag-drop">
+      <div {...getRootProps()} className="drag-drop__dropzone">
+        <input {...getInputProps()} className="drag-drop__input" data-testid="drag-drop__input" />
+        {isDragActive && <div className="drag-drop__message">{t('Drop files here to upload')}</div>}
+        {!isDragActive && !children && (
+          <div className="drag-drop__message">
+            {t("Drag 'n' drop some files here, or click to select files")}
+          </div>
+        )}
+        {!isDragActive && children}
+      </div>
+    </div>
+  );
+};
+
+export default DragAndDrop;

+ 41 - 0
package-lock.json

@@ -48,6 +48,7 @@
         "qs": "6.9.7",
         "react": "18.2.0",
         "react-dom": "18.2.0",
+        "react-dropzone": "14.3.5",
         "react-i18next": "11.18.6",
         "react-joyride": "2.7.0",
         "react-syntax-highlighter": "15.5.0",
@@ -5348,6 +5349,14 @@
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
       "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
     },
+    "node_modules/attr-accept": {
+      "version": "2.2.5",
+      "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
+      "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/available-typed-arrays": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz",
@@ -8354,6 +8363,22 @@
       "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
       "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
     },
+    "node_modules/file-selector": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
+      "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
+      "dependencies": {
+        "tslib": "^2.7.0"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/file-selector/node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+    },
     "node_modules/filelist": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -15670,6 +15695,22 @@
         "react": "^18.2.0"
       }
     },
+    "node_modules/react-dropzone": {
+      "version": "14.3.5",
+      "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz",
+      "integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==",
+      "dependencies": {
+        "attr-accept": "^2.2.4",
+        "file-selector": "^2.1.0",
+        "prop-types": "^15.8.1"
+      },
+      "engines": {
+        "node": ">= 10.13"
+      },
+      "peerDependencies": {
+        "react": ">= 16.8 || 18.0.0"
+      }
+    },
     "node_modules/react-floater": {
       "version": "0.7.6",
       "resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.6.tgz",

+ 1 - 0
package.json

@@ -69,6 +69,7 @@
     "qs": "6.9.7",
     "react": "18.2.0",
     "react-dom": "18.2.0",
+    "react-dropzone": "14.3.5",
     "react-i18next": "11.18.6",
     "react-joyride": "2.7.0",
     "react-syntax-highlighter": "15.5.0",