mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 02:35:42 +00:00
feat: improve signature undo process
This commit is contained in:
@@ -10,7 +10,7 @@ export interface SignatureViewProps {
|
|||||||
export const SignatureView = ({ signature, showCharacterPortrait = false }: SignatureViewProps) => {
|
export const SignatureView = ({ signature, showCharacterPortrait = false }: SignatureViewProps) => {
|
||||||
const isWormhole = signature?.group === SignatureGroup.Wormhole;
|
const isWormhole = signature?.group === SignatureGroup.Wormhole;
|
||||||
const hasCharacterInfo = showCharacterPortrait && signature.character_eve_id;
|
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';
|
const characterName = signature.character_name || 'Unknown character';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export type HeaderProps = {
|
|||||||
lazyDeleteValue: boolean;
|
lazyDeleteValue: boolean;
|
||||||
onLazyDeleteChange: (checked: boolean) => void;
|
onLazyDeleteChange: (checked: boolean) => void;
|
||||||
pendingCount: number;
|
pendingCount: number;
|
||||||
pendingTimeRemaining?: number; // Time remaining in ms
|
undoCountdown?: number;
|
||||||
onUndoClick: () => void;
|
onUndoClick: () => void;
|
||||||
onSettingsClick: () => void;
|
onSettingsClick: () => void;
|
||||||
};
|
};
|
||||||
@@ -29,7 +29,7 @@ export const SystemSignaturesHeader = ({
|
|||||||
lazyDeleteValue,
|
lazyDeleteValue,
|
||||||
onLazyDeleteChange,
|
onLazyDeleteChange,
|
||||||
pendingCount,
|
pendingCount,
|
||||||
pendingTimeRemaining,
|
undoCountdown,
|
||||||
onUndoClick,
|
onUndoClick,
|
||||||
onSettingsClick,
|
onSettingsClick,
|
||||||
}: HeaderProps) => {
|
}: HeaderProps) => {
|
||||||
@@ -43,13 +43,6 @@ export const SystemSignaturesHeader = ({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH);
|
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 (
|
return (
|
||||||
<div ref={containerRef} className="w-full">
|
<div ref={containerRef} className="w-full">
|
||||||
<div className="flex justify-between items-center text-xs w-full h-full">
|
<div className="flex justify-between items-center text-xs w-full h-full">
|
||||||
@@ -78,7 +71,9 @@ export const SystemSignaturesHeader = ({
|
|||||||
<WdImgButton
|
<WdImgButton
|
||||||
className={PrimeIcons.UNDO}
|
className={PrimeIcons.UNDO}
|
||||||
style={{ color: 'red' }}
|
style={{ color: 'red' }}
|
||||||
tooltip={{ content: `Undo pending changes (${pendingCount})${formatTimeRemaining()}` }}
|
tooltip={{
|
||||||
|
content: `Undo pending deletions (${pendingCount})${undoCountdown && undoCountdown > 0 ? ` — ${undoCountdown}s left` : ''}`,
|
||||||
|
}}
|
||||||
onClick={onUndoClick}
|
onClick={onUndoClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||||
import { SystemSignaturesContent } from './SystemSignaturesContent';
|
import { SystemSignaturesContent } from './SystemSignaturesContent';
|
||||||
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
|
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
|
||||||
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
|
|
||||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||||
import { useHotkey } from '@/hooks/Mapper/hooks';
|
|
||||||
import { SystemSignaturesHeader } from './SystemSignatureHeader';
|
import { SystemSignaturesHeader } from './SystemSignatureHeader';
|
||||||
import useLocalStorageState from 'use-local-storage-state';
|
import useLocalStorageState from 'use-local-storage-state';
|
||||||
|
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
|
||||||
import {
|
import {
|
||||||
SETTINGS_KEYS,
|
SETTINGS_KEYS,
|
||||||
SETTINGS_VALUES,
|
SETTINGS_VALUES,
|
||||||
SIGNATURE_DELETION_TIMEOUTS,
|
|
||||||
SIGNATURE_SETTING_STORE_KEY,
|
SIGNATURE_SETTING_STORE_KEY,
|
||||||
SIGNATURE_WINDOW_ID,
|
SIGNATURE_WINDOW_ID,
|
||||||
SIGNATURES_DELETION_TIMING,
|
|
||||||
SignatureSettingsType,
|
SignatureSettingsType,
|
||||||
|
SIGNATURES_DELETION_TIMING,
|
||||||
|
SIGNATURE_DELETION_TIMEOUTS,
|
||||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
} 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);
|
* Custom hook for managing pending signature deletions and undo countdown.
|
||||||
const [sigCount, setSigCount] = useState<number>(0);
|
*/
|
||||||
const [pendingSigs, setPendingSigs] = useState<SystemSignature[]>([]);
|
function useSignatureUndo(
|
||||||
const [pendingTimeRemaining, setPendingTimeRemaining] = useState<number | undefined>();
|
systemId: string | undefined,
|
||||||
const undoPendingFnRef = useRef<() => void>(() => {});
|
settings: SignatureSettingsType,
|
||||||
|
outCommand: OutCommandHandler,
|
||||||
|
) {
|
||||||
|
const [pendingIds, setPendingIds] = useState<string[]>([]);
|
||||||
|
const [countdown, setCountdown] = useState(0);
|
||||||
|
const intervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const {
|
const addDeleted = useCallback((ids: string[]) => {
|
||||||
data: { selectedSystems },
|
setPendingIds(prev => [...prev, ...ids]);
|
||||||
} = useMapRootState();
|
|
||||||
|
|
||||||
const [currentSettings, setCurrentSettings] = useLocalStorageState(SIGNATURE_SETTING_STORE_KEY, {
|
|
||||||
defaultValue: SETTINGS_VALUES,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSigCountChange = useCallback((count: number) => {
|
|
||||||
setSigCount(count);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [systemId] = selectedSystems;
|
// kick off or clear countdown whenever pendingIds changes
|
||||||
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
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pendingSigs.length === 0) {
|
// clear any existing timer
|
||||||
setPendingTimeRemaining(undefined);
|
if (intervalRef.current != null) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingIds.length === 0) {
|
||||||
|
setCountdown(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculate = () => {
|
// determine timeout from settings
|
||||||
setPendingTimeRemaining(() => calculateTimeRemaining(pendingSigs));
|
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();
|
// start new interval
|
||||||
const interval = setInterval(calculate, 1000);
|
intervalRef.current = window.setInterval(() => {
|
||||||
return () => clearInterval(interval);
|
setCountdown(prev => {
|
||||||
}, [pendingSigs]);
|
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 (
|
return (
|
||||||
<Widget
|
<Widget
|
||||||
@@ -101,16 +154,16 @@ export const SystemSignatures = () => {
|
|||||||
<SystemSignaturesHeader
|
<SystemSignaturesHeader
|
||||||
sigCount={sigCount}
|
sigCount={sigCount}
|
||||||
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
|
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
|
||||||
pendingCount={pendingSigs.length}
|
pendingCount={pendingIds.length}
|
||||||
pendingTimeRemaining={pendingTimeRemaining}
|
undoCountdown={countdown}
|
||||||
onLazyDeleteChange={handleLazyDeleteChange}
|
onLazyDeleteChange={handleLazyDeleteToggle}
|
||||||
onUndoClick={handleUndoClick}
|
onUndoClick={handleUndo}
|
||||||
onSettingsClick={handleSettingsButtonClick}
|
onSettingsClick={openSettings}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
windowId={SIGNATURE_WINDOW_ID}
|
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">
|
<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
|
System is not selected
|
||||||
</div>
|
</div>
|
||||||
@@ -118,22 +171,17 @@ export const SystemSignatures = () => {
|
|||||||
<SystemSignaturesContent
|
<SystemSignaturesContent
|
||||||
systemId={systemId}
|
systemId={systemId}
|
||||||
settings={currentSettings}
|
settings={currentSettings}
|
||||||
deletionTiming={
|
onLazyDeleteChange={handleLazyDeleteToggle}
|
||||||
SIGNATURE_DELETION_TIMEOUTS[
|
onCountChange={handleCountChange}
|
||||||
(currentSettings[SETTINGS_KEYS.DELETION_TIMING] as keyof typeof SIGNATURE_DELETION_TIMEOUTS) ||
|
onSignatureDeleted={addDeleted}
|
||||||
SIGNATURES_DELETION_TIMING.DEFAULT
|
|
||||||
] as number
|
|
||||||
}
|
|
||||||
onLazyDeleteChange={handleLazyDeleteChange}
|
|
||||||
onCountChange={handleSigCountChange}
|
|
||||||
onPendingChange={handlePendingChange}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{visible && (
|
{visible && (
|
||||||
<SystemSignatureSettingsDialog
|
<SystemSignatureSettingsDialog
|
||||||
settings={currentSettings}
|
settings={currentSettings}
|
||||||
onCancel={() => setVisible(false)}
|
onCancel={() => setVisible(false)}
|
||||||
onSave={handleSettingsChange}
|
onSave={handleSettingsSave}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Widget>
|
</Widget>
|
||||||
|
|||||||
@@ -57,12 +57,8 @@ interface SystemSignaturesContentProps {
|
|||||||
onSelect?: (signature: SystemSignature) => void;
|
onSelect?: (signature: SystemSignature) => void;
|
||||||
onLazyDeleteChange?: (value: boolean) => void;
|
onLazyDeleteChange?: (value: boolean) => void;
|
||||||
onCountChange?: (count: number) => void;
|
onCountChange?: (count: number) => void;
|
||||||
onPendingChange?: (
|
|
||||||
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
|
|
||||||
undo: () => void,
|
|
||||||
) => void;
|
|
||||||
deletionTiming?: number;
|
|
||||||
filterSignature?: (signature: SystemSignature) => boolean;
|
filterSignature?: (signature: SystemSignature) => boolean;
|
||||||
|
onSignatureDeleted?: (deletedIds: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SystemSignaturesContent = ({
|
export const SystemSignaturesContent = ({
|
||||||
@@ -73,9 +69,8 @@ export const SystemSignaturesContent = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onLazyDeleteChange,
|
onLazyDeleteChange,
|
||||||
onCountChange,
|
onCountChange,
|
||||||
onPendingChange,
|
|
||||||
deletionTiming,
|
|
||||||
filterSignature,
|
filterSignature,
|
||||||
|
onSignatureDeleted,
|
||||||
}: SystemSignaturesContentProps) => {
|
}: SystemSignaturesContentProps) => {
|
||||||
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
|
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
|
||||||
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
|
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
|
||||||
@@ -100,9 +95,8 @@ export const SystemSignaturesContent = ({
|
|||||||
systemId,
|
systemId,
|
||||||
settings,
|
settings,
|
||||||
onCountChange,
|
onCountChange,
|
||||||
onPendingChange,
|
|
||||||
onLazyDeleteChange,
|
onLazyDeleteChange,
|
||||||
deletionTiming,
|
onSignatureDeleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -125,6 +119,10 @@ export const SystemSignaturesContent = ({
|
|||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
if (onSignatureDeleted && selectedSignatures.length > 0) {
|
||||||
|
const deletedIds = selectedSignatures.map(s => s.eve_id);
|
||||||
|
onSignatureDeleted(deletedIds);
|
||||||
|
}
|
||||||
handleDeleteSelected();
|
handleDeleteSelected();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,7 +153,7 @@ export const SystemSignaturesContent = ({
|
|||||||
(e: { value: SystemSignature[] }) => {
|
(e: { value: SystemSignature[] }) => {
|
||||||
selectable ? onSelect?.(e.value[0]) : setSelectedSignatures(e.value as ExtendedSystemSignature[]);
|
selectable ? onSelect?.(e.value[0]) : setSelectedSignatures(e.value as ExtendedSystemSignature[]);
|
||||||
},
|
},
|
||||||
[selectable],
|
[onSelect, selectable, setSelectedSignatures],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
|
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
import { useCallback, useRef, useEffect } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||||
import { prepareUpdatePayload, scheduleLazyTimers } from '../helpers';
|
import { prepareUpdatePayload } from '../helpers';
|
||||||
import { UsePendingDeletionParams } from './types';
|
import { UsePendingDeletionParams } from './types';
|
||||||
import { FINAL_DURATION_MS } from '../constants';
|
|
||||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||||
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
||||||
|
|
||||||
export function usePendingDeletions({
|
export function usePendingDeletions({
|
||||||
systemId,
|
systemId,
|
||||||
setSignatures,
|
setSignatures,
|
||||||
deletionTiming,
|
|
||||||
onPendingChange,
|
onPendingChange,
|
||||||
}: UsePendingDeletionParams) {
|
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) {
|
||||||
const { outCommand } = useMapRootState();
|
const { outCommand } = useMapRootState();
|
||||||
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
|
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(
|
const processRemovedSignatures = useCallback(
|
||||||
async (
|
async (
|
||||||
removed: ExtendedSystemSignature[],
|
removed: ExtendedSystemSignature[],
|
||||||
@@ -25,63 +20,15 @@ export function usePendingDeletions({
|
|||||||
updated: ExtendedSystemSignature[],
|
updated: ExtendedSystemSignature[],
|
||||||
) => {
|
) => {
|
||||||
if (!removed.length) return;
|
if (!removed.length) return;
|
||||||
|
await outCommand({
|
||||||
// If deletion timing is 0, immediately delete without pending state
|
type: OutCommand.updateSignatures,
|
||||||
if (finalDuration === 0) {
|
data: prepareUpdatePayload(systemId, added, updated, removed),
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[systemId, outCommand, finalDuration],
|
[systemId, outCommand],
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearPendingDeletions = useCallback(() => {
|
const clearPendingDeletions = useCallback(() => {
|
||||||
Object.values(pendingDeletionMapRef.current).forEach(({ finalTimeoutId }) => {
|
|
||||||
clearTimeout(finalTimeoutId);
|
|
||||||
});
|
|
||||||
pendingDeletionMapRef.current = {};
|
pendingDeletionMapRef.current = {};
|
||||||
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
|
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
|
||||||
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
|
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export const useSystemSignaturesData = ({
|
|||||||
onCountChange,
|
onCountChange,
|
||||||
onPendingChange,
|
onPendingChange,
|
||||||
onLazyDeleteChange,
|
onLazyDeleteChange,
|
||||||
deletionTiming,
|
onSignatureDeleted,
|
||||||
}: UseSystemSignaturesDataProps) => {
|
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & { onSignatureDeleted?: (deletedIds: string[]) => void }) => {
|
||||||
const { outCommand } = useMapRootState();
|
const { outCommand } = useMapRootState();
|
||||||
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
|
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
|
||||||
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
|
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
|
||||||
@@ -27,7 +27,6 @@ export const useSystemSignaturesData = ({
|
|||||||
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
|
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
|
||||||
systemId,
|
systemId,
|
||||||
setSignatures,
|
setSignatures,
|
||||||
deletionTiming,
|
|
||||||
onPendingChange,
|
onPendingChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,6 +58,10 @@ export const useSystemSignaturesData = ({
|
|||||||
|
|
||||||
if (removed.length > 0) {
|
if (removed.length > 0) {
|
||||||
await processRemovedSignatures(removed, added, updated);
|
await processRemovedSignatures(removed, added, updated);
|
||||||
|
if (onSignatureDeleted) {
|
||||||
|
const deletedIds = removed.map(sig => sig.eve_id);
|
||||||
|
onSignatureDeleted(deletedIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updated.length !== 0 || added.length !== 0) {
|
if (updated.length !== 0 || added.length !== 0) {
|
||||||
@@ -78,17 +81,16 @@ export const useSystemSignaturesData = ({
|
|||||||
onLazyDeleteChange?.(false);
|
onLazyDeleteChange?.(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange],
|
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteSelected = useCallback(async () => {
|
const handleDeleteSelected = useCallback(async () => {
|
||||||
if (!selectedSignatures.length) return;
|
if (!selectedSignatures.length) return;
|
||||||
const selectedIds = selectedSignatures.map(s => s.eve_id);
|
const selectedIds = selectedSignatures.map(s => s.eve_id);
|
||||||
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
|
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
|
||||||
|
|
||||||
await handleUpdateSignatures(finalList, false, true);
|
await handleUpdateSignatures(finalList, false, true);
|
||||||
setSelectedSignatures([]);
|
setSelectedSignatures([]);
|
||||||
}, [selectedSignatures, signatures]);
|
}, [handleUpdateSignatures, selectedSignatures, signatures]);
|
||||||
|
|
||||||
const handleSelectAll = useCallback(() => {
|
const handleSelectAll = useCallback(() => {
|
||||||
setSelectedSignatures(signatures);
|
setSelectedSignatures(signatures);
|
||||||
@@ -119,7 +121,7 @@ export const useSystemSignaturesData = ({
|
|||||||
}, [signatures]);
|
}, [signatures]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
signatures,
|
signatures: signatures.filter(sig => !sig.deleted),
|
||||||
selectedSignatures,
|
selectedSignatures,
|
||||||
setSelectedSignatures,
|
setSelectedSignatures,
|
||||||
handleDeleteSelected,
|
handleDeleteSelected,
|
||||||
|
|||||||
@@ -234,15 +234,12 @@ export enum OutCommand {
|
|||||||
addSystemComment = 'addSystemComment',
|
addSystemComment = 'addSystemComment',
|
||||||
deleteSystemComment = 'deleteSystemComment',
|
deleteSystemComment = 'deleteSystemComment',
|
||||||
getSystemComments = 'getSystemComments',
|
getSystemComments = 'getSystemComments',
|
||||||
// toggleTrack = 'toggle_track',
|
|
||||||
toggleFollow = 'toggle_follow',
|
toggleFollow = 'toggle_follow',
|
||||||
getCharacterInfo = 'getCharacterInfo',
|
getCharacterInfo = 'getCharacterInfo',
|
||||||
getCharactersTrackingInfo = 'getCharactersTrackingInfo',
|
getCharactersTrackingInfo = 'getCharactersTrackingInfo',
|
||||||
updateCharacterTracking = 'updateCharacterTracking',
|
updateCharacterTracking = 'updateCharacterTracking',
|
||||||
updateFollowingCharacter = 'updateFollowingCharacter',
|
updateFollowingCharacter = 'updateFollowingCharacter',
|
||||||
updateMainCharacter = 'updateMainCharacter',
|
updateMainCharacter = 'updateMainCharacter',
|
||||||
|
|
||||||
// Only UI commands
|
|
||||||
openSettings = 'open_settings',
|
openSettings = 'open_settings',
|
||||||
showActivity = 'show_activity',
|
showActivity = 'show_activity',
|
||||||
showTracking = 'show_tracking',
|
showTracking = 'show_tracking',
|
||||||
@@ -250,7 +247,7 @@ export enum OutCommand {
|
|||||||
updateUserSettings = 'update_user_settings',
|
updateUserSettings = 'update_user_settings',
|
||||||
unlinkSignature = 'unlink_signature',
|
unlinkSignature = 'unlink_signature',
|
||||||
searchSystems = 'search_systems',
|
searchSystems = 'search_systems',
|
||||||
|
undoDeleteSignatures = 'undo_delete_signatures',
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
export type OutCommandHandler = <T = unknown>(event: { type: OutCommand; data: unknown }) => Promise<T>;
|
||||||
export type OutCommandHandler = <T = any>(event: { type: OutCommand; data: any }) => Promise<T>;
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type GroupType = {
|
|||||||
export type SignatureCustomInfo = {
|
export type SignatureCustomInfo = {
|
||||||
k162Type?: string;
|
k162Type?: string;
|
||||||
isEOL?: boolean;
|
isEOL?: boolean;
|
||||||
|
isCrit?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SystemSignature = {
|
export type SystemSignature = {
|
||||||
@@ -46,6 +47,7 @@ export type SystemSignature = {
|
|||||||
linked_system?: SolarSystemStaticInfoRaw;
|
linked_system?: SolarSystemStaticInfoRaw;
|
||||||
inserted_at?: string;
|
inserted_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
deleted?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ExtendedSystemSignature extends SystemSignature {
|
export interface ExtendedSystemSignature extends SystemSignature {
|
||||||
@@ -53,6 +55,7 @@ export interface ExtendedSystemSignature extends SystemSignature {
|
|||||||
pendingAddition?: boolean;
|
pendingAddition?: boolean;
|
||||||
pendingUntil?: number;
|
pendingUntil?: number;
|
||||||
finalTimeoutId?: number;
|
finalTimeoutId?: number;
|
||||||
|
deleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SignatureKindENG {
|
export enum SignatureKindENG {
|
||||||
|
|||||||
BIN
assets/static/images/news/05-08-undo/undo.png
Executable file
BIN
assets/static/images/news/05-08-undo/undo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
@@ -24,7 +24,10 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
)
|
)
|
||||||
|
|
||||||
define(:by_system_id, action: :by_system_id, args: [:system_id])
|
define(:by_system_id, action: :by_system_id, args: [:system_id])
|
||||||
|
define(:by_system_id_all, action: :by_system_id_all, args: [:system_id])
|
||||||
|
define(:by_system_id_and_eve_ids, action: :by_system_id_and_eve_ids, args: [:system_id, :eve_ids])
|
||||||
define(:by_linked_system_id, action: :by_linked_system_id, args: [:linked_system_id])
|
define(:by_linked_system_id, action: :by_linked_system_id, args: [:linked_system_id])
|
||||||
|
define(:by_deleted_and_updated_before!, action: :by_deleted_and_updated_before, args: [:deleted, :updated_before])
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
@@ -36,7 +39,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
:description,
|
:description,
|
||||||
:kind,
|
:kind,
|
||||||
:group,
|
:group,
|
||||||
:type
|
:type,
|
||||||
|
:deleted
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:read, :destroy]
|
defaults [:read, :destroy]
|
||||||
@@ -64,7 +68,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
:kind,
|
:kind,
|
||||||
:group,
|
:group,
|
||||||
:type,
|
:type,
|
||||||
:custom_info
|
:custom_info,
|
||||||
|
:deleted
|
||||||
]
|
]
|
||||||
|
|
||||||
argument :system_id, :uuid, allow_nil?: false
|
argument :system_id, :uuid, allow_nil?: false
|
||||||
@@ -83,7 +88,7 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
:group,
|
:group,
|
||||||
:type,
|
:type,
|
||||||
:custom_info,
|
:custom_info,
|
||||||
:updated
|
:deleted
|
||||||
]
|
]
|
||||||
|
|
||||||
primary? true
|
primary? true
|
||||||
@@ -105,14 +110,32 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
read :by_system_id do
|
read :by_system_id do
|
||||||
argument(:system_id, :string, allow_nil?: false)
|
argument(:system_id, :string, allow_nil?: false)
|
||||||
|
|
||||||
|
filter(expr(system_id == ^arg(:system_id) and deleted == false))
|
||||||
|
end
|
||||||
|
|
||||||
|
read :by_system_id_all do
|
||||||
|
argument(:system_id, :string, allow_nil?: false)
|
||||||
filter(expr(system_id == ^arg(:system_id)))
|
filter(expr(system_id == ^arg(:system_id)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
read :by_system_id_and_eve_ids do
|
||||||
|
argument(:system_id, :string, allow_nil?: false)
|
||||||
|
argument(:eve_ids, {:array, :string}, allow_nil?: false)
|
||||||
|
filter(expr(system_id == ^arg(:system_id) and eve_id in ^arg(:eve_ids)))
|
||||||
|
end
|
||||||
|
|
||||||
read :by_linked_system_id do
|
read :by_linked_system_id do
|
||||||
argument(:linked_system_id, :integer, allow_nil?: false)
|
argument(:linked_system_id, :integer, allow_nil?: false)
|
||||||
|
|
||||||
filter(expr(linked_system_id == ^arg(:linked_system_id)))
|
filter(expr(linked_system_id == ^arg(:linked_system_id)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
read :by_deleted_and_updated_before do
|
||||||
|
argument(:deleted, :boolean, allow_nil?: false)
|
||||||
|
argument(:updated_before, :utc_datetime, allow_nil?: false)
|
||||||
|
|
||||||
|
filter(expr(deleted == ^arg(:deleted) and updated_at < ^arg(:updated_before)))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
@@ -149,7 +172,10 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
allow_nil? true
|
allow_nil? true
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :updated, :integer
|
attribute :deleted, :boolean do
|
||||||
|
allow_nil? false
|
||||||
|
default false
|
||||||
|
end
|
||||||
|
|
||||||
create_timestamp(:inserted_at)
|
create_timestamp(:inserted_at)
|
||||||
update_timestamp(:updated_at)
|
update_timestamp(:updated_at)
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ defmodule WandererApp.Map.Manager do
|
|||||||
|
|
||||||
alias WandererApp.Map.Server
|
alias WandererApp.Map.Server
|
||||||
alias WandererApp.Map.ServerSupervisor
|
alias WandererApp.Map.ServerSupervisor
|
||||||
|
alias WandererApp.Api.MapSystemSignature
|
||||||
|
|
||||||
@maps_start_per_second 5
|
@maps_start_per_second 5
|
||||||
@maps_start_interval 1000
|
@maps_start_interval 1000
|
||||||
@maps_queue :maps_queue
|
@maps_queue :maps_queue
|
||||||
@garbage_collection_interval :timer.hours(1)
|
@garbage_collection_interval :timer.hours(1)
|
||||||
@check_maps_queue_interval :timer.seconds(1)
|
@check_maps_queue_interval :timer.seconds(1)
|
||||||
|
@signatures_cleanup_interval :timer.minutes(30)
|
||||||
|
@delete_after_minutes 30
|
||||||
|
|
||||||
def start_map(map_id) when is_binary(map_id),
|
def start_map(map_id) when is_binary(map_id),
|
||||||
do: WandererApp.Queue.push_uniq(@maps_queue, map_id)
|
do: WandererApp.Queue.push_uniq(@maps_queue, map_id)
|
||||||
@@ -44,6 +47,9 @@ defmodule WandererApp.Map.Manager do
|
|||||||
{:ok, garbage_collector_timer} =
|
{:ok, garbage_collector_timer} =
|
||||||
:timer.send_interval(@garbage_collection_interval, :garbage_collect)
|
:timer.send_interval(@garbage_collection_interval, :garbage_collect)
|
||||||
|
|
||||||
|
{:ok, signatures_cleanup_timer} =
|
||||||
|
:timer.send_interval(@signatures_cleanup_interval, :cleanup_signatures)
|
||||||
|
|
||||||
try do
|
try do
|
||||||
Task.async(fn ->
|
Task.async(fn ->
|
||||||
start_last_active_maps()
|
start_last_active_maps()
|
||||||
@@ -56,7 +62,8 @@ defmodule WandererApp.Map.Manager do
|
|||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
garbage_collector_timer: garbage_collector_timer,
|
garbage_collector_timer: garbage_collector_timer,
|
||||||
check_maps_queue_timer: check_maps_queue_timer
|
check_maps_queue_timer: check_maps_queue_timer,
|
||||||
|
signatures_cleanup_timer: signatures_cleanup_timer
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -118,6 +125,36 @@ defmodule WandererApp.Map.Manager do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:cleanup_signatures, state) do
|
||||||
|
try do
|
||||||
|
cleanup_deleted_signatures()
|
||||||
|
{:noreply, state}
|
||||||
|
rescue
|
||||||
|
e ->
|
||||||
|
Logger.error("Failed to cleanup signatures: #{inspect(e)}")
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup_deleted_signatures() do
|
||||||
|
delete_after_date = DateTime.utc_now() |> DateTime.add(-1 * @delete_after_minutes, :minute)
|
||||||
|
|
||||||
|
case MapSystemSignature.by_deleted_and_updated_before!(true, delete_after_date) do
|
||||||
|
{:ok, deleted_signatures} ->
|
||||||
|
|
||||||
|
Enum.each(deleted_signatures, fn sig ->
|
||||||
|
Ash.destroy!(sig)
|
||||||
|
end)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
Logger.error("Failed to fetch deleted signatures: #{inspect(error)}")
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp start_last_active_maps() do
|
defp start_last_active_maps() do
|
||||||
{:ok, last_map_states} =
|
{:ok, last_map_states} =
|
||||||
WandererApp.Api.MapState.get_last_active(
|
WandererApp.Api.MapState.get_last_active(
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ defmodule WandererApp.Map.Server.Impl do
|
|||||||
Process.send_after(self(), :cleanup_connections, 5_000)
|
Process.send_after(self(), :cleanup_connections, 5_000)
|
||||||
Process.send_after(self(), :cleanup_systems, 10_000)
|
Process.send_after(self(), :cleanup_systems, 10_000)
|
||||||
Process.send_after(self(), :cleanup_characters, :timer.minutes(5))
|
Process.send_after(self(), :cleanup_characters, :timer.minutes(5))
|
||||||
|
Process.send_after(self(), :cleanup_signatures, :timer.minutes(30))
|
||||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||||
|
|
||||||
WandererApp.Cache.insert("map_#{map_id}:started", true)
|
WandererApp.Cache.insert("map_#{map_id}:started", true)
|
||||||
@@ -311,6 +312,11 @@ defmodule WandererApp.Map.Server.Impl do
|
|||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event(:cleanup_signatures, state) do
|
||||||
|
Process.send_after(self(), :cleanup_signatures, :timer.minutes(30))
|
||||||
|
state |> SignaturesImpl.cleanup_signatures()
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event(msg, state) do
|
def handle_event(msg, state) do
|
||||||
Logger.warning("Unhandled event: #{inspect(msg)}")
|
Logger.warning("Unhandled event: #{inspect(msg)}")
|
||||||
|
|
||||||
|
|||||||
@@ -3,147 +3,183 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
|||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
alias WandererApp.Api.{MapSystem, MapSystemSignature}
|
||||||
|
alias WandererApp.Character
|
||||||
|
alias WandererApp.User.ActivityTracker
|
||||||
alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl}
|
alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Public entrypoint for updating signatures on a map system.
|
||||||
|
"""
|
||||||
def update_signatures(
|
def update_signatures(
|
||||||
%{map_id: map_id} = state,
|
state = %{map_id: map_id},
|
||||||
%{
|
%{
|
||||||
solar_system_id: solar_system_id,
|
solar_system_id: system_solar_id,
|
||||||
character_id: character_id,
|
character_id: char_id,
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
delete_connection_with_sigs: delete_connection_with_sigs,
|
delete_connection_with_sigs: delete_conn?,
|
||||||
added_signatures: added_signatures,
|
added_signatures: added_params,
|
||||||
updated_signatures: updated_signatures,
|
updated_signatures: updated_params,
|
||||||
removed_signatures: removed_signatures
|
removed_signatures: removed_params
|
||||||
} =
|
}
|
||||||
_signatures_update
|
|
||||||
)
|
)
|
||||||
when not is_nil(character_id) do
|
when not is_nil(char_id) do
|
||||||
WandererApp.Api.MapSystem.read_by_map_and_solar_system(%{
|
with {:ok, system} <-
|
||||||
map_id: map_id,
|
MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: system_solar_id}),
|
||||||
solar_system_id: solar_system_id
|
{:ok, %{eve_id: char_eve_id}} <- Character.get_character(char_id) do
|
||||||
})
|
do_update_signatures(
|
||||||
|> case do
|
state,
|
||||||
{:ok, system} ->
|
system,
|
||||||
{:ok, %{eve_id: character_eve_id}} = WandererApp.Character.get_character(character_id)
|
char_eve_id,
|
||||||
|
user_id,
|
||||||
added_signatures =
|
delete_conn?,
|
||||||
added_signatures
|
added_params,
|
||||||
|> parse_signatures(character_eve_id, system.id)
|
updated_params,
|
||||||
|
removed_params
|
||||||
updated_signatures =
|
)
|
||||||
updated_signatures
|
else
|
||||||
|> parse_signatures(character_eve_id, system.id)
|
error ->
|
||||||
|
Logger.warning("Skipping signature update: #{inspect(error)}")
|
||||||
updated_signatures_eve_ids =
|
|
||||||
updated_signatures
|
|
||||||
|> Enum.map(fn s -> s.eve_id end)
|
|
||||||
|
|
||||||
removed_signatures_eve_ids =
|
|
||||||
removed_signatures
|
|
||||||
|> parse_signatures(character_eve_id, system.id)
|
|
||||||
|> Enum.map(fn s -> s.eve_id end)
|
|
||||||
|
|
||||||
WandererApp.Api.MapSystemSignature.by_system_id!(system.id)
|
|
||||||
|> Enum.filter(fn s -> s.eve_id in removed_signatures_eve_ids end)
|
|
||||||
|> Enum.each(fn s ->
|
|
||||||
if delete_connection_with_sigs && not is_nil(s.linked_system_id) do
|
|
||||||
state
|
|
||||||
|> ConnectionsImpl.delete_connection(%{
|
|
||||||
solar_system_source_id: system.solar_system_id,
|
|
||||||
solar_system_target_id: s.linked_system_id
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
if not is_nil(s.linked_system_id) do
|
|
||||||
state
|
|
||||||
|> SystemsImpl.update_system_linked_sig_eve_id(%{
|
|
||||||
solar_system_id: s.linked_system_id,
|
|
||||||
linked_sig_eve_id: nil
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
s
|
|
||||||
|> Ash.destroy!()
|
|
||||||
end)
|
|
||||||
|
|
||||||
WandererApp.Api.MapSystemSignature.by_system_id!(system.id)
|
|
||||||
|> Enum.filter(fn s -> s.eve_id in updated_signatures_eve_ids end)
|
|
||||||
|> Enum.each(fn s ->
|
|
||||||
updated = updated_signatures |> Enum.find(fn u -> u.eve_id == s.eve_id end)
|
|
||||||
|
|
||||||
if not is_nil(updated) do
|
|
||||||
s
|
|
||||||
|> WandererApp.Api.MapSystemSignature.update(
|
|
||||||
updated
|
|
||||||
|> Map.put(:updated, System.os_time())
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
added_signatures
|
|
||||||
|> Enum.each(fn s ->
|
|
||||||
s |> WandererApp.Api.MapSystemSignature.create!()
|
|
||||||
end)
|
|
||||||
|
|
||||||
added_signatures_eve_ids =
|
|
||||||
added_signatures
|
|
||||||
|> Enum.map(fn s -> s.eve_id end)
|
|
||||||
|
|
||||||
if not (added_signatures_eve_ids |> Enum.empty?()) do
|
|
||||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_added, %{
|
|
||||||
character_id: character_id,
|
|
||||||
user_id: user_id,
|
|
||||||
map_id: map_id,
|
|
||||||
solar_system_id: system.solar_system_id,
|
|
||||||
signatures: added_signatures_eve_ids
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
if not (removed_signatures_eve_ids |> Enum.empty?()) do
|
|
||||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_removed, %{
|
|
||||||
character_id: character_id,
|
|
||||||
user_id: user_id,
|
|
||||||
map_id: map_id,
|
|
||||||
solar_system_id: system.solar_system_id,
|
|
||||||
signatures: removed_signatures_eve_ids
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
Impl.broadcast!(map_id, :signatures_updated, system.solar_system_id)
|
|
||||||
|
|
||||||
state
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
state
|
state
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_signatures(
|
def update_signatures(state, _), do: state
|
||||||
state,
|
|
||||||
_signatures_update
|
|
||||||
),
|
|
||||||
do: state
|
|
||||||
|
|
||||||
defp parse_signatures(signatures, character_eve_id, system_id),
|
defp do_update_signatures(
|
||||||
do:
|
state,
|
||||||
signatures
|
system,
|
||||||
|> Enum.map(fn %{
|
character_eve_id,
|
||||||
"eve_id" => eve_id,
|
user_id,
|
||||||
"name" => name,
|
delete_conn?,
|
||||||
"kind" => kind,
|
added_params,
|
||||||
"group" => group
|
updated_params,
|
||||||
} = signature ->
|
removed_params
|
||||||
%{
|
) do
|
||||||
system_id: system_id,
|
# parse incoming DTOs
|
||||||
eve_id: eve_id,
|
added_sigs = parse_signatures(added_params, character_eve_id, system.id)
|
||||||
name: name,
|
updated_sigs = parse_signatures(updated_params, character_eve_id, system.id)
|
||||||
description: Map.get(signature, "description"),
|
removed_sigs = parse_signatures(removed_params, character_eve_id, system.id)
|
||||||
kind: kind,
|
|
||||||
group: group,
|
# fetch both current & all (including deleted) signatures once
|
||||||
type: Map.get(signature, "type"),
|
existing_current = MapSystemSignature.by_system_id!(system.id)
|
||||||
custom_info: Map.get(signature, "custom_info"),
|
existing_all = MapSystemSignature.by_system_id_all!(system.id)
|
||||||
character_eve_id: character_eve_id
|
|
||||||
}
|
removed_ids = Enum.map(removed_sigs, & &1.eve_id)
|
||||||
end)
|
updated_ids = Enum.map(updated_sigs, & &1.eve_id)
|
||||||
|
added_ids = Enum.map(added_sigs, & &1.eve_id)
|
||||||
|
|
||||||
|
# 1. Removals
|
||||||
|
existing_current
|
||||||
|
|> Enum.filter(&(&1.eve_id in removed_ids))
|
||||||
|
|> Enum.each(&remove_signature(&1, state, system, delete_conn?))
|
||||||
|
|
||||||
|
# 2. Updates
|
||||||
|
existing_current
|
||||||
|
|> Enum.filter(&(&1.eve_id in updated_ids))
|
||||||
|
|> Enum.each(fn existing ->
|
||||||
|
update = Enum.find(updated_sigs, &(&1.eve_id == existing.eve_id))
|
||||||
|
apply_update_signature(existing, update)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# 3. Additions & restorations
|
||||||
|
added_eve_ids = Enum.map(added_sigs, & &1.eve_id)
|
||||||
|
existing_index = MapSystemSignature.by_system_id_all!(system.id)
|
||||||
|
|> Enum.filter(&(&1.eve_id in added_eve_ids))
|
||||||
|
|> Map.new(&{&1.eve_id, &1})
|
||||||
|
|
||||||
|
added_sigs
|
||||||
|
|> Enum.each(fn sig ->
|
||||||
|
case existing_index[sig.eve_id] do
|
||||||
|
nil ->
|
||||||
|
MapSystemSignature.create!(sig)
|
||||||
|
|
||||||
|
%MapSystemSignature{deleted: true} = deleted_sig ->
|
||||||
|
MapSystemSignature.update!(
|
||||||
|
deleted_sig,
|
||||||
|
%{sig | deleted: false}
|
||||||
|
)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
:noop
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# 4. Activity tracking
|
||||||
|
if added_ids != [] do
|
||||||
|
track_activity(:signatures_added, state.map_id, system.solar_system_id, user_id, character_eve_id,
|
||||||
|
added_ids
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if removed_ids != [] do
|
||||||
|
track_activity(
|
||||||
|
:signatures_removed,
|
||||||
|
state.map_id,
|
||||||
|
system.solar_system_id,
|
||||||
|
user_id,
|
||||||
|
character_eve_id,
|
||||||
|
removed_ids
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 5. Broadcast to any live subscribers
|
||||||
|
Impl.broadcast!(state.map_id, :signatures_updated, system.solar_system_id)
|
||||||
|
|
||||||
|
state
|
||||||
|
end
|
||||||
|
|
||||||
|
defp remove_signature(sig, state, system, delete_conn?) do
|
||||||
|
# optionally remove the linked connection
|
||||||
|
if delete_conn? && sig.linked_system_id do
|
||||||
|
ConnectionsImpl.delete_connection(state, %{
|
||||||
|
solar_system_source_id: system.solar_system_id,
|
||||||
|
solar_system_target_id: sig.linked_system_id
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
# clear any linked_sig_eve_id on the target system
|
||||||
|
if sig.linked_system_id do
|
||||||
|
SystemsImpl.update_system_linked_sig_eve_id(state, %{
|
||||||
|
solar_system_id: sig.linked_system_id,
|
||||||
|
linked_sig_eve_id: nil
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
# mark as deleted
|
||||||
|
MapSystemSignature.update!(sig, %{deleted: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_update_signature(%MapSystemSignature{} = existing, update_params)
|
||||||
|
when not is_nil(update_params) do
|
||||||
|
MapSystemSignature.update(existing, update_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do
|
||||||
|
ActivityTracker.track_map_event(event, %{
|
||||||
|
map_id: map_id,
|
||||||
|
solar_system_id: solar_system_id,
|
||||||
|
user_id: user_id,
|
||||||
|
character_id: character_id,
|
||||||
|
signatures: signatures
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
defp parse_signatures(signatures, character_eve_id, system_id) do
|
||||||
|
Enum.map(signatures, fn sig ->
|
||||||
|
%{
|
||||||
|
system_id: system_id,
|
||||||
|
eve_id: sig["eve_id"],
|
||||||
|
name: sig["name"],
|
||||||
|
description: Map.get(sig, "description"),
|
||||||
|
kind: sig["kind"],
|
||||||
|
group: sig["group"],
|
||||||
|
type: Map.get(sig, "type"),
|
||||||
|
custom_info: Map.get(sig, "custom_info"),
|
||||||
|
character_eve_id: character_eve_id,
|
||||||
|
deleted: false
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -269,6 +269,39 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_ui_event(
|
||||||
|
"undo_delete_signatures",
|
||||||
|
%{"system_id" => solar_system_id, "eve_ids" => eve_ids} = payload,
|
||||||
|
%{
|
||||||
|
assigns: %{
|
||||||
|
map_id: map_id,
|
||||||
|
main_character_id: main_character_id,
|
||||||
|
user_permissions: %{update_system: true}
|
||||||
|
}
|
||||||
|
} = socket
|
||||||
|
)
|
||||||
|
when not is_nil(main_character_id) do
|
||||||
|
case WandererApp.Api.MapSystem.read_by_map_and_solar_system(%{
|
||||||
|
map_id: map_id,
|
||||||
|
solar_system_id: get_integer(solar_system_id)
|
||||||
|
}) do
|
||||||
|
{:ok, system} ->
|
||||||
|
restored =
|
||||||
|
WandererApp.Api.MapSystemSignature.by_system_id_all!(system.id)
|
||||||
|
|> Enum.filter(fn s -> s.eve_id in eve_ids end)
|
||||||
|
|> Enum.map(fn s ->
|
||||||
|
s |> WandererApp.Api.MapSystemSignature.update!(%{deleted: false})
|
||||||
|
end)
|
||||||
|
Phoenix.PubSub.broadcast!(WandererApp.PubSub, map_id, %{
|
||||||
|
event: :signatures_updated,
|
||||||
|
payload: system.solar_system_id
|
||||||
|
})
|
||||||
|
{:noreply, socket}
|
||||||
|
_ ->
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def handle_ui_event(event, body, socket),
|
def handle_ui_event(event, body, socket),
|
||||||
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
|
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ defmodule WandererAppWeb.MapEventHandler do
|
|||||||
"update_signatures",
|
"update_signatures",
|
||||||
"get_signatures",
|
"get_signatures",
|
||||||
"link_signature_to_system",
|
"link_signature_to_system",
|
||||||
"unlink_signature"
|
"unlink_signature",
|
||||||
|
"undo_delete_signatures"
|
||||||
]
|
]
|
||||||
|
|
||||||
@map_structures_events [
|
@map_structures_events [
|
||||||
|
|||||||
70
priv/posts/2025/05-08-signature-deletion-flow.md
Normal file
70
priv/posts/2025/05-08-signature-deletion-flow.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
%{
|
||||||
|
title: "Instant Signature Deletion & Undo: A New Flow for Map Signatures",
|
||||||
|
author: "Wanderer Team",
|
||||||
|
cover_image_uri: "/images/news/05-08-undo/undo.png",
|
||||||
|
tags: ~w(signatures deletion undo map realtime guide),
|
||||||
|
description: "Learn about the new instant signature deletion flow, real-time updates, and the ability to undo removals in Wanderer maps."
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Introduction
|
||||||
|
|
||||||
|
Managing cosmic signatures is a core part of mapping and navigation in EVE Online. With our latest update, signature deletion is now **instant, real-time, and reversible**—making it easier than ever to keep your map up to date and error-free.
|
||||||
|
|
||||||
|
This guide covers the new signature deletion flow, how to use the undo feature, and what happens behind the scenes to keep your map clean and synchronized for all users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. The New User Flow: Instant, Real-Time, Reversible
|
||||||
|
|
||||||
|
- **Delete a signature:** When you remove a signature, it disappears from your map (and all other users' maps) instantly after a server roundtrip.
|
||||||
|
- **Undo:** If you make a mistake, you have a window of up to 30s to undo the deletion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. How to Use the New Signature Deletion Flow
|
||||||
|
|
||||||
|
1. **Select and Delete:**
|
||||||
|
- Open the system signatures widget.
|
||||||
|
- Select one or more signatures and click delete (or paste and use lazy delete).
|
||||||
|
- The signatures will disappear for all users viewing the same system.
|
||||||
|
|
||||||
|
2. **Undo a Deletion:**
|
||||||
|
- After deleting, an **Undo** button appears for you (the user who deleted the signature) and remains visible based on your timeout settings.
|
||||||
|
- Click **Undo** to restore the removed signatures instantly for all users.
|
||||||
|
- If you don't click Undo in time, the deletion becomes permanent
|
||||||
|
|
||||||
|
3. **Real-Time Updates:**
|
||||||
|
- All users see signature changes (add, update, remove, undo) in real time—no need to refresh.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. FAQ & Troubleshooting
|
||||||
|
|
||||||
|
|
||||||
|
**Q: Who sees the Undo button?**
|
||||||
|
- Only the user who deleted the signature sees the Undo button
|
||||||
|
|
||||||
|
**Q: Do all users see the same signature list in real time?**
|
||||||
|
- Yes! All changes are broadcast instantly to everyone viewing the same map.
|
||||||
|
|
||||||
|
**Q: Can I configure the undo timeout?**
|
||||||
|
- Yes, in the user inteface settings for the signatures widget
|
||||||
|
|
||||||
|
**Q: What about performance?**
|
||||||
|
- The new flow is optimized for real-time collaboration and efficient cleanup, ensuring your map stays fast and accurate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Summary
|
||||||
|
|
||||||
|
The new signature deletion flow brings instant, real-time updates and a safety net for accidental removals. Enjoy a more collaborative, error-resistant mapping experience—now live for all Wanderer users!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Fly safe,
|
||||||
|
**The Wanderer Team**
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
defmodule WandererApp.Repo.Migrations.AddDeletedSignature do
|
||||||
|
@moduledoc """
|
||||||
|
Updates resources based on their most recent snapshots.
|
||||||
|
|
||||||
|
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
alter table(:map_system_signatures_v1) do
|
||||||
|
remove :updated
|
||||||
|
add :deleted, :boolean, null: false, default: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:map_system_signatures_v1) do
|
||||||
|
remove :deleted
|
||||||
|
add :updated, :bigint
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"gen_random_uuid()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "id",
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "eve_id",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "character_eve_id",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "name",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "description",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "type",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "linked_system_id",
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "kind",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "group",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "custom_info",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "false",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "deleted",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "inserted_at",
|
||||||
|
"type": "utc_datetime_usec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "updated_at",
|
||||||
|
"type": "utc_datetime_usec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": {
|
||||||
|
"deferrable": false,
|
||||||
|
"destination_attribute": "id",
|
||||||
|
"destination_attribute_default": null,
|
||||||
|
"destination_attribute_generated": null,
|
||||||
|
"index?": false,
|
||||||
|
"match_type": null,
|
||||||
|
"match_with": null,
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"name": "map_system_signatures_v1_system_id_fkey",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"schema": "public",
|
||||||
|
"table": "map_system_v1"
|
||||||
|
},
|
||||||
|
"size": null,
|
||||||
|
"source": "system_id",
|
||||||
|
"type": "uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"custom_indexes": [],
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "63D26445C9E67459C4D41CF31D61C3EE2356BE664F0D44AB5BC04C2100B701F3",
|
||||||
|
"identities": [
|
||||||
|
{
|
||||||
|
"all_tenants?": false,
|
||||||
|
"base_filter": null,
|
||||||
|
"index_name": "map_system_signatures_v1_uniq_system_eve_id_index",
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"type": "atom",
|
||||||
|
"value": "system_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "atom",
|
||||||
|
"value": "eve_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "uniq_system_eve_id",
|
||||||
|
"nils_distinct?": true,
|
||||||
|
"where": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.WandererApp.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "map_system_signatures_v1"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user