feat: improve signature undo process

This commit is contained in:
Guarzo
2025-05-07 13:55:19 -04:00
parent 6378754c57
commit da2605ee03
18 changed files with 741 additions and 322 deletions

View File

@@ -10,7 +10,7 @@ export interface SignatureViewProps {
export const SignatureView = ({ signature, showCharacterPortrait = false }: SignatureViewProps) => {
const isWormhole = signature?.group === SignatureGroup.Wormhole;
const hasCharacterInfo = showCharacterPortrait && signature.character_eve_id;
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : (signature?.group ?? SignatureGroup.CosmicSignature);
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : signature?.group ?? SignatureGroup.CosmicSignature;
const characterName = signature.character_name || 'Unknown character';
return (

View File

@@ -19,7 +19,7 @@ export type HeaderProps = {
lazyDeleteValue: boolean;
onLazyDeleteChange: (checked: boolean) => void;
pendingCount: number;
pendingTimeRemaining?: number; // Time remaining in ms
undoCountdown?: number;
onUndoClick: () => void;
onSettingsClick: () => void;
};
@@ -29,7 +29,7 @@ export const SystemSignaturesHeader = ({
lazyDeleteValue,
onLazyDeleteChange,
pendingCount,
pendingTimeRemaining,
undoCountdown,
onUndoClick,
onSettingsClick,
}: HeaderProps) => {
@@ -43,13 +43,6 @@ export const SystemSignaturesHeader = ({
const containerRef = useRef<HTMLDivElement>(null);
const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH);
// Format time remaining as seconds
const formatTimeRemaining = () => {
if (!pendingTimeRemaining) return '';
const seconds = Math.ceil(pendingTimeRemaining / 1000);
return ` (${seconds}s remaining)`;
};
return (
<div ref={containerRef} className="w-full">
<div className="flex justify-between items-center text-xs w-full h-full">
@@ -78,7 +71,9 @@ export const SystemSignaturesHeader = ({
<WdImgButton
className={PrimeIcons.UNDO}
style={{ color: 'red' }}
tooltip={{ content: `Undo pending changes (${pendingCount})${formatTimeRemaining()}` }}
tooltip={{
content: `Undo pending deletions (${pendingCount})${undoCountdown && undoCountdown > 0 ? `${undoCountdown}s left` : ''}`,
}}
onClick={onUndoClick}
/>
)}

View File

@@ -1,99 +1,152 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useHotkey } from '@/hooks/Mapper/hooks';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import useLocalStorageState from 'use-local-storage-state';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import {
SETTINGS_KEYS,
SETTINGS_VALUES,
SIGNATURE_DELETION_TIMEOUTS,
SIGNATURE_SETTING_STORE_KEY,
SIGNATURE_WINDOW_ID,
SIGNATURES_DELETION_TIMING,
SignatureSettingsType,
SIGNATURES_DELETION_TIMING,
SIGNATURE_DELETION_TIMEOUTS,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { calculateTimeRemaining } from './helpers';
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
export const SystemSignatures = () => {
const [visible, setVisible] = useState(false);
const [sigCount, setSigCount] = useState<number>(0);
const [pendingSigs, setPendingSigs] = useState<SystemSignature[]>([]);
const [pendingTimeRemaining, setPendingTimeRemaining] = useState<number | undefined>();
const undoPendingFnRef = useRef<() => void>(() => {});
/**
* Custom hook for managing pending signature deletions and undo countdown.
*/
function useSignatureUndo(
systemId: string | undefined,
settings: SignatureSettingsType,
outCommand: OutCommandHandler,
) {
const [pendingIds, setPendingIds] = useState<string[]>([]);
const [countdown, setCountdown] = useState(0);
const intervalRef = useRef<number | null>(null);
const {
data: { selectedSystems },
} = useMapRootState();
const [currentSettings, setCurrentSettings] = useLocalStorageState(SIGNATURE_SETTING_STORE_KEY, {
defaultValue: SETTINGS_VALUES,
});
const handleSigCountChange = useCallback((count: number) => {
setSigCount(count);
const addDeleted = useCallback((ids: string[]) => {
setPendingIds(prev => [...prev, ...ids]);
}, []);
const [systemId] = selectedSystems;
const isNotSelectedSystem = selectedSystems.length !== 1;
const handleSettingsChange = useCallback((newSettings: SignatureSettingsType) => {
setCurrentSettings(newSettings);
setVisible(false);
}, []);
const handleLazyDeleteChange = useCallback((value: boolean) => {
setCurrentSettings(prev => ({ ...prev, [SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value }));
}, []);
useHotkey(true, ['z'], event => {
if (pendingSigs.length > 0) {
event.preventDefault();
event.stopPropagation();
undoPendingFnRef.current();
setPendingSigs([]);
setPendingTimeRemaining(undefined);
}
});
const handleUndoClick = useCallback(() => {
undoPendingFnRef.current();
setPendingSigs([]);
setPendingTimeRemaining(undefined);
}, []);
const handleSettingsButtonClick = useCallback(() => {
setVisible(true);
}, []);
const handlePendingChange = useCallback(
(pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>, newUndo: () => void) => {
setPendingSigs(() => {
return Object.values(pending.current).filter(sig => sig.pendingDeletion);
});
undoPendingFnRef.current = newUndo;
},
[],
);
// Calculate the minimum time remaining for any pending signature
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
if (pendingSigs.length === 0) {
setPendingTimeRemaining(undefined);
// clear any existing timer
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (pendingIds.length === 0) {
setCountdown(0);
return;
}
const calculate = () => {
setPendingTimeRemaining(() => calculateTimeRemaining(pendingSigs));
};
// determine timeout from settings
const timingKey = Number(settings[SETTINGS_KEYS.DELETION_TIMING] ?? SIGNATURES_DELETION_TIMING.DEFAULT);
const timeoutMs =
Number(SIGNATURE_DELETION_TIMEOUTS[timingKey as keyof typeof SIGNATURE_DELETION_TIMEOUTS]) || 10000;
setCountdown(Math.ceil(timeoutMs / 1000));
calculate();
const interval = setInterval(calculate, 1000);
return () => clearInterval(interval);
}, [pendingSigs]);
// start new interval
intervalRef.current = window.setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
setPendingIds([]);
return 0;
}
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [pendingIds, settings]);
// undo handler
const handleUndo = useCallback(async () => {
if (!systemId || pendingIds.length === 0) return;
await outCommand({
type: OutCommand.undoDeleteSignatures,
data: { system_id: systemId, eve_ids: pendingIds },
});
setPendingIds([]);
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [systemId, pendingIds, outCommand]);
return {
pendingIds,
countdown,
addDeleted,
handleUndo,
};
}
export const SystemSignatures = () => {
const [visible, setVisible] = useState(false);
const [sigCount, setSigCount] = useState(0);
const {
data: { selectedSystems },
outCommand,
} = useMapRootState();
const [currentSettings, setCurrentSettings] = useLocalStorageState<SignatureSettingsType>(
SIGNATURE_SETTING_STORE_KEY,
{
defaultValue: SETTINGS_VALUES,
},
);
const [systemId] = selectedSystems;
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
const { pendingIds, countdown, addDeleted, handleUndo } = useSignatureUndo(systemId, currentSettings, outCommand);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (pendingIds.length > 0 && countdown > 0) {
event.preventDefault();
event.stopPropagation();
handleUndo();
}
});
const handleCountChange = useCallback((count: number) => {
setSigCount(count);
}, []);
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
setCurrentSettings(newSettings);
setVisible(false);
},
[setCurrentSettings],
);
const handleLazyDeleteToggle = useCallback(
(value: boolean) => {
setCurrentSettings(prev => ({
...prev,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value,
}));
},
[setCurrentSettings],
);
const openSettings = useCallback(() => setVisible(true), []);
return (
<Widget
@@ -101,16 +154,16 @@ export const SystemSignatures = () => {
<SystemSignaturesHeader
sigCount={sigCount}
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
pendingCount={pendingSigs.length}
pendingTimeRemaining={pendingTimeRemaining}
onLazyDeleteChange={handleLazyDeleteChange}
onUndoClick={handleUndoClick}
onSettingsClick={handleSettingsButtonClick}
pendingCount={pendingIds.length}
undoCountdown={countdown}
onLazyDeleteChange={handleLazyDeleteToggle}
onUndoClick={handleUndo}
onSettingsClick={openSettings}
/>
}
windowId={SIGNATURE_WINDOW_ID}
>
{isNotSelectedSystem ? (
{!isSystemSelected ? (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
System is not selected
</div>
@@ -118,22 +171,17 @@ export const SystemSignatures = () => {
<SystemSignaturesContent
systemId={systemId}
settings={currentSettings}
deletionTiming={
SIGNATURE_DELETION_TIMEOUTS[
(currentSettings[SETTINGS_KEYS.DELETION_TIMING] as keyof typeof SIGNATURE_DELETION_TIMEOUTS) ||
SIGNATURES_DELETION_TIMING.DEFAULT
] as number
}
onLazyDeleteChange={handleLazyDeleteChange}
onCountChange={handleSigCountChange}
onPendingChange={handlePendingChange}
onLazyDeleteChange={handleLazyDeleteToggle}
onCountChange={handleCountChange}
onSignatureDeleted={addDeleted}
/>
)}
{visible && (
<SystemSignatureSettingsDialog
settings={currentSettings}
onCancel={() => setVisible(false)}
onSave={handleSettingsChange}
onSave={handleSettingsSave}
/>
)}
</Widget>

View File

@@ -57,12 +57,8 @@ interface SystemSignaturesContentProps {
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void;
onPendingChange?: (
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
undo: () => void,
) => void;
deletionTiming?: number;
filterSignature?: (signature: SystemSignature) => boolean;
onSignatureDeleted?: (deletedIds: string[]) => void;
}
export const SystemSignaturesContent = ({
@@ -73,9 +69,8 @@ export const SystemSignaturesContent = ({
onSelect,
onLazyDeleteChange,
onCountChange,
onPendingChange,
deletionTiming,
filterSignature,
onSignatureDeleted,
}: SystemSignaturesContentProps) => {
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
@@ -100,9 +95,8 @@ export const SystemSignaturesContent = ({
systemId,
settings,
onCountChange,
onPendingChange,
onLazyDeleteChange,
deletionTiming,
onSignatureDeleted,
});
useEffect(() => {
@@ -125,6 +119,10 @@ export const SystemSignaturesContent = ({
event.preventDefault();
event.stopPropagation();
if (onSignatureDeleted && selectedSignatures.length > 0) {
const deletedIds = selectedSignatures.map(s => s.eve_id);
onSignatureDeleted(deletedIds);
}
handleDeleteSelected();
});
@@ -155,7 +153,7 @@ export const SystemSignaturesContent = ({
(e: { value: SystemSignature[] }) => {
selectable ? onSelect?.(e.value[0]) : setSelectedSignatures(e.value as ExtendedSystemSignature[]);
},
[selectable],
[onSelect, selectable, setSelectedSignatures],
);
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(

View File

@@ -1,23 +1,18 @@
import { useCallback, useRef, useEffect } from 'react';
import { useCallback, useRef } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { prepareUpdatePayload, scheduleLazyTimers } from '../helpers';
import { prepareUpdatePayload } from '../helpers';
import { UsePendingDeletionParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
export function usePendingDeletions({
systemId,
setSignatures,
deletionTiming,
onPendingChange,
}: UsePendingDeletionParams) {
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) {
const { outCommand } = useMapRootState();
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
// Use the provided deletion timing or fall back to the default
const finalDuration = deletionTiming !== undefined ? deletionTiming : FINAL_DURATION_MS;
const processRemovedSignatures = useCallback(
async (
removed: ExtendedSystemSignature[],
@@ -25,63 +20,15 @@ export function usePendingDeletions({
updated: ExtendedSystemSignature[],
) => {
if (!removed.length) return;
// If deletion timing is 0, immediately delete without pending state
if (finalDuration === 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
return;
}
const now = Date.now();
const processedRemoved = removed.map(r => ({
...r,
pendingDeletion: true,
pendingUntil: now + finalDuration,
}));
pendingDeletionMapRef.current = {
...pendingDeletionMapRef.current,
...processedRemoved.reduce((acc: any, sig) => {
acc[sig.eve_id] = sig;
return acc;
}, {}),
};
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
setSignatures(prev =>
prev.map(sig => {
if (processedRemoved.find(r => r.eve_id === sig.eve_id)) {
return { ...sig, pendingDeletion: true, pendingUntil: now + finalDuration };
}
return sig;
}),
);
scheduleLazyTimers(
processedRemoved,
pendingDeletionMapRef,
async sig => {
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, [], [], [sig]),
});
delete pendingDeletionMapRef.current[sig.eve_id];
setSignatures(prev => prev.filter(x => x.eve_id !== sig.eve_id));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
},
finalDuration,
);
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
},
[systemId, outCommand, finalDuration],
[systemId, outCommand],
);
const clearPendingDeletions = useCallback(() => {
Object.values(pendingDeletionMapRef.current).forEach(({ finalTimeoutId }) => {
clearTimeout(finalTimeoutId);
});
pendingDeletionMapRef.current = {};
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);

View File

@@ -18,8 +18,8 @@ export const useSystemSignaturesData = ({
onCountChange,
onPendingChange,
onLazyDeleteChange,
deletionTiming,
}: UseSystemSignaturesDataProps) => {
onSignatureDeleted,
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & { onSignatureDeleted?: (deletedIds: string[]) => void }) => {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
@@ -27,7 +27,6 @@ export const useSystemSignaturesData = ({
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
systemId,
setSignatures,
deletionTiming,
onPendingChange,
});
@@ -59,6 +58,10 @@ export const useSystemSignaturesData = ({
if (removed.length > 0) {
await processRemovedSignatures(removed, added, updated);
if (onSignatureDeleted) {
const deletedIds = removed.map(sig => sig.eve_id);
onSignatureDeleted(deletedIds);
}
}
if (updated.length !== 0 || added.length !== 0) {
@@ -78,17 +81,16 @@ export const useSystemSignaturesData = ({
onLazyDeleteChange?.(false);
}
},
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange],
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted],
);
const handleDeleteSelected = useCallback(async () => {
if (!selectedSignatures.length) return;
const selectedIds = selectedSignatures.map(s => s.eve_id);
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
await handleUpdateSignatures(finalList, false, true);
setSelectedSignatures([]);
}, [selectedSignatures, signatures]);
}, [handleUpdateSignatures, selectedSignatures, signatures]);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
@@ -119,7 +121,7 @@ export const useSystemSignaturesData = ({
}, [signatures]);
return {
signatures,
signatures: signatures.filter(sig => !sig.deleted),
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,