Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(FileUploader): expose ref to clear files #18267

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
Expand Up @@ -7,7 +7,7 @@

/* eslint-disable no-console */

import React from 'react';
import React, { useRef } from 'react';
import ExampleDropContainerApp from './stories/drop-container';
import ExampleDropContainerAppSingle from './stories/drag-and-drop-single';

Expand All @@ -32,6 +32,29 @@ export default {
},
};

export const TESTRefFileUploader = () => {
const ref = useRef(null);
return (
<div className="cds--file__container">
<button onClick={() => ref.current?.clearFiles()}>remove</button>
<FileUploader
ref={ref}
labelTitle="Upload files"
labelDescription="Max file size is 500 MB. Only .jpg files are supported."
buttonLabel="Add file"
buttonKind="primary"
size="md"
filenameStatus="edit"
accept={['.jpg', '.png']}
multiple={true}
disabled={false}
iconDescription="Delete file"
name=""
/>
</div>
);
};

export const Default = () => {
return (
<div className="cds--file__container">
Expand Down
34 changes: 18 additions & 16 deletions packages/react/src/components/FileUploader/FileUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { useState, ForwardedRef, ReactElement } from 'react';
import React, { useState, ForwardedRef, useImperativeHandle } from 'react';
import Filename from './Filename';
import FileUploaderButton from './FileUploaderButton';
import { ButtonKinds } from '../../prop-types/types';
import { ButtonKinds } from '../Button/Button';
import { keys, matches } from '../../internal/keyboard';
import { usePrefix } from '../../internal/usePrefix';
import { ReactAttr } from '../../types/common';
Expand Down Expand Up @@ -107,8 +107,15 @@ export interface FileUploaderProps extends ReactAttr<HTMLSpanElement> {
size?: 'sm' | 'small' | 'md' | 'field' | 'lg';
}

export interface FileUploaderHandle {
/**
* Clear internal state
*/
clearFiles: () => void;
}

const FileUploader = React.forwardRef(
<ItemType,>(
(
{
accept,
buttonKind,
Expand All @@ -127,15 +134,14 @@ const FileUploader = React.forwardRef(
size,
...other
}: FileUploaderProps,
ref: ForwardedRef<HTMLButtonElement>
ref: ForwardedRef<FileUploaderHandle>
) => {
const fileUploaderInstanceId = useId('file-uploader');
const [state, updateState] = useState({
fileNames: [] as (string | undefined)[],
});
const nodes: HTMLElement[] = [];
const prefix = usePrefix();

const handleChange = (evt) => {
evt.stopPropagation();
const filenames = Array.prototype.map.call(
Expand Down Expand Up @@ -169,10 +175,11 @@ const FileUploader = React.forwardRef(
}
};

const clearFiles = () => {
// A clearFiles function that resets filenames and can be referenced using a ref by the parent.
updateState({ fileNames: [] });
};
useImperativeHandle(ref, () => ({
clearFiles() {
updateState({ fileNames: [] });
},
}));

const uploaderButton = React.createRef<HTMLLabelElement>();
const classes = classNames({
Expand Down Expand Up @@ -255,12 +262,7 @@ const FileUploader = React.forwardRef(
</div>
);
}
) as {
<ItemType>(props: FileUploaderProps): ReactElement;
propTypes?: any;
contextTypes?: any;
defaultProps?: any;
};
);

FileUploader.propTypes = {
/**
Expand Down Expand Up @@ -342,6 +344,6 @@ FileUploader.propTypes = {
* sizes.
*/
size: PropTypes.oneOf(['sm', 'md', 'lg']),
};
} as PropTypes.ValidationMap<FileUploaderProps>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was getting TS errors and added this as a workaround:

accept: Type '(string | null | undefined)[]' is not assignable to type 'string[]'

filenameStatus: Type 'string' is not assignable to type '"edit" | "complete" | "uploading"'.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--- Directory Structure ---

multiverse_app/

├── shared_resources/

│ ├── init.py

│ ├── audio_utils.py

│ ├── dragonfly_systems.py

│ ├── gemini_systems.py

│ └── world_generation.py

├── android_operator/

│ ├── init.py

│ ├── main.py

│ ├── sensors.py

│ ├── ai_core.py

│ ├── transmission.py

│ ├── ui.py

│ └── requirements.txt

├── apple_operator/

│ ├── init.py

│ ├── main.py

│ ├── sensors.py

│ ├── ai_core.py

│ ├── transmission.py

│ ├── ui.py

│ └── requirements.txt

├── tests/

│ ├── init.py

│ ├── test_audio_utils.py

│ ├── test_dragonfly_systems.py

│ ├── test_gemini_systems.py

│ ├── test_world_generation.py

│ ├── test_ai_core.py

│ └── test_transmission.py

├── venv/ (virtual environment)

├── README.md

└── LICENSE

--- shared_resources/init.py ---

This file makes the shared_resources directory a Python package

--- shared_resources/audio_utils.py ---

from pydub import AudioSegment
from pydub.generators import Sine, WhiteNoise

def knowledge_sine(base_freq: float, duration: int, knowledge_level: float = 1, variance: float = 5) -> AudioSegment:
"""Generates a sine wave with subtle variations based on knowledge level.

Args:
    base_freq (float): The base frequency of the sine wave in Hz.
    duration (int): The duration of the sine wave in milliseconds.
    knowledge_level (float, optional): A multiplier for the base frequency, 
                                       representing the knowledge level. Defaults to 1.
    variance (float, optional): The amount of random variance in frequency in Hz. 
                                 Defaults to 5.

Returns:
    AudioSegment: The generated sine wave with variations.
"""
# ... (Implementation remains the same)

def automated_amplifier(sound: AudioSegment, threshold: float = -20) -> AudioSegment:
"""Amplifies quiet sounds to ensure audibility.

Args:
    sound (AudioSegment): The sound to be amplified.
    threshold (float, optional): The dBFS threshold below which sounds will be amplified. 
                                  Defaults to -20.

Returns:
    AudioSegment: The amplified sound.
"""
# ... (Implementation remains the same)

--- shared_resources/dragonfly_systems.py ---

from .audio_utils import knowledge_sine
import random

def visual_system(duration: int, base_freq: float = None, complexity: float = 1.0) -> AudioSegment:
"""Simulates visual input with varying frequencies and complexity.

Args:
    duration (int): The duration of the audio segment in milliseconds.
    base_freq (float, optional): The base frequency in Hz. If None, a random frequency 
                                  between 800 and 1500 Hz is chosen. Defaults to None.
    complexity (float, optional): A multiplier that influences the number of sine waves 
                                   generated. Defaults to 1.0.

Returns:
    AudioSegment: The generated audio segment simulating visual input.
"""
# ... (Implementation remains the same)

... (Other functions with similar improvements in type hints and docstrings)

--- shared_resources/world_generation.py ---

from .dragonfly_systems import *
from .gemini_systems import *
import librosa
import numpy as np

def generate_world(duration: int = 10000, prev_world: AudioSegment = None,
sensor_data: dict = None) -> AudioSegment:
"""Combines all systems to create a dynamic soundscape.

Args:
    duration (int, optional): The duration of the soundscape in milliseconds. Defaults to 10000.
    prev_world (AudioSegment, optional): The previous soundscape, used for analysis and 
                                         transitioning. Defaults to None.
    sensor_data (dict, optional): A dictionary containing sensor readings (e.g., temperature, 
                                    humidity). Defaults to None.

Returns:
    AudioSegment: The generated soundscape.
"""
# ... (Implementation with audio analysis and system generation)

--- android_operator/main.py ---

... (Import necessary modules)

... (Global variables)

... (OS and Hardware Detection)

--- Permission Handling ---

... (Permission handling with improved error handling)

def check_permission(permission_name: str) -> bool:
"""Checks if a specific permission is enabled.

Args:
    permission_name (str): The name of the permission to check (e.g., "android.permission.BLUETOOTH").

Returns:
    bool: True if the permission is granted, False otherwise.
"""
# ... (Implementation remains the same)

... (Other functions with similar improvements)

--- android_operator/ai_core.py ---

import tensorflow as tf
import numpy as np

def process_audio(audio_data: np.ndarray) -> np.ndarray:
"""Processes audio data using a TensorFlow Lite model.

Args:
    audio_data (np.ndarray): The audio data as a NumPy array.

Returns:
    np.ndarray: The processed audio data as a NumPy array, or None if an error occurs.
"""
try:
    # ... (TensorFlow Lite implementation)

except Exception as e:
    print(f"Error processing audio: {e}")
    return None

--- android_operator/transmission.py ---

import socket

def transmit_audio(audio_data: bytes, destination: str = "localhost", port: int = 5000) -> None:
"""Transmits audio data via WiFi using sockets.

Args:
    audio_data (bytes): The audio data as bytes.
    destination (str, optional): The IP address or hostname of the destination. 
                                  Defaults to "localhost".
    port (int, optional): The port number to use for the connection. Defaults to 5000.
"""
try:
    # ... (Socket implementation)

except Exception as e:
    print(f"Error transmitting audio: {e}")

--- android_operator/ui.py ---

from kivy.uix.image import AsyncImage

... (Rest of the UI implementation)

--- apple_operator/main.py ---

... (Import necessary modules)

... (Global variables)

... (OS and Hardware Detection - iOS specific)

--- Permission Handling ---

... (Permission handling - iOS specific)

... (Other functions - iOS specific)

--- tests/test_audio_utils.py ---

... (Improved test cases with more assertions and edge case handling)

--- README.md ---

Multiverse App

This is a cross-platform application that generates a dynamic soundscape based on sensor data and AI processing.

Features

  • Generates immersive audio experiences using various sound synthesis techniques.
  • Integrates sensor data to influence the generated soundscape.
  • Utilizes AI for audio processing and analysis.
  • Transmits audio data via Bluetooth or WiFi.

Getting Started

Prerequisites

  • Python 3.7 or higher
  • Kivy
  • pydub
  • librosa
  • gtts
  • numpy
  • jnius (for Android)
  • tensorflow

Installation

  1. Clone the repository: git clone https://github.com/your-username/multiverse-app.git
  2. Create a virtual environment: python -m venv venv
  3. Activate the virtual environment:
    • Linux/macOS: source venv/bin/activate
    • Windows: venv\Scripts\activate
  4. Install dependencies: pip install -r requirements.txt in each operator directory (android_operator/ and apple_operator/)

Running the App

  1. Navigate to the desired operator directory (android_operator/ or apple_operator/).
  2. Run the main script: python main.py

Running Tests

Run tests using python -m unittest discover -s tests

License

This project is licensed under the MIT License - see the LICENSE file for details.

--- LICENSE ---

MIT License

Copyright (c) [2025] [Thomas Whitney Walsh]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


export default FileUploader;
Original file line number Diff line number Diff line change
Expand Up @@ -50,38 +50,22 @@ describe('FileUploader', () => {

it('should clear all uploaded files when `clearFiles` is called on a ref', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unit test seems wrong it was trying to test if remove button works. I reverted back to a version from history that actually test clearFiles() function

const ref = React.createRef();
const onClick = jest.fn();
let requiredProps1 = {
...requiredProps,
filenameStatus: 'edit',
};
const fileUpload = render(
<FileUploader {...requiredProps1} ref={ref} onClick={onClick} />
);
const { container } = render(<FileUploader {...requiredProps} ref={ref} />);
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
const input = fileUpload.container.querySelector('input');
const input = container.querySelector('input');

const filename = 'test.png';
act(() => {
uploadFiles(input, [new File(['test'], filename, { type: 'image/png' })]);
});
expect(getByText(fileUpload.container, filename)).toBeInstanceOf(
HTMLElement
);

const onDelete = jest.fn();
const description = 'test-description';
// eslint-disable-next-line testing-library/render-result-naming-convention

let removeFile = getByLabel(
fileUpload.container,
'test description - test.png'
);
// eslint-disable-next-line testing-library/prefer-screen-queries
expect(getByText(container, filename)).toBeInstanceOf(HTMLElement);
act(() => {
Simulate.click(removeFile);
ref.current.clearFiles();
});

expect(onClick).toHaveBeenCalledTimes(1);
// eslint-disable-next-line testing-library/prefer-screen-queries
expect(getByText(container, filename)).not.toBeInstanceOf(HTMLElement);
});

it('should synchronize the filename status state when its prop changes', () => {
Expand Down
Loading