mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-01 12:18:41 -05:00
- Fix the clipboard functionality to work on non local hosts and https - Improvements in sockets on front-end and backend. Storing session in local browser storage for reconnect. Logic to prevent socket echos coausing rerender and performance issues. - Fixes and udpates to re-ordering logic in adding a new task, reordering items on the task table. - Allowing assignee to not be hardcoded enum. - Fix to Document Version Control (Improvements still needed in the Milkdown editor conversion to store in the docs. - Adding types to remove [any] typescript issues.
172 lines
4.4 KiB
TypeScript
172 lines
4.4 KiB
TypeScript
import React, { memo, useState, useEffect, useCallback, useRef } from 'react';
|
|
|
|
interface DebouncedInputProps {
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
className?: string;
|
|
type?: 'text' | 'textarea';
|
|
rows?: number;
|
|
}
|
|
|
|
// Memoized input component that manages its own state
|
|
export const DebouncedInput = memo(({
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
className,
|
|
type = 'text',
|
|
rows = 5
|
|
}: DebouncedInputProps) => {
|
|
const [localValue, setLocalValue] = useState(value);
|
|
const timeoutRef = useRef<NodeJS.Timeout>();
|
|
const isFirstRender = useRef(true);
|
|
|
|
// Sync with external value only on mount or when externally changed
|
|
useEffect(() => {
|
|
if (isFirstRender.current) {
|
|
isFirstRender.current = false;
|
|
return;
|
|
}
|
|
// Only update if the external value is different from local
|
|
if (value !== localValue) {
|
|
setLocalValue(value);
|
|
}
|
|
}, [value]);
|
|
|
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
const newValue = e.target.value;
|
|
setLocalValue(newValue);
|
|
|
|
// Clear existing timeout
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
|
|
// Set new timeout for debounced update
|
|
timeoutRef.current = setTimeout(() => {
|
|
onChange(newValue);
|
|
}, 300);
|
|
}, [onChange]);
|
|
|
|
// Cleanup timeout on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
if (type === 'textarea') {
|
|
return (
|
|
<textarea
|
|
value={localValue}
|
|
onChange={handleChange}
|
|
placeholder={placeholder}
|
|
className={className}
|
|
rows={rows}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<input
|
|
type="text"
|
|
value={localValue}
|
|
onChange={handleChange}
|
|
placeholder={placeholder}
|
|
className={className}
|
|
/>
|
|
);
|
|
}, (prevProps, nextProps) => {
|
|
// Custom comparison - only re-render if external value or onChange changes
|
|
return prevProps.value === nextProps.value &&
|
|
prevProps.onChange === nextProps.onChange &&
|
|
prevProps.placeholder === nextProps.placeholder &&
|
|
prevProps.className === nextProps.className;
|
|
});
|
|
|
|
DebouncedInput.displayName = 'DebouncedInput';
|
|
|
|
interface FeatureInputProps {
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
projectFeatures: import('../types/jsonb').ProjectFeature[];
|
|
isLoadingFeatures: boolean;
|
|
placeholder?: string;
|
|
className?: string;
|
|
}
|
|
|
|
// Memoized feature input with datalist
|
|
export const FeatureInput = memo(({
|
|
value,
|
|
onChange,
|
|
projectFeatures,
|
|
isLoadingFeatures,
|
|
placeholder,
|
|
className
|
|
}: FeatureInputProps) => {
|
|
const [localValue, setLocalValue] = useState(value);
|
|
const timeoutRef = useRef<NodeJS.Timeout>();
|
|
|
|
// Sync with external value
|
|
useEffect(() => {
|
|
if (value !== localValue) {
|
|
setLocalValue(value);
|
|
}
|
|
}, [value]);
|
|
|
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newValue = e.target.value;
|
|
setLocalValue(newValue);
|
|
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
|
|
timeoutRef.current = setTimeout(() => {
|
|
onChange(newValue);
|
|
}, 300);
|
|
}, [onChange]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
value={localValue}
|
|
onChange={handleChange}
|
|
placeholder={placeholder}
|
|
className={className}
|
|
list="features-list"
|
|
/>
|
|
<datalist id="features-list">
|
|
{projectFeatures.map((feature) => (
|
|
<option key={feature.id} value={feature.label}>
|
|
{feature.label} ({feature.type})
|
|
</option>
|
|
))}
|
|
</datalist>
|
|
{isLoadingFeatures && (
|
|
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
|
<div className="w-4 h-4 animate-spin rounded-full border-2 border-cyan-400 border-t-transparent" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}, (prevProps, nextProps) => {
|
|
return prevProps.value === nextProps.value &&
|
|
prevProps.onChange === nextProps.onChange &&
|
|
prevProps.isLoadingFeatures === nextProps.isLoadingFeatures &&
|
|
prevProps.projectFeatures === nextProps.projectFeatures;
|
|
});
|
|
|
|
FeatureInput.displayName = 'FeatureInput'; |