-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cd85557
commit b496a0a
Showing
18 changed files
with
737 additions
and
678 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { useDebounce } from './useDebounce'; |
64 changes: 64 additions & 0 deletions
64
gui/frontend/src/components/hooks/useDebounce/useDebounce.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { renderHook, act } from '@testing-library/react'; | ||
import { describe, it, expect, vi } from 'vitest'; | ||
|
||
import { useDebounce } from './useDebounce'; | ||
|
||
describe('useDebounce', () => { | ||
vi.useFakeTimers(); | ||
|
||
it('should return the initial value immediately', () => { | ||
const { result } = renderHook(() => useDebounce('test', 500)); | ||
expect(result.current).toBe('test'); | ||
}); | ||
|
||
it('should debounce the value', () => { | ||
vi.useFakeTimers(); // Setup functions required to use `advanceTimersByTime`. | ||
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { | ||
initialProps: { value: 'initial', delay: 500 }, | ||
}); | ||
|
||
expect(result.current).toBe('initial'); | ||
|
||
rerender({ value: 'updated', delay: 500 }); | ||
|
||
// The value should not change immediately | ||
expect(result.current).toBe('initial'); | ||
|
||
act(() => { | ||
vi.advanceTimersByTime(500); | ||
}); | ||
|
||
expect(result.current).toBe('updated'); | ||
}); | ||
|
||
it('should reset the timer when the value changes quickly', () => { | ||
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { | ||
initialProps: { value: 'initial', delay: 500 }, | ||
}); | ||
|
||
expect(result.current).toBe('initial'); | ||
|
||
// Update the value multiple times | ||
rerender({ value: 'first update', delay: 500 }); | ||
act(() => { | ||
vi.advanceTimersByTime(200); | ||
}); | ||
rerender({ value: 'second update', delay: 500 }); | ||
act(() => { | ||
vi.advanceTimersByTime(200); | ||
}); | ||
|
||
// The value should still be the initial value | ||
expect(result.current).toBe('initial'); | ||
|
||
// Fast-forward time by 500ms | ||
act(() => { | ||
vi.advanceTimersByTime(500); | ||
}); | ||
|
||
// Now the value should update to the latest one | ||
expect(result.current).toBe('second update'); | ||
}); | ||
|
||
vi.useRealTimers(); | ||
}); |
33 changes: 33 additions & 0 deletions
33
gui/frontend/src/components/hooks/useDebounce/useDebounce.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { useState, useEffect } from 'react'; | ||
|
||
/** | ||
* Custom React hook to debounce a value. The value updates after the specified delay time. | ||
* | ||
* @template T The type of the value to debounce. | ||
* @param value The value to debounce. | ||
* @param delay The debounce delay in milliseconds. | ||
* @returns The debounced value. | ||
* | ||
* @example | ||
* const [search, setSearch] = useState(''); | ||
* const debouncedSearch = useDebounce(search, 500); | ||
* | ||
* useEffect(() => { | ||
* // Perform a search API call with debouncedSearch. | ||
* }, [debouncedSearch]); | ||
*/ | ||
export function useDebounce<T>(value: T, delay: number): T { | ||
const [debouncedValue, setDebouncedValue] = useState(value); | ||
|
||
useEffect(() => { | ||
const handler = setTimeout(() => { | ||
setDebouncedValue(value); | ||
}, delay); | ||
|
||
return () => { | ||
clearTimeout(handler); | ||
}; | ||
}, [value, delay]); | ||
|
||
return debouncedValue; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 17 additions & 93 deletions
110
gui/frontend/src/components/organisms/ConvertForm/PathSelector.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,113 +1,37 @@ | ||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'; | ||
import ErrorIcon from '@mui/icons-material/Error'; | ||
import { Box, Chip, Grid2 as Grid } from '@mui/material'; | ||
import CircularProgress from '@mui/material/CircularProgress'; | ||
import { open } from '@tauri-apps/plugin-dialog'; | ||
import { Box, Chip } from '@mui/material'; | ||
|
||
import { Button } from '@/components/molecules/Button'; | ||
import { hashDjb2 } from '@/lib/hash-djb2'; | ||
import { loadDirNode } from '@/services/api/serde_hkx'; | ||
|
||
import { useConvertContext } from './ConvertProvider'; | ||
import { OutFormatList } from './OutFormatList'; | ||
import { PathTreeSelector } from './PathTreeSelector'; | ||
import { SelectionTypeRadios } from './SelectionTypeRadios'; | ||
import { renderStatusIcon } from './renderStatusIcon'; | ||
|
||
import type { ComponentPropsWithRef } from 'react'; | ||
|
||
export const PathSelector = () => { | ||
const { | ||
selectionType, | ||
selectedFiles, | ||
setSelectedFiles, | ||
selectedDirs, | ||
setSelectedDirs, | ||
selectedTree, | ||
setSelectedTree, | ||
convertStatuses, | ||
setConvertStatuses, | ||
} = useConvertContext(); | ||
const { selectionType, selectedFiles, setSelectedFiles, selectedDirs, setSelectedDirs, convertStatuses } = | ||
useConvertContext(); | ||
const isDirMode = selectionType === 'dir'; | ||
const selectedPaths = isDirMode ? selectedDirs : selectedFiles; | ||
const setSelectedPaths = isDirMode ? setSelectedDirs : setSelectedFiles; | ||
|
||
const handleDelete: ComponentPropsWithRef<typeof Chip>['onDelete'] = (fileToDelete: string) => | ||
setSelectedPaths(selectedPaths.filter((file) => file !== fileToDelete)); | ||
|
||
const handlePathSelect = async () => { | ||
const newSelectedPaths = await open({ | ||
title: isDirMode ? 'Select directory' : 'Select files', | ||
filters: [{ name: '', extensions: ['hkx', 'xml', 'json', 'yaml'] }], | ||
multiple: true, | ||
directory: ['dir', 'tree'].includes(selectionType), | ||
defaultPath: selectedPaths.at(0), | ||
}); | ||
|
||
if (selectionType === 'tree') { | ||
const roots = (() => { | ||
if (Array.isArray(newSelectedPaths)) { | ||
return newSelectedPaths; | ||
} | ||
if (newSelectedPaths !== null) { | ||
return [newSelectedPaths]; | ||
} | ||
})(); | ||
|
||
if (roots) { | ||
setSelectedTree({ ...selectedTree, roots, tree: await loadDirNode(roots) }); | ||
} | ||
return; | ||
} | ||
|
||
if (Array.isArray(newSelectedPaths)) { | ||
setSelectedPaths(newSelectedPaths); | ||
setConvertStatuses(new Map()); // Clear the conversion status when a new selection is made. | ||
} else if (newSelectedPaths !== null) { | ||
setSelectedPaths([newSelectedPaths]); | ||
setConvertStatuses(new Map()); // Clear the conversion status when a new selection is made. | ||
} | ||
}; | ||
|
||
return ( | ||
<Box> | ||
<Grid container={true} spacing={2} sx={{ justifyContent: 'space-between' }}> | ||
<Grid> | ||
<SelectionTypeRadios /> | ||
<Button onClick={handlePathSelect} /> | ||
</Grid> | ||
|
||
<Grid> | ||
<OutFormatList /> | ||
</Grid> | ||
</Grid> | ||
|
||
<Box mt={2}> | ||
{selectionType === 'tree' ? ( | ||
<PathTreeSelector /> | ||
) : ( | ||
selectedPaths.map((path) => { | ||
const pathId = hashDjb2(path); | ||
const statusId = convertStatuses.get(pathId) ?? 0; | ||
|
||
return ( | ||
<Chip icon={renderStatusIcon(statusId)} key={pathId} label={path} onDelete={() => handleDelete(path)} /> | ||
); | ||
}) | ||
)} | ||
</Box> | ||
<Box mt={2}> | ||
{selectionType === 'tree' ? ( | ||
<PathTreeSelector /> | ||
) : ( | ||
selectedPaths.map((path) => { | ||
const pathId = hashDjb2(path); | ||
const statusId = convertStatuses.get(pathId) ?? 0; | ||
|
||
return ( | ||
<Chip icon={renderStatusIcon(statusId)} key={pathId} label={path} onDelete={() => handleDelete(path)} /> | ||
); | ||
}) | ||
)} | ||
</Box> | ||
); | ||
}; | ||
|
||
export const renderStatusIcon = (status: number) => { | ||
switch (status) { | ||
case 1: | ||
return <CircularProgress size={20} />; | ||
case 2: | ||
return <CheckCircleIcon color='success' />; | ||
case 3: | ||
return <ErrorIcon color='error' />; | ||
default: | ||
return undefined; | ||
} | ||
}; |
Oops, something went wrong.