diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.module.scss b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.module.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx index c293c884..fa91620f 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx @@ -1,3 +1,4 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Widget } from '@/hooks/Mapper/components/mapInterface/components'; import { InfoDrawer, @@ -19,28 +20,29 @@ import { STRUCTURE, SystemSignatureSettingsDialog, } from './SystemSignatureSettingsDialog'; -import { SignatureGroup } from '@/hooks/Mapper/types'; - -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - +import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types'; import { PrimeIcons } from 'primereact/api'; - import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { CheckboxChangeEvent } from 'primereact/checkbox'; -import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts'; +import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth'; import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper'; +import { useHotkey } from '@/hooks/Mapper/hooks'; +import { COMPACT_MAX_WIDTH } from './constants'; -const SIGNATURE_SETTINGS_KEY = 'wanderer_system_signature_settings_v5_2'; +export const SIGNATURE_SETTINGS_KEY = 'wanderer_system_signature_settings_v5_2'; export const SHOW_DESCRIPTION_COLUMN_SETTING = 'show_description_column_setting'; export const SHOW_UPDATED_COLUMN_SETTING = 'SHOW_UPDATED_COLUMN_SETTING'; export const LAZY_DELETE_SIGNATURES_SETTING = 'LAZY_DELETE_SIGNATURES_SETTING'; export const KEEP_LAZY_DELETE_SETTING = 'KEEP_LAZY_DELETE_ENABLED_SETTING'; +export const HIDE_LINKED_SIGNATURES_SETTING = 'HIDE_LINKED_SIGNATURES_SETTING'; -const settings: Setting[] = [ +const SETTINGS: Setting[] = [ { key: SHOW_UPDATED_COLUMN_SETTING, name: 'Show Updated Column', value: false, isFilter: false }, { key: SHOW_DESCRIPTION_COLUMN_SETTING, name: 'Show Description Column', value: false, isFilter: false }, { key: LAZY_DELETE_SIGNATURES_SETTING, name: 'Lazy Delete Signatures', value: false, isFilter: false }, { key: KEEP_LAZY_DELETE_SETTING, name: 'Keep "Lazy Delete" Enabled', value: false, isFilter: false }, + { key: HIDE_LINKED_SIGNATURES_SETTING, name: 'Hide Linked Signatures', value: false, isFilter: false }, + { key: COSMIC_ANOMALY, name: 'Show Anomalies', value: true, isFilter: true }, { key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true, isFilter: true }, { key: DEPLOYABLE, name: 'Show Deployables', value: true, isFilter: true }, @@ -56,121 +58,160 @@ const settings: Setting[] = [ { key: SignatureGroup.CombatSite, name: 'Show Combat Sites', value: true, isFilter: true }, ]; -const defaultSettings = () => { - return [...settings]; -}; +const getDefaultSettings = (): Setting[] => [...SETTINGS]; -export const SystemSignatures = () => { +export const SystemSignatures: React.FC = () => { const { data: { selectedSystems }, } = useMapRootState(); const [visible, setVisible] = useState(false); - const [settings, setSettings] = useState(defaultSettings); + + const [currentSettings, setCurrentSettings] = useState(() => { + const stored = localStorage.getItem(SIGNATURE_SETTINGS_KEY); + if (stored) { + try { + return JSON.parse(stored) as Setting[]; + } catch (error) { + console.error('Error parsing stored settings', error); + } + } + return getDefaultSettings(); + }); + + useEffect(() => { + localStorage.setItem(SIGNATURE_SETTINGS_KEY, JSON.stringify(currentSettings)); + }, [currentSettings]); + + const [sigCount, setSigCount] = useState(0); + const [pendingSigs, setPendingSigs] = useState([]); + const [undoPending, setUndoPending] = useState<() => void>(() => () => {}); + + const handleSigCountChange = useCallback((count: number) => { + setSigCount(count); + }, []); const [systemId] = selectedSystems; - const isNotSelectedSystem = selectedSystems.length !== 1; - const lazyDeleteValue = useMemo(() => { - return settings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)!.value; - }, [settings]); + const lazyDeleteValue = useMemo( + () => currentSettings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)?.value || false, + [currentSettings], + ); - const handleSettingsChange = useCallback((settings: Setting[]) => { - setSettings(settings); - localStorage.setItem(SIGNATURE_SETTINGS_KEY, JSON.stringify(settings)); + const handleSettingsChange = useCallback((newSettings: Setting[]) => { + setCurrentSettings(newSettings); setVisible(false); }, []); const handleLazyDeleteChange = useCallback((value: boolean) => { - setSettings(settings => { - const lazyDelete = settings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)!; - lazyDelete.value = value; - localStorage.setItem(SIGNATURE_SETTINGS_KEY, JSON.stringify(settings)); - return [...settings]; - }); + setCurrentSettings(prevSettings => + prevSettings.map(setting => (setting.key === LAZY_DELETE_SIGNATURES_SETTING ? { ...setting, value } : setting)), + ); }, []); - useEffect(() => { - const restoredSettings = localStorage.getItem(SIGNATURE_SETTINGS_KEY); + const containerRef = useRef(null); + const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH); - if (restoredSettings) { - setSettings(JSON.parse(restoredSettings)); + useHotkey(true, ['z'], (event: KeyboardEvent) => { + if (pendingSigs.length > 0) { + event.preventDefault(); + event.stopPropagation(); + undoPending(); + setPendingSigs([]); } + }); + + const handleUndoClick = useCallback(() => { + undoPending(); + setPendingSigs([]); + }, [undoPending]); + + const handleSettingsButtonClick = useCallback(() => { + setVisible(true); }, []); - const ref = useRef(null); - const compact = useMaxWidth(ref, 260); + const renderLabel = () => ( +
+
+ {!isCompact && ( +
+ {sigCount ? `[${sigCount}] ` : ''}Signatures {isNotSelectedSystem ? '' : 'in'} +
+ )} + {!isNotSelectedSystem && } +
+ + + handleLazyDeleteChange(!!event.checked)} + /> + + {pendingSigs.length > 0 && ( + + )} + + How to add/update signature?}> + In game you need to select one or more signatures
in the list in{' '} + Probe scanner.
Use next hotkeys: +
+ Shift + LMB or Ctrl + LMB +
or Ctrl + A for select all +
and then use Ctrl + C, after you need to go
+ here, select Solar system and paste it with Ctrl + V +
+ How to select?}> + For selecting any signature, click on it
with hotkeys{' '} + Shift + LMB or Ctrl + LMB +
+ How to delete?}> + To delete any signature, first select it
and then press Del +
+
+ ) as React.ReactNode, + }} + /> + + + + ); return ( - -
- {!compact && ( -
- Signatures {isNotSelectedSystem ? '' : 'in'} -
- )} - {!isNotSelectedSystem && } -
- - - - handleLazyDeleteChange(!!event.checked)} - /> - - - - How to add/update signature?}> - In game you need select one or more signatures
in list in{' '} - Probe scanner.
Use next hotkeys: -
- Shift + LMB or Ctrl + LMB -
or Ctrl + A for select all -
- and then use Ctrl + C, after you need to go
- here select Solar system and paste it with Ctrl + V -
- How to select?}> - For select any signature need click on that,
with hotkeys{' '} - Shift + LMB or Ctrl + LMB -
- How to delete?}> - For delete any signature first of all you need select before -
and then use Del -
- - ) as React.ReactNode, - }} - /> - setVisible(true)} /> -
- - } - > + {isNotSelectedSystem ? (
System is not selected
) : ( - + { + setPendingSigs(pending); + setUndoPending(() => undo); + }} + /> )} {visible && ( setVisible(false)} onSave={handleSettingsChange} /> @@ -178,3 +219,5 @@ export const SystemSignatures = () => {
); }; + +export default SystemSignatures; diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.module.scss b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.module.scss deleted file mode 100644 index aefc722d..00000000 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.TableRowCompact { - height: 8px; - max-height: 8px; - font-size: 12px !important; - line-height: 8px; -} - -.Table { - -} diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.tsx b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.tsx index 07ef2547..abcf2f94 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.tsx @@ -1,29 +1,26 @@ -import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; -import { parseSignatures } from '@/hooks/Mapper/helpers'; -import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts'; -import { WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit'; -import { - getGroupIdByRawGroup, - GROUPS_LIST, -} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; - +import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; import { DataTable, DataTableRowClickEvent, DataTableRowMouseEvent, SortOrder } from 'primereact/datatable'; import { Column } from 'primereact/column'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import useRefState from 'react-usestateref'; -import { Setting } from '../SystemSignatureSettingsDialog'; -import { useHotkey } from '@/hooks/Mapper/hooks'; -import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts'; -import { useClipboard } from '@/hooks/Mapper/hooks/useClipboard'; +import { PrimeIcons } from 'primereact/api'; +import useLocalStorageState from 'use-local-storage-state'; -import classes from './SystemSignaturesContent.module.scss'; -import clsx from 'clsx'; -import { SystemSignature } from '@/hooks/Mapper/types'; +import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types'; +import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings'; +import { WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit'; import { SignatureView } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView'; import { - getActualSigs, - getRowColorByTimeLeft, -} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers'; + COMPACT_MAX_WIDTH, + GROUPS_LIST, + MEDIUM_MAX_WIDTH, + OTHER_COLUMNS_WIDTH, +} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants'; +import { + KEEP_LAZY_DELETE_SETTING, + LAZY_DELETE_SIGNATURES_SETTING, + SHOW_DESCRIPTION_COLUMN_SETTING, + SHOW_UPDATED_COLUMN_SETTING, +} from '../SystemSignatures'; +import { COSMIC_SIGNATURE } from '../SystemSignatureSettingsDialog'; import { renderAddedTimeLeft, renderDescription, @@ -31,415 +28,288 @@ import { renderInfoColumn, renderUpdatedTimeLeft, } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders'; -import useLocalStorageState from 'use-local-storage-state'; -import { PrimeIcons } from 'primereact/api'; -import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings'; -import { useMapEventListener } from '@/hooks/Mapper/events'; -import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper'; -import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog'; -import { - SHOW_DESCRIPTION_COLUMN_SETTING, - SHOW_UPDATED_COLUMN_SETTING, - LAZY_DELETE_SIGNATURES_SETTING, - KEEP_LAZY_DELETE_SETTING, -} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures'; -type SystemSignaturesSortSettings = { - sortField: string; - sortOrder: SortOrder; -}; - -const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = { - sortField: 'inserted_at', - sortOrder: -1, -}; +import { ExtendedSystemSignature } from '../helpers/contentHelpers'; +import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData'; +import { getSignatureRowClass } from '../helpers/rowStyles'; +import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth'; +import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks'; interface SystemSignaturesContentProps { systemId: string; - settings: Setting[]; + settings: { key: string; value: boolean }[]; hideLinkedSignatures?: boolean; selectable?: boolean; onSelect?: (signature: SystemSignature) => void; onLazyDeleteChange?: (value: boolean) => void; + onCountChange: (count: number) => void; + onPendingChange?: (pending: ExtendedSystemSignature[], undo: () => void) => void; } -export const SystemSignaturesContent = ({ + +const headerInlineStyle = { padding: '2px', fontSize: '12px', lineHeight: '1.333' }; + +export function SystemSignaturesContent({ systemId, settings, hideLinkedSignatures, selectable, onSelect, onLazyDeleteChange, -}: SystemSignaturesContentProps) => { - const { outCommand } = useMapRootState(); - - const [signatures, setSignatures, signaturesRef] = useRefState([]); - const [selectedSignatures, setSelectedSignatures] = useState([]); - const [nameColumnWidth, setNameColumnWidth] = useState('auto'); - const [selectedSignature, setSelectedSignature] = useState(null); - - const [hoveredSig, setHoveredSig] = useState(null); - - const [sortSettings, setSortSettings] = useLocalStorageState('window:signatures:sort', { - defaultValue: SORT_DEFAULT_VALUES, - }); - - const tableRef = useRef(null); - const compact = useMaxWidth(tableRef, 260); - const medium = useMaxWidth(tableRef, 380); - const refData = useRef({ selectable }); - refData.current = { selectable }; - - const tooltipRef = useRef(null); - - const { clipboardContent, setClipboardContent } = useClipboard(); - - const lazyDeleteValue = useMemo(() => { - return settings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)?.value ?? false; - }, [settings]); - - const keepLazyDeleteValue = useMemo(() => { - return settings.find(setting => setting.key === KEEP_LAZY_DELETE_SETTING)?.value ?? false; - }, [settings]); - - const handleResize = useCallback(() => { - if (tableRef.current) { - const tableWidth = tableRef.current.offsetWidth; - const otherColumnsWidth = 276; - const availableWidth = tableWidth - otherColumnsWidth; - setNameColumnWidth(`${availableWidth}px`); - } - }, []); - - const groupSettings = useMemo(() => settings.filter(s => (GROUPS_LIST as string[]).includes(s.key)), [settings]); - const showDescriptionColumn = useMemo( - () => settings.find(s => s.key === SHOW_DESCRIPTION_COLUMN_SETTING)?.value, - [settings], - ); - - const showUpdatedColumn = useMemo(() => settings.find(s => s.key === SHOW_UPDATED_COLUMN_SETTING)?.value, [settings]); - - const filteredSignatures = useMemo(() => { - return signatures - .filter(x => { - if (hideLinkedSignatures && !!x.linked_system) { - return false; - } - - const isCosmicSignature = x.kind === COSMIC_SIGNATURE; - const preparedGroup = getGroupIdByRawGroup(x.group); - - if (isCosmicSignature) { - const showCosmicSignatures = settings.find(y => y.key === COSMIC_SIGNATURE)?.value; - if (showCosmicSignatures) { - return !x.group || groupSettings.find(y => y.key === preparedGroup)?.value; - } else { - return !!x.group && groupSettings.find(y => y.key === preparedGroup)?.value; - } - } - - return settings.find(y => y.key === x.kind)?.value; - }) - .sort((a, b) => { - return new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime(); - }); - }, [signatures, settings, groupSettings, hideLinkedSignatures]); - - const handleGetSignatures = useCallback(async () => { - const { signatures } = await outCommand({ - type: OutCommand.getSignatures, - data: { system_id: systemId }, + onCountChange, + onPendingChange, +}: SystemSignaturesContentProps) { + const { signatures, selectedSignatures, setSelectedSignatures, handleDeleteSelected, handleSelectAll, handlePaste } = + useSystemSignaturesData({ + systemId, + settings, + onCountChange, + onPendingChange, + onLazyDeleteChange, }); - setSignatures(signatures); - }, [outCommand, systemId]); - - const handleUpdateSignatures = useCallback( - async (newSignatures: SystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => { - const { added, updated, removed } = getActualSigs( - signaturesRef.current, - newSignatures, - updateOnly, - skipUpdateUntouched, - ); - - const { signatures: updatedSignatures } = await outCommand({ - type: OutCommand.updateSignatures, - data: { - system_id: systemId, - added, - updated, - removed, - }, - }); - - setSignatures(() => updatedSignatures); - setSelectedSignatures([]); - }, - [outCommand, systemId], + const [sortSettings, setSortSettings] = useLocalStorageState<{ sortField: string; sortOrder: SortOrder }>( + 'window:signatures:sort', + { defaultValue: { sortField: 'inserted_at', sortOrder: -1 } }, ); - const handleDeleteSelected = useCallback( - async (e: KeyboardEvent) => { - if (selectable) { - return; - } - if (selectedSignatures.length === 0) { - return; - } + const tableRef = useRef(null); + const tooltipRef = useRef(null); + const [hoveredSignature, setHoveredSignature] = useState(null); - e.preventDefault(); - e.stopPropagation(); + const isCompact = useMaxWidth(tableRef, COMPACT_MAX_WIDTH); + const isMedium = useMaxWidth(tableRef, MEDIUM_MAX_WIDTH); - const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id); - await handleUpdateSignatures( - signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)), - false, - true, - ); - }, - [handleUpdateSignatures, selectable, signatures, selectedSignatures], - ); - - const handleSelectAll = useCallback(() => { - setSelectedSignatures(signatures); - }, [signatures]); - - const handleSelectSignatures = useCallback( - // TODO still will be good to define types if we use typescript - // @ts-ignore - e => { - if (selectable) { - onSelect?.(e.value); - } else { - setSelectedSignatures(e.value); - } - }, - [onSelect, selectable], - ); - - const handlePaste = async (clipboardContent: string) => { - const newSignatures = parseSignatures( - clipboardContent, - settings.map(x => x.key), - ); - - handleUpdateSignatures(newSignatures, !lazyDeleteValue); - - if (lazyDeleteValue && !keepLazyDeleteValue) { - onLazyDeleteChange?.(false); - } - }; - - const handleEnterRow = useCallback( - (e: DataTableRowMouseEvent) => { - setHoveredSig(filteredSignatures[e.index]); - tooltipRef.current?.show(e.originalEvent); - }, - [filteredSignatures], - ); - - const handleLeaveRow = useCallback((e: DataTableRowMouseEvent) => { - tooltipRef.current?.hide(e.originalEvent); - setHoveredSig(null); - }, []); + const lazyDeleteEnabled = settings.find(s => s.key === LAZY_DELETE_SIGNATURES_SETTING)?.value ?? false; + const keepLazyDeleteEnabled = settings.find(s => s.key === KEEP_LAZY_DELETE_SETTING)?.value ?? false; + const { clipboardContent, setClipboardContent } = useClipboard(); useEffect(() => { - if (refData.current.selectable) { - return; - } - - if (!clipboardContent?.text) { - return; - } + if (selectable) return; + if (!clipboardContent?.text) return; handlePaste(clipboardContent.text); + + if (lazyDeleteEnabled && !keepLazyDeleteEnabled) { + onLazyDeleteChange?.(false); + } setClipboardContent(null); - }, [clipboardContent, selectable, lazyDeleteValue, keepLazyDeleteValue]); + }, [ + selectable, + clipboardContent, + handlePaste, + setClipboardContent, + lazyDeleteEnabled, + keepLazyDeleteEnabled, + onLazyDeleteChange, + ]); useHotkey(true, ['a'], handleSelectAll); - useHotkey(false, ['Backspace', 'Delete'], handleDeleteSelected); - - useEffect(() => { - if (!systemId) { - setSignatures([]); - return; - } - - handleGetSignatures(); - }, [systemId]); - - useMapEventListener(event => { - switch (event.name) { - case Commands.signaturesUpdated: - if (event.data?.toString() !== systemId.toString()) { - return; - } - - handleGetSignatures(); - return true; - } + useHotkey(false, ['Backspace', 'Delete'], (event: KeyboardEvent) => { + event.preventDefault(); + event.stopPropagation(); + handleDeleteSelected(); }); - useEffect(() => { - const observer = new ResizeObserver(handleResize); - if (tableRef.current) { - observer.observe(tableRef.current); - } - - handleResize(); // Call on mount to set initial width - - return () => { - if (tableRef.current) { - observer.unobserve(tableRef.current); - } - }; + const [nameColumnWidth, setNameColumnWidth] = useState('auto'); + const handleResize = useCallback(() => { + if (!tableRef.current) return; + const tableWidth = tableRef.current.offsetWidth; + const otherColumnsWidth = OTHER_COLUMNS_WIDTH; + setNameColumnWidth(`${tableWidth - otherColumnsWidth}px`); }, []); + useEffect(() => { + if (!tableRef.current) return; + const observer = new ResizeObserver(handleResize); + observer.observe(tableRef.current); + handleResize(); + return () => { + observer.disconnect(); + }; + }, [handleResize]); - const renderToolbar = (/*row: SystemSignature*/) => { - return ( -
- - - -
- ); - }; - + const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState(null); const [showSignatureSettings, setShowSignatureSettings] = useState(false); const handleRowClick = (e: DataTableRowClickEvent) => { - setSelectedSignature(e.data as SystemSignature); + setSelectedSignatureForDialog(e.data as SystemSignature); setShowSignatureSettings(true); }; - return ( - <> -
- {filteredSignatures.length === 0 ? ( -
- No signatures -
- ) : ( - <> - {/* @ts-ignore */} - setSortSettings(() => ({ sortField: event.sortField, sortOrder: event.sortOrder }))} - onRowMouseEnter={compact || medium ? handleEnterRow : undefined} - onRowMouseLeave={compact || medium ? handleLeaveRow : undefined} - rowClassName={row => { - if (selectedSignatures.some(x => x.eve_id === row.eve_id)) { - return clsx(classes.TableRowCompact, 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200'); - } - - const dateClass = getRowColorByTimeLeft(row.inserted_at ? new Date(row.inserted_at) : undefined); - if (!dateClass) { - return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200'); - } - - return clsx(classes.TableRowCompact, dateClass); - }} - > - renderIcon(x)} - style={{ maxWidth: 26, minWidth: 26, width: 26, height: 25 }} - > - - - - - {showDescriptionColumn && ( - - )} - - - - {showUpdatedColumn && ( - - )} - - {!selectable && ( - - )} - - - )} - : null} - /> - - {showSignatureSettings && ( - setShowSignatureSettings(false)} - signatureData={selectedSignature} - /> - )} -
- + const handleSelectSignatures = useCallback( + (e: { value: SystemSignature[] }) => { + if (selectable) { + onSelect?.(e.value[0]); + } else { + setSelectedSignatures(e.value as ExtendedSystemSignature[]); + } + }, + [selectable, onSelect, setSelectedSignatures], ); -}; + + const groupSettings = settings.filter(s => GROUPS_LIST.includes(s.key as SignatureGroup)); + const showDescriptionColumn = settings.find(s => s.key === SHOW_DESCRIPTION_COLUMN_SETTING)?.value; + const showUpdatedColumn = settings.find(s => s.key === SHOW_UPDATED_COLUMN_SETTING)?.value; + + const filteredSignatures = useMemo(() => { + return signatures.filter(sig => { + if (hideLinkedSignatures && sig.linked_system) { + return false; + } + if (sig.kind === COSMIC_SIGNATURE) { + const showCosmic = settings.find(y => y.key === COSMIC_SIGNATURE)?.value; + if (!showCosmic) { + return false; + } + if (sig.group && groupSettings.find(y => y.key === sig.group)?.value === false) { + return false; + } + return true; + } else { + return settings.find(y => y.key === sig.kind)?.value; + } + }); + }, [signatures, settings, groupSettings, hideLinkedSignatures]); + + return ( +
+ {filteredSignatures.length === 0 ? ( +
+ No signatures +
+ ) : ( + setSortSettings({ sortField: e.sortField, sortOrder: e.sortOrder })} + onRowMouseEnter={ + isCompact || isMedium + ? (e: DataTableRowMouseEvent) => { + setHoveredSignature(filteredSignatures[e.index]); + tooltipRef.current?.show(e.originalEvent); + } + : undefined + } + onRowMouseLeave={ + isCompact || isMedium + ? () => { + setHoveredSignature(null); + tooltipRef.current?.hide(); + } + : undefined + } + rowClassName={rowData => getSignatureRowClass(rowData as ExtendedSystemSignature, selectedSignatures)} + > + renderIcon(sig)} + bodyClassName="p-0 px-1" + style={{ maxWidth: 26, minWidth: 26, width: 26 }} + /> + + sig.group ?? ''} + hidden={isCompact} + sortable + /> + + )} + + : null} + /> + + {showSignatureSettings && ( + setShowSignatureSettings(false)} + signatureData={selectedSignatureForDialog || undefined} + /> + )} +
+ ); +} diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts index de7eda14..1e01b5c9 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts @@ -10,6 +10,13 @@ import { export const TIME_ONE_MINUTE = 1000 * 60; export const TIME_TEN_MINUTES = 1000 * 60 * 10; +export const TIME_ONE_DAY = 24 * 60 * 60 * 1000; +export const TIME_ONE_WEEK = 7 * TIME_ONE_DAY; +export const FINAL_DURATION_MS = 10000; + +export const COMPACT_MAX_WIDTH = 260; +export const MEDIUM_MAX_WIDTH = 380; +export const OTHER_COLUMNS_WIDTH = 276; export const GROUPS_LIST = [ SignatureGroup.GasSite, diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/contentHelpers.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/contentHelpers.ts new file mode 100644 index 00000000..1159d310 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/contentHelpers.ts @@ -0,0 +1,78 @@ +import { SystemSignature } from '@/hooks/Mapper/types'; +import { FINAL_DURATION_MS } from '../constants'; + +export interface ExtendedSystemSignature extends SystemSignature { + pendingDeletion?: boolean; + pendingAddition?: boolean; + pendingUntil?: number; +} + +export function prepareUpdatePayload( + systemId: string, + added: ExtendedSystemSignature[], + updated: ExtendedSystemSignature[], + removed: ExtendedSystemSignature[], +) { + return { + system_id: systemId, + added: added.map(s => ({ ...s })), + updated: updated.map(s => ({ ...s })), + removed: removed.map(s => ({ ...s })), + }; +} + +export function schedulePendingAdditionForSig( + sig: ExtendedSystemSignature, + finalDuration: number, + setSignatures: React.Dispatch>, + pendingAdditionMapRef: React.MutableRefObject>, + setPendingUndoAdditions: React.Dispatch>, +) { + setPendingUndoAdditions(prev => [...prev, sig]); + + const now = Date.now(); + const finalTimeoutId = window.setTimeout(() => { + setSignatures(prev => + prev.map(x => (x.eve_id === sig.eve_id ? { ...x, pendingAddition: false, pendingUntil: undefined } : x)), + ); + const clone = { ...pendingAdditionMapRef.current }; + delete clone[sig.eve_id]; + pendingAdditionMapRef.current = clone; + + setPendingUndoAdditions(prev => prev.filter(x => x.eve_id !== sig.eve_id)); + }, finalDuration); + + pendingAdditionMapRef.current = { + ...pendingAdditionMapRef.current, + [sig.eve_id]: { + finalUntil: now + finalDuration, + finalTimeoutId, + }, + }; + + setSignatures(prev => + prev.map(x => (x.eve_id === sig.eve_id ? { ...x, pendingAddition: true, pendingUntil: now + finalDuration } : x)), + ); +} + +export function scheduleLazyDeletionTimers( + toRemove: ExtendedSystemSignature[], + setPendingMap: React.Dispatch>>, + finalizeRemoval: (sig: ExtendedSystemSignature) => Promise, + finalDuration = FINAL_DURATION_MS, +) { + const now = Date.now(); + toRemove.forEach(sig => { + const finalTimeoutId = window.setTimeout(async () => { + await finalizeRemoval(sig); + }, finalDuration); + + setPendingMap(prev => ({ + ...prev, + [sig.eve_id]: { + finalUntil: now + finalDuration, + finalTimeoutId, + }, + })); + }); +} diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getActualSigs.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getActualSigs.ts index 6ffdc247..ccb8cb29 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getActualSigs.ts +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getActualSigs.ts @@ -1,6 +1,6 @@ -import { SystemSignature } from '@/hooks/Mapper/types'; -import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; -import { getState } from './getState.ts'; +import { SystemSignature, SignatureKind, SignatureGroup } from '@/hooks/Mapper/types'; +import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants'; +import { getState } from './getState'; export const getActualSigs = ( oldSignatures: SystemSignature[], @@ -10,16 +10,68 @@ export const getActualSigs = ( ): { added: SystemSignature[]; updated: SystemSignature[]; removed: SystemSignature[] } => { const updated: SystemSignature[] = []; const removed: SystemSignature[] = []; + const added: SystemSignature[] = []; + const mergedNewIds = new Set(); oldSignatures.forEach(oldSig => { - // if old sigs is not contains in newSigs we need mark it as removed - // otherwise we check - const newSig = newSignatures.find(s => s.eve_id === oldSig.eve_id); + let newSig: SystemSignature | undefined; + if ( + oldSig.kind === SignatureKind.CosmicSignature && + oldSig.group === SignatureGroup.Wormhole && + oldSig.eve_id.length !== 7 + ) { + newSig = newSignatures.find( + s => + s.kind === SignatureKind.CosmicSignature && + s.group === SignatureGroup.Wormhole && + s.eve_id.toUpperCase().startsWith(oldSig.eve_id.toUpperCase() + '-'), + ); + if (newSig) { + const mergedSig: SystemSignature = { ...newSig, kind: oldSig.kind, name: oldSig.name }; + added.push(mergedSig); + removed.push(oldSig); + mergedNewIds.add(newSig.eve_id); + return; + } + } else { + newSig = newSignatures.find(s => s.eve_id === oldSig.eve_id); + } if (newSig) { - // we take new sig and now we need check that sig has been updated - const isNeedUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig); - if (isNeedUpgrade) { - updated.push({ ...oldSig, group: newSig.group, name: newSig.name }); + const needUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig); + const mergedSig = { ...oldSig }; + let changed = false; + if (needUpgrade) { + mergedSig.group = newSig.group; + mergedSig.name = newSig.name; + changed = true; + } + if (newSig.description && newSig.description !== oldSig.description) { + mergedSig.description = newSig.description; + changed = true; + } + try { + const oldInfo = JSON.parse(oldSig.custom_info || '{}'); + const newInfo = JSON.parse(newSig.custom_info || '{}'); + let infoChanged = false; + for (const key in newInfo) { + if (oldInfo[key] !== newInfo[key]) { + oldInfo[key] = newInfo[key]; + infoChanged = true; + } + } + if (infoChanged) { + mergedSig.custom_info = JSON.stringify(oldInfo); + changed = true; + } + } catch (e) { + console.error(`getActualSigs: Error merging custom_info for ${oldSig.eve_id}`, e); + } + if (newSig.updated_at !== oldSig.updated_at) { + mergedSig.updated_at = newSig.updated_at; + changed = true; + } + if (changed) { + updated.push(mergedSig); } else if (!skipUpdateUntouched) { updated.push({ ...oldSig }); } @@ -30,8 +82,11 @@ export const getActualSigs = ( } }); - const oldSignaturesIds = oldSignatures.map(x => x.eve_id); - const added = newSignatures.filter(s => !oldSignaturesIds.includes(s.eve_id)); - + const oldIds = new Set(oldSignatures.map(x => x.eve_id)); + newSignatures.forEach(s => { + if (!oldIds.has(s.eve_id) && !mergedNewIds.has(s.eve_id)) { + added.push(s); + } + }); return { added, updated, removed }; }; diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getRowColorByTimeLeft.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getRowBackgroundColor.ts similarity index 82% rename from assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getRowColorByTimeLeft.ts rename to assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getRowBackgroundColor.ts index 37b21817..f04a53de 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getRowColorByTimeLeft.ts +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getRowBackgroundColor.ts @@ -3,9 +3,9 @@ import { TIME_TEN_MINUTES, } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; -export const getRowColorByTimeLeft = (date: Date | undefined) => { +export const getRowBackgroundColor = (date: Date | undefined): string => { if (!date) { - return null; + return ''; } const currentDate = new Date(); @@ -18,4 +18,6 @@ export const getRowColorByTimeLeft = (date: Date | undefined) => { if (diff < TIME_TEN_MINUTES) { return 'bg-lime-700/40 transition hover:bg-lime-700/50'; } + + return ''; }; diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getState.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getState.ts index b3df81a5..fee63a96 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getState.ts +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/getState.ts @@ -1,11 +1,8 @@ import { SystemSignature } from '@/hooks/Mapper/types'; -// also we need detect changes, we need understand that sigs has states -// first state when kind is Cosmic Signature or Cosmic Anomaly and group is empty -// and we should detect it for ungrade sigs export const getState = (_: string[], newSig: SystemSignature) => { let state = -1; - if (!newSig.group || newSig.group === '') { + if (!newSig.group) { state = 0; } else if (!newSig.name || newSig.name === '') { state = 1; diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/index.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/index.ts index ba69a270..7a81e5f4 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/index.ts +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/index.ts @@ -1,3 +1,5 @@ export * from './getState'; -export * from './getRowColorByTimeLeft'; +export * from './getRowBackgroundColor'; export * from './getActualSigs'; +export * from './contentHelpers'; +export * from './rowStyles'; diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/rowStyles.module.scss b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/rowStyles.module.scss new file mode 100644 index 00000000..23b3aa45 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/rowStyles.module.scss @@ -0,0 +1,34 @@ +.pendingDeletion { + background-color: #f87171; + transition: background-color 0.2s ease; +} + + +.pendingDeletion td { + background-color: #f87171; + transition: background-color 0.2s ease; +} + +.pendingDeletion { + background-color: #f87171; + transition: background-color 0.2s ease; +} + +.Table thead tr { + font-size: 12px !important; + line-height: 1.333; +} + +.TableRowCompact { + font-size: 12px !important; + line-height: 1.333; +} + +.Table td { + padding: 2px; + height: 25px; + border: 1px solid #383838; + box-sizing: border-box; +} + + diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/rowStyles.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/rowStyles.ts new file mode 100644 index 00000000..86715dc4 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers/rowStyles.ts @@ -0,0 +1,20 @@ +import clsx from 'clsx'; +import { ExtendedSystemSignature } from './contentHelpers'; +import { getRowBackgroundColor } from './getRowBackgroundColor'; +import classes from './rowStyles.module.scss'; + +export function getSignatureRowClass( + row: ExtendedSystemSignature, + selectedSignatures: ExtendedSystemSignature[], +): string { + const isSelected = selectedSignatures.some(s => s.eve_id === row.eve_id); + + return clsx( + classes.TableRowCompact, + 'p-selectable-row', + isSelected && 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200', + !isSelected && row.pendingDeletion && classes.pendingDeletion, + !isSelected && getRowBackgroundColor(row.inserted_at ? new Date(row.inserted_at) : undefined), + 'hover:bg-purple-400/20 transition duration-200', + ); +} diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/types.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/types.ts new file mode 100644 index 00000000..c80084c2 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/types.ts @@ -0,0 +1,42 @@ +// types.ts +import { ExtendedSystemSignature } from '../helpers/contentHelpers'; +import { OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers'; // or your function type + +/** + * The aggregator’s props + */ +export interface UseSystemSignaturesDataProps { + systemId: string; + settings: { key: string; value: boolean }[]; + hideLinkedSignatures?: boolean; + onCountChange: (count: number) => void; + onPendingChange?: (pending: ExtendedSystemSignature[], undo: () => void) => void; + onLazyDeleteChange?: (value: boolean) => void; +} + +/** + * The minimal fetch logic + */ +export interface UseFetchingParams { + systemId: string; + signaturesRef: React.MutableRefObject; + setSignatures: React.Dispatch>; + outCommand: OutCommandHandler; + localPendingDeletions: ExtendedSystemSignature[]; +} + +/** + * For the deletion sub-hook + */ +export interface UsePendingDeletionParams { + systemId: string; + outCommand: OutCommandHandler; + setSignatures: React.Dispatch>; +} + +/** + * For the additions sub-hook + */ +export interface UsePendingAdditionParams { + setSignatures: React.Dispatch>; +} diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/usePendingAdditions.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/usePendingAdditions.ts new file mode 100644 index 00000000..abaa3bb3 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/usePendingAdditions.ts @@ -0,0 +1,46 @@ +import { useState, useCallback, useRef } from 'react'; +import { ExtendedSystemSignature, schedulePendingAdditionForSig } from '../helpers/contentHelpers'; +import { UsePendingAdditionParams } from './types'; +import { FINAL_DURATION_MS } from '../constants'; + +export function usePendingAdditions({ setSignatures }: UsePendingAdditionParams) { + const [pendingUndoAdditions, setPendingUndoAdditions] = useState([]); + + const pendingAdditionMapRef = useRef>({}); + + const processAddedSignatures = useCallback( + (added: ExtendedSystemSignature[]) => { + if (!added.length) return; + added.forEach(sig => { + schedulePendingAdditionForSig( + sig, + FINAL_DURATION_MS, + setSignatures, + pendingAdditionMapRef, + setPendingUndoAdditions, + ); + }); + }, + [setSignatures], + ); + + const clearPendingAdditions = useCallback(() => { + Object.values(pendingAdditionMapRef.current).forEach(({ finalTimeoutId }) => { + clearTimeout(finalTimeoutId); + }); + pendingAdditionMapRef.current = {}; + + setSignatures(prev => + prev.map(x => (x.pendingAddition ? { ...x, pendingAddition: false, pendingUntil: undefined } : x)), + ); + setPendingUndoAdditions([]); + }, [setSignatures]); + + return { + pendingUndoAdditions, + setPendingUndoAdditions, + pendingAdditionMapRef, + processAddedSignatures, + clearPendingAdditions, + }; +} diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/usePendingDeletions.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/usePendingDeletions.ts new file mode 100644 index 00000000..24a16db2 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/usePendingDeletions.ts @@ -0,0 +1,77 @@ +import { useState, useCallback } from 'react'; +import { OutCommand } from '@/hooks/Mapper/types/mapHandlers'; +import { ExtendedSystemSignature, prepareUpdatePayload, scheduleLazyDeletionTimers } from '../helpers'; +import { UsePendingDeletionParams } from './types'; +import { FINAL_DURATION_MS } from '../constants'; + +export function usePendingDeletions({ systemId, outCommand, setSignatures }: UsePendingDeletionParams) { + const [localPendingDeletions, setLocalPendingDeletions] = useState([]); + const [pendingDeletionMap, setPendingDeletionMap] = useState< + Record + >({}); + + const processRemovedSignatures = useCallback( + async ( + removed: ExtendedSystemSignature[], + added: ExtendedSystemSignature[], + updated: ExtendedSystemSignature[], + ) => { + if (!removed.length) return; + const processedRemoved = removed.map(r => ({ ...r, pendingDeletion: true, pendingAddition: false })); + setLocalPendingDeletions(prev => [...prev, ...processedRemoved]); + + const resp = await outCommand({ + type: OutCommand.updateSignatures, + data: prepareUpdatePayload(systemId, added, updated, []), + }); + const updatedFromServer = resp.signatures as ExtendedSystemSignature[]; + + scheduleLazyDeletionTimers( + processedRemoved, + setPendingDeletionMap, + async sig => { + await outCommand({ + type: OutCommand.updateSignatures, + data: prepareUpdatePayload(systemId, [], [], [sig]), + }); + setLocalPendingDeletions(prev => prev.filter(x => x.eve_id !== sig.eve_id)); + setSignatures(prev => prev.filter(x => x.eve_id !== sig.eve_id)); + }, + FINAL_DURATION_MS, + ); + + const now = Date.now(); + const updatedWithRemoval = updatedFromServer.map(sig => { + const wasRemoved = processedRemoved.find(r => r.eve_id === sig.eve_id); + return wasRemoved ? { ...sig, pendingDeletion: true, pendingUntil: now + FINAL_DURATION_MS } : sig; + }); + + const extras = processedRemoved + .map(r => ({ ...r, pendingDeletion: true, pendingUntil: now + FINAL_DURATION_MS })) + .filter(r => !updatedWithRemoval.some(m => m.eve_id === r.eve_id)); + + setSignatures([...updatedWithRemoval, ...extras]); + }, + [systemId, outCommand, setSignatures], + ); + + const clearPendingDeletions = useCallback(() => { + Object.values(pendingDeletionMap).forEach(({ finalTimeoutId }) => clearTimeout(finalTimeoutId)); + setPendingDeletionMap({}); + + setSignatures(prev => + prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false, pendingUntil: undefined } : x)), + ); + setLocalPendingDeletions([]); + }, [pendingDeletionMap, setSignatures]); + + return { + localPendingDeletions, + setLocalPendingDeletions, + pendingDeletionMap, + setPendingDeletionMap, + + processRemovedSignatures, + clearPendingDeletions, + }; +} diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSignatureFetching.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSignatureFetching.ts new file mode 100644 index 00000000..e76ecd0a --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSignatureFetching.ts @@ -0,0 +1,54 @@ +import { useCallback } from 'react'; +import { SystemSignature } from '@/hooks/Mapper/types'; +import { OutCommand } from '@/hooks/Mapper/types/mapHandlers'; +import { ExtendedSystemSignature, prepareUpdatePayload, getActualSigs } from '../helpers'; +import { UseFetchingParams } from './types'; + +export function useSignatureFetching({ + systemId, + signaturesRef, + setSignatures, + outCommand, + localPendingDeletions, +}: UseFetchingParams) { + const handleGetSignatures = useCallback(async () => { + if (!systemId) { + setSignatures([]); + return; + } + if (localPendingDeletions.length) { + return; + } + const resp = await outCommand({ + type: OutCommand.getSignatures, + data: { system_id: systemId }, + }); + const serverSigs = (resp.signatures ?? []) as SystemSignature[]; + const extended = serverSigs.map(x => ({ ...x })) as ExtendedSystemSignature[]; + setSignatures(extended); + }, [systemId, localPendingDeletions, outCommand, setSignatures]); + + const handleUpdateSignatures = useCallback( + async (newList: ExtendedSystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => { + const { added, updated, removed } = getActualSigs( + signaturesRef.current, + newList, + updateOnly, + skipUpdateUntouched, + ); + + const resp = await outCommand({ + type: OutCommand.updateSignatures, + data: prepareUpdatePayload(systemId, added, updated, removed), + }); + const final = (resp.signatures ?? []) as SystemSignature[]; + setSignatures(final.map(x => ({ ...x })) as ExtendedSystemSignature[]); + }, + [systemId, signaturesRef, outCommand, setSignatures], + ); + + return { + handleGetSignatures, + handleUpdateSignatures, + }; +} diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts new file mode 100644 index 00000000..8be89fa7 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts @@ -0,0 +1,205 @@ +import { useCallback, useEffect, useState } from 'react'; +import useRefState from 'react-usestateref'; +import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; +import { useMapEventListener } from '@/hooks/Mapper/events'; +import { Commands, SystemSignature } from '@/hooks/Mapper/types'; +import { OutCommand } from '@/hooks/Mapper/types/mapHandlers'; +import { parseSignatures } from '@/hooks/Mapper/helpers'; +import { + KEEP_LAZY_DELETE_SETTING, + LAZY_DELETE_SIGNATURES_SETTING, +} from '@/hooks/Mapper/components/mapInterface/widgets'; +import { ExtendedSystemSignature, getActualSigs } from '../helpers'; +import { useSignatureFetching } from './useSignatureFetching'; +import { usePendingAdditions } from './usePendingAdditions'; +import { usePendingDeletions } from './usePendingDeletions'; +import { UseSystemSignaturesDataProps } from './types'; +import { TIME_ONE_DAY, TIME_ONE_WEEK } from '../constants'; +import { SignatureGroup } from '@/hooks/Mapper/types'; + +export function useSystemSignaturesData({ + systemId, + settings, + onCountChange, + onPendingChange, + onLazyDeleteChange, +}: UseSystemSignaturesDataProps) { + const { outCommand } = useMapRootState(); + + const [signatures, setSignatures, signaturesRef] = useRefState([]); + + const [selectedSignatures, setSelectedSignatures] = useState([]); + + const { localPendingDeletions, setLocalPendingDeletions, processRemovedSignatures, clearPendingDeletions } = + usePendingDeletions({ + systemId, + outCommand, + setSignatures, + }); + + const { pendingUndoAdditions, setPendingUndoAdditions, processAddedSignatures, clearPendingAdditions } = + usePendingAdditions({ + setSignatures, + }); + + const { handleGetSignatures, handleUpdateSignatures } = useSignatureFetching({ + systemId, + signaturesRef, + setSignatures, + outCommand, + localPendingDeletions, + }); + + const handlePaste = useCallback( + async (clipboardString: string) => { + const lazyDeleteValue = settings.find(s => s.key === LAZY_DELETE_SIGNATURES_SETTING)?.value ?? false; + + const incomingSignatures = parseSignatures( + clipboardString, + settings.map(s => s.key), + ) as ExtendedSystemSignature[]; + + const current = signaturesRef.current; + const currentNonPending = lazyDeleteValue + ? current.filter(sig => !sig.pendingDeletion) + : current.filter(sig => !sig.pendingDeletion && !sig.pendingAddition); + + const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, true); + + if (added.length > 0) { + processAddedSignatures(added); + } + if (removed.length > 0) { + await processRemovedSignatures(removed, added, updated); + } else { + const resp = await outCommand({ + type: OutCommand.updateSignatures, + data: { + system_id: systemId, + added, + updated, + removed: [], + }, + }); + const finalSigs = (resp.signatures ?? []) as SystemSignature[]; + setSignatures(finalSigs.map(x => ({ ...x }))); + } + + const keepLazy = settings.find(s => s.key === KEEP_LAZY_DELETE_SETTING)?.value ?? false; + if (lazyDeleteValue && !keepLazy) { + onLazyDeleteChange?.(false); + } + }, + [ + settings, + signaturesRef, + processAddedSignatures, + processRemovedSignatures, + outCommand, + systemId, + setSignatures, + onLazyDeleteChange, + ], + ); + + 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]); + + const handleSelectAll = useCallback(() => { + setSelectedSignatures(signatures); + }, [signatures]); + + const undoPending = useCallback(() => { + clearPendingDeletions(); + clearPendingAdditions(); + + setSignatures(prev => + prev.map(x => { + if (x.pendingDeletion) { + return { ...x, pendingDeletion: false, pendingUntil: undefined }; + } + return x; + }), + ); + + if (pendingUndoAdditions.length) { + pendingUndoAdditions.forEach(async sig => { + await outCommand({ + type: OutCommand.updateSignatures, + data: { + system_id: systemId, + added: [], + updated: [], + removed: [sig], + }, + }); + }); + setSignatures(prev => prev.filter(x => !pendingUndoAdditions.some(u => u.eve_id === x.eve_id))); + setPendingUndoAdditions([]); + } + + setLocalPendingDeletions([]); + }, [ + clearPendingDeletions, + clearPendingAdditions, + pendingUndoAdditions, + setPendingUndoAdditions, + setLocalPendingDeletions, + setSignatures, + outCommand, + systemId, + ]); + + useEffect(() => { + const combined = [...localPendingDeletions, ...pendingUndoAdditions]; + onPendingChange?.(combined, undoPending); + }, [localPendingDeletions, pendingUndoAdditions, onPendingChange, undoPending]); + + useEffect(() => { + if (!systemId) return; + const now = Date.now(); + const oldOnes = signaturesRef.current.filter(sig => { + if (!sig.inserted_at) return false; + const inserted = new Date(sig.inserted_at).getTime(); + const threshold = sig.group === SignatureGroup.Wormhole ? TIME_ONE_DAY : TIME_ONE_WEEK; + return now - inserted > threshold; + }); + if (oldOnes.length) { + const remain = signaturesRef.current.filter(x => !oldOnes.includes(x)); + handleUpdateSignatures(remain, false, true); + } + }, [systemId, handleUpdateSignatures, signaturesRef]); + + useMapEventListener(event => { + if (event.name === Commands.signaturesUpdated && String(event.data) === String(systemId)) { + handleGetSignatures(); + return true; + } + }); + + useEffect(() => { + if (!systemId) { + setSignatures([]); + return; + } + handleGetSignatures(); + }, [systemId, handleGetSignatures, setSignatures]); + + useEffect(() => { + onCountChange(signatures.length); + }, [signatures, onCountChange]); + + return { + signatures, + selectedSignatures, + setSelectedSignatures, + handleDeleteSelected, + handleSelectAll, + handlePaste, + }; +} diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders/renderInfoColumn.tsx b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders/renderInfoColumn.tsx index e49ad6ea..d7974a28 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders/renderInfoColumn.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders/renderInfoColumn.tsx @@ -24,6 +24,12 @@ export const renderInfoColumn = (row: SystemSignature) => { )} + {customInfo.isCrit && ( + +
+
+ )} + {row.type && ( void; - signatureData: SystemSignature | null; + signatureData: SystemSignature | undefined; } export const SignatureSettings = ({ systemId, show, onHide, signatureData }: MapSettingsProps) => {