Compare commits

..

2 Commits

Author SHA1 Message Date
Dmitry Popov
40c57d195a Merge branch 'develop' into bridges 2025-07-18 13:50:16 +02:00
Dmitry Popov
d48c7de152 chore: added new connection type, for jump-bridges 2025-07-15 14:57:06 +02:00
476 changed files with 150336 additions and 25190 deletions

View File

@@ -1,7 +1,5 @@
export WEB_APP_URL="http://localhost:8000"
export RELEASE_COOKIE="PDpbnyo6mEI_0T4ZsHH_ESmi1vT1toQ8PTc0vbfg5FIT4Ih-Lh98mw=="
# Erlang node name for distributed Erlang (optional - defaults to wanderer@hostname)
# export RELEASE_NODE="wanderer@localhost"
export EVE_CLIENT_ID="<EVE_CLIENT_ID>"
export EVE_CLIENT_SECRET="<EVE_CLIENT_SECRET>"
export EVE_CLIENT_WITH_WALLET_ID="<EVE_CLIENT_WITH_WALLET_ID>"
@@ -15,4 +13,4 @@ export WANDERER_KILLS_BASE_URL="ws://host.docker.internal:4004"
export WANDERER_SSE_ENABLED="true"
export WANDERER_WEBHOOKS_ENABLED="true"
export WANDERER_SSE_MAX_CONNECTIONS="1000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"

View File

@@ -1,234 +0,0 @@
name: Build Develop
on:
push:
branches:
- develop
env:
MIX_ENV: prod
GH_TOKEN: ${{ github.token }}
REGISTRY_IMAGE: wandererltd/community-edition
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
build:
name: 🛠 Build
runs-on: ubuntu-22.04
if: ${{ github.ref == 'refs/heads/develop' && github.event_name == 'push' }}
permissions:
checks: write
contents: write
packages: write
attestations: write
id-token: write
pull-requests: write
repository-projects: write
strategy:
matrix:
otp: ["27"]
elixir: ["1.17"]
node-version: ["18.x"]
outputs:
commit_hash: ${{ steps.set-commit-develop.outputs.commit_hash }}
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Setup Elixir
uses: erlef/setup-beam@v1
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
# nix build would also work here because `todos` is the default package
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ssh-key: "${{ secrets.COMMIT_KEY }}"
fetch-depth: 0
- name: 😅 Cache deps
id: cache-deps
uses: actions/cache@v4
env:
cache-name: cache-elixir-deps
with:
path: |
deps
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
- name: 😅 Cache compiled build
id: cache-build
uses: actions/cache@v4
env:
cache-name: cache-compiled-build
with:
path: |
_build
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
restore-keys: |
${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-
${{ runner.os }}-build-
# Step: Download project dependencies. If unchanged, uses
# the cached version.
- name: 🌐 Install dependencies
run: mix deps.get --only "prod"
# Step: Compile the project treating any warnings as errors.
# Customize this step if a different behavior is desired.
- name: 🛠 Compiles without warnings
if: steps.cache-build.outputs.cache-hit != 'true'
run: mix compile
- name: Set commit hash for develop
id: set-commit-develop
run: |
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
docker:
name: 🛠 Build Docker Images
needs: build
runs-on: ubuntu-22.04
permissions:
checks: write
contents: write
packages: write
attestations: write
id-token: write
pull-requests: write
repository-projects: write
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ref: ${{ needs.build.outputs.commit_hash }}
fetch-depth: 0
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.WANDERER_DOCKER_USER }}
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
push: true
context: .
file: ./Dockerfile
cache-from: type=gha
cache-to: type=gha,mode=max
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platform }}
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
build-args: |
MIX_ENV=prod
BUILD_METADATA=${{ steps.meta.outputs.json }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs:
- docker
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.WANDERER_DOCKER_USER }}
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY_IMAGE }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
type=raw,value=develop-{{sha}},enable=${{ github.ref == 'refs/heads/develop' }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
notify:
name: 🏷 Notify about develop release
runs-on: ubuntu-22.04
needs: [docker, merge]
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL_DEV }}
content: |
📣 New develop release available 🚀
**Commit**: `${{ github.sha }}`
**Status**: Development/Testing Release
Docker image: `wandererltd/community-edition:develop`
⚠️ This is an unstable development release for testing purposes.

View File

@@ -4,7 +4,7 @@ on:
push:
branches:
- main
- "releases/*"
env:
MIX_ENV: prod
GH_TOKEN: ${{ github.token }}
@@ -21,7 +21,7 @@ jobs:
build:
name: 🛠 Build
runs-on: ubuntu-22.04
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
if: ${{ (github.ref == 'refs/heads/main') && github.event_name == 'push' }}
permissions:
checks: write
contents: write
@@ -52,7 +52,6 @@ jobs:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ssh-key: "${{ secrets.COMMIT_KEY }}"
fetch-depth: 0
- name: 😅 Cache deps
id: cache-deps
@@ -94,15 +93,9 @@ jobs:
git config --global user.name 'CI'
git config --global user.email 'ci@users.noreply.github.com'
mix git_ops.release --force-patch --yes
git commit --allow-empty -m 'chore: [skip ci]'
git push --follow-tags
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Set commit hash for develop
id: set-commit-develop
run: |
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
docker:
name: 🛠 Build Docker Images
needs: build
@@ -136,17 +129,17 @@ jobs:
ref: ${{ needs.build.outputs.commit_hash }}
fetch-depth: 0
- name: Prepare Changelog
run: |
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
- name: Get Release Tag
id: get-latest-tag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: 1.0.0
- name: Prepare Changelog
run: |
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
@@ -260,25 +253,19 @@ jobs:
create-release:
name: 🏷 Create Release
runs-on: ubuntu-22.04
needs: [docker, merge]
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
needs: build
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get Release Tag
id: get-latest-tag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: 1.0.0
- name: 🏷 Create Draft Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.get-latest-tag.outputs.tag }}
name: Release ${{ steps.get-latest-tag.outputs.tag }}
tag_name: ${{ needs.docker.outputs.release-tag }}
name: Release ${{ needs.docker.outputs.release-tag }}
body: |
## Info
Commit ${{ github.sha }} was deployed to `staging`. [See code diff](${{ github.event.compare }}).
@@ -289,11 +276,6 @@ jobs:
In order to promote this to prod, edit the draft and press **"Publish release"**.
draft: true
notify:
name: 🏷 Notify about release
runs-on: ubuntu-22.04
needs: [docker, merge]
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
with:

View File

@@ -1,3 +1,3 @@
erlang 26.2.5.5
elixir 1.17.3-otp-26
erlang 27.0
elixir 1.17.2-otp-27
nodejs 18.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -21,17 +21,21 @@ RUN mkdir config
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/
COPY priv priv
COPY lib lib
COPY assets assets
RUN mix assets.deploy
RUN mix compile
RUN mix assets.deploy
# Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/
COPY rel rel
COPY rel rel
RUN mix release
# start a new build stage so that the final image will only contain

View File

@@ -30,7 +30,7 @@ format f:
mix format
test t:
MIX_ENV=test mix test
mix test
coverage cover co:
mix test --cover
@@ -45,3 +45,4 @@ versions v:
@cat .tool-versions
@cat Aptfile
@echo

View File

@@ -18,28 +18,5 @@ module.exports = {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
"linebreak-style": "off",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "primereact/button",
"importNames": ["Button"],
"message": "Use WdButton instead Button"
}
]
}
],
"react/forbid-elements": [
"error",
{
"forbid": [
{
"element": "Button",
"message": "Use WdButton instead Button"
}
]
}
]
},
};

View File

@@ -73,9 +73,7 @@ body > div:first-of-type {
}
.maps_bg {
/* OLD image */
/* background-image: url('../images/maps_bg.webp'); */
background-image: url('https://wanderer-industries.github.io/wanderer-assets/images/eve-screen-catalyst-expansion-bg.jpg');
background-image: url('../images/maps_bg.webp');
background-size: cover;
background-position: center;
width: 100%;

View File

@@ -9,7 +9,6 @@ import { useMapperHandlers } from './useMapperHandlers';
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
import './common-styles/main.scss';
import { ToastProvider } from '@/hooks/Mapper/ToastProvider.tsx';
const ErrorFallback = () => {
return <div className="!z-100 absolute w-screen h-screen bg-transparent"></div>;
@@ -40,15 +39,13 @@ export default function MapRoot({ hooks }) {
return (
<PrimeReactProvider>
<ToastProvider>
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand}>
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
<ReactFlowProvider>
<MapRootContent />
</ReactFlowProvider>
</ErrorBoundary>
</MapRootProvider>
</ToastProvider>
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand}>
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
<ReactFlowProvider>
<MapRootContent />
</ReactFlowProvider>
</ErrorBoundary>
</MapRootProvider>
</PrimeReactProvider>
);
}

View File

@@ -1,31 +0,0 @@
import React, { createContext, useContext, useRef } from 'react';
import { Toast } from 'primereact/toast';
import type { ToastMessage } from 'primereact/toast';
interface ToastContextValue {
toastRef: React.RefObject<Toast>;
show: (message: ToastMessage | ToastMessage[]) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const toastRef = useRef<Toast>(null);
const show = (message: ToastMessage | ToastMessage[]) => {
toastRef.current?.show(message);
};
return (
<ToastContext.Provider value={{ toastRef, show }}>
<Toast ref={toastRef} position="top-right" />
{children}
</ToastContext.Provider>
);
};
export const useToast = (): ToastContextValue => {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within a ToastProvider');
return context;
};

View File

@@ -284,7 +284,3 @@
border-left-color: #e67e22;
}
.p-dialog-header-icon.p-dialog-header-close.p-link {
position: relative;
left: 6px;
}

View File

@@ -1,13 +1,18 @@
// import './tailwind.css';
@use 'primereact/resources/primereact.min.css';
@use 'primeicons/primeicons.css';
//@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css';
//@import 'primereact/resources/themes/lara-dark-purple/theme.css';
//@import "prime-fixes";
@import 'primereact/resources/primereact.min.css';
//@import 'primeflex/primeflex.css';
@import 'primeicons/primeicons.css';
//@import 'primereact/resources/primereact.css';
@use "fixes";
@use "prime-fixes";
@use "custom-scrollbar";
@use "tooltip";
@use "context-menu";
@import "fixes";
@import "prime-fixes";
@import "custom-scrollbar";
@import "tooltip";
@import "context-menu";
.fixedImportant {

View File

@@ -1,7 +1,7 @@
.vertical-tabs-container {
display: flex;
width: 100%;
min-height: 200px;
min-height: 300px;
.p-tabview {
width: 100%;
@@ -68,28 +68,6 @@
}
}
&.color-warn {
@apply bg-yellow-600/5 border-r-yellow-600/20;
&:hover {
@apply bg-yellow-600/10 border-r-yellow-600/40;
}
&.p-tabview-selected {
@apply bg-yellow-600/10 border-r-yellow-600;
.p-tabview-nav-link {
@apply text-yellow-600;
}
&:hover {
@apply bg-yellow-600/10 border-r-yellow-600;
}
}
}
}
}

View File

@@ -1,3 +1,6 @@
@use "fix-dialog";
@use "fix-popup";
@use "fix-tabs";
@import "fix-dialog";
@import "fix-popup";
@import "fix-tabs";
//@import "fix-input";
//@import "theme";

View File

@@ -51,8 +51,20 @@ export const Characters = ({ data }: CharactersProps) => {
['border-lime-600/70']: character.online,
},
)}
title={character.name}
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name}
>
{character.tracking_paused && (
<>
<span
className={clsx(
'absolute flex flex-col p-[2px] top-[0px] left-[0px] w-[35px] h-[35px]',
'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
'pi',
PrimeIcons.PAUSE,
)}
/>
</>
)}
{mainCharacterEveId === character.eve_id && (
<span
className={clsx(

View File

@@ -19,7 +19,7 @@ export interface ContextMenuSystemProps {
onSystemStatus(val: number): void;
onSystemLabels(val: string): void;
onCustomLabelDialog(): void;
onTogglePing(type: PingType, solar_system_id: string, ping_id: string | undefined, hasPing: boolean): void;
onTogglePing(type: PingType, solar_system_id: string, hasPing: boolean): void;
onWaypointSet: WaypointSetContextHandler;
}

View File

@@ -5,7 +5,8 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { getSystemById } from '@/hooks/Mapper/helpers';
import clsx from 'clsx';
import { GRADIENT_MENU_ACTIVE_CLASSES } from '@/hooks/Mapper/constants.ts';
import { LayoutEventBlocker, WdButton } from '@/hooks/Mapper/components/ui-kit';
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
import { Button } from 'primereact/button';
const AVAILABLE_TAGS = [
'A',
@@ -60,7 +61,7 @@ export const useTagMenu = (
<LayoutEventBlocker className="flex flex-col gap-1 w-[200px] h-full px-2">
<div className="grid grid-cols-[auto_auto_auto_auto_auto_auto] gap-1">
{AVAILABLE_TAGS.map(x => (
<WdButton
<Button
outlined={system?.tag !== x}
severity="warning"
key={x}
@@ -70,9 +71,9 @@ export const useTagMenu = (
onClick={() => system?.tag !== x && onSystemTag(x)}
>
{x}
</WdButton>
</Button>
))}
<WdButton
<Button
disabled={!isSelectedTag}
icon="pi pi-ban"
size="small"
@@ -80,7 +81,7 @@ export const useTagMenu = (
outlined
severity="help"
onClick={() => onSystemTag()}
></WdButton>
></Button>
</div>
</LayoutEventBlocker>
);

View File

@@ -109,7 +109,7 @@ export const useContextMenuSystemItems = ({
{ separator: true },
{
command: () => onTogglePing(PingType.Rally, systemId, ping?.id, hasPing),
command: () => onTogglePing(PingType.Rally, systemId, hasPing),
disabled: !isShowPingBtn,
template: () => {
const iconClasses = clsx({
@@ -118,11 +118,7 @@ export const useContextMenuSystemItems = ({
});
if (isShowPingBtn) {
return (
<WdMenuItem icon={iconClasses} className="!ml-[-2px]">
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
</WdMenuItem>
);
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>;
}
return (
@@ -130,7 +126,7 @@ export const useContextMenuSystemItems = ({
infoTitle="Locked. Ping can be set only for one system."
infoClass="pi-lock text-stone-500 mr-[12px]"
>
<WdMenuItem disabled icon={iconClasses} className="!ml-[-2px]">
<WdMenuItem disabled icon={iconClasses}>
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
</WdMenuItem>
</MenuItemWithInfo>

View File

@@ -2,60 +2,25 @@ import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { checkPermissions } from '@/hooks/Mapper/components/map/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
export interface ContextMenuSystemMultipleProps {
contextMenuRef: RefObject<ContextMenu>;
onDeleteSystems(): void;
onCopySystems(): void;
}
export const ContextMenuSystemMultiple: React.FC<ContextMenuSystemMultipleProps> = ({
contextMenuRef,
onDeleteSystems,
onCopySystems,
}) => {
const {
data: { options, userPermissions },
} = useMapRootState();
const items: MenuItem[] = useMemo(() => {
const allowCopy = checkPermissions(userPermissions, options.allowed_copy_for);
return [
{
label: 'Delete',
icon: clsx(PrimeIcons.TRASH, 'text-red-400'),
icon: PrimeIcons.TRASH,
command: onDeleteSystems,
},
{ separator: true },
{
label: 'Copy',
icon: PrimeIcons.COPY,
command: onCopySystems,
disabled: !allowCopy,
template: () => {
if (allowCopy) {
return <WdMenuItem icon="pi pi-copy">Copy</WdMenuItem>;
}
return (
<MenuItemWithInfo
infoTitle="Action is blocked because you dont have permission to Copy."
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
tooltipWrapperClassName="flex"
>
<WdMenuItem disabled icon="pi pi-copy">
Copy
</WdMenuItem>
</MenuItemWithInfo>
);
},
},
];
}, [onCopySystems, onDeleteSystems, options, userPermissions]);
}, [onDeleteSystems]);
return (
<>

View File

@@ -1,81 +1,40 @@
import { Node } from 'reactflow';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { encodeJsonToUriBase64 } from '@/hooks/Mapper/utils';
import { useToast } from '@/hooks/Mapper/ToastProvider.tsx';
export const useContextMenuSystemMultipleHandlers = () => {
const {
data: { pings, connections },
} = useMapRootState();
const { show } = useToast();
const contextMenuRef = useRef<ContextMenu | null>(null);
const [systems, setSystems] = useState<Node<SolarSystemRawType>[]>();
const { deleteSystems } = useDeleteSystems();
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
const refVars = useRef({ systems, ping, connections, deleteSystems });
refVars.current = { systems, ping, connections, deleteSystems };
const handleSystemMultipleContext = useCallback<NodeSelectionMouseHandler>((ev, systems_) => {
const handleSystemMultipleContext: NodeSelectionMouseHandler = (ev, systems_) => {
setSystems(systems_);
ev.preventDefault();
ctxManager.next('ctxSysMult', contextMenuRef.current);
contextMenuRef.current?.show(ev);
}, []);
};
const onDeleteSystems = useCallback(() => {
const { systems, ping, deleteSystems } = refVars.current;
if (!systems) {
return;
}
const sysToDel = systems
.filter(x => !x.data.locked)
.filter(x => x.id !== ping?.solar_system_id)
.map(x => x.id);
const sysToDel = systems.filter(x => !x.data.locked).map(x => x.id);
if (sysToDel.length === 0) {
return;
}
deleteSystems(sysToDel);
}, []);
const onCopySystems = useCallback(async () => {
const { systems, connections } = refVars.current;
if (!systems) {
return;
}
const connectionToCopy = connections.filter(
c => systems.filter(s => [c.target, c.source].includes(s.id)).length == 2,
);
await navigator.clipboard.writeText(
encodeJsonToUriBase64({ systems: systems.map(x => x.data), connections: connectionToCopy }),
);
show({
severity: 'success',
summary: 'Copied to clipboard',
detail: `Successfully copied to clipboard - [${systems.length}] systems and [${connectionToCopy.length}] connections`,
life: 3000,
});
}, [show]);
}, [deleteSystems, systems]);
return {
handleSystemMultipleContext,
contextMenuRef,
onDeleteSystems,
onCopySystems,
};
};

View File

@@ -1,10 +1,10 @@
import { useCallback, useRef } from 'react';
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { useCallback, useRef } from 'react';
import classes from './FastSystemActions.module.scss';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import classes from './FastSystemActions.module.scss';
export interface FastSystemActionsProps {
systemId: string;
@@ -27,7 +27,7 @@ export const FastSystemActions = ({
ref.current = { systemId, systemName, regionName, isWH };
const handleOpenZKB = useCallback(
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}/`, '_blank'),
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}`, '_blank'),
[],
);

View File

@@ -8,4 +8,6 @@ export type WaypointSetContextHandlerProps = {
destination: string;
};
export type WaypointSetContextHandler = (props: WaypointSetContextHandlerProps) => void;
export type NodeSelectionMouseHandler = (event: React.MouseEvent<Element, MouseEvent>, nodes: Node[]) => void;
export type NodeSelectionMouseHandler =
| ((event: React.MouseEvent<Element, MouseEvent>, nodes: Node[]) => void)
| undefined;

View File

@@ -1,6 +1,6 @@
import { MapUserSettings, SettingsWrapper } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { MapUserSettings, SettingsWithVersion } from '@/hooks/Mapper/mapRootProvider/types.ts';
export const REQUIRED_KEYS = [
const REQUIRED_KEYS = [
'widgets',
'interface',
'onTheMap',
@@ -19,8 +19,11 @@ export class MapUserSettingsParseError extends Error {
}
}
/** Minimal check that an object matches SettingsWrapper<*> */
const isSettings = (v: unknown): v is SettingsWrapper<unknown> => typeof v === 'object' && v !== null;
const isNumber = (v: unknown): v is number => typeof v === 'number' && !Number.isNaN(v);
/** Minimal check that an object matches SettingsWithVersion<*> */
const isSettingsWithVersion = (v: unknown): v is SettingsWithVersion<unknown> =>
typeof v === 'object' && v !== null && isNumber((v as any).version) && 'settings' in (v as any);
/** Ensure every required key is present */
const hasAllRequiredKeys = (v: unknown): v is Record<RequiredKeys, unknown> =>
@@ -49,8 +52,8 @@ export const parseMapUserSettings = (json: unknown): MapUserSettings => {
}
for (const key of REQUIRED_KEYS) {
if (!isSettings((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWrapper<T>`);
if (!isSettingsWithVersion((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWithVersion<T>`);
}
}

View File

@@ -1,4 +1,3 @@
export * from './useSystemInfo';
export * from './useGetOwnOnlineCharacters';
export * from './useElementWidth';
export * from './useDetectSettingsChanged';

View File

@@ -1,23 +0,0 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useEffect, useState } from 'react';
export const useDetectSettingsChanged = () => {
const {
storedSettings: {
interfaceSettings,
settingsRoutes,
settingsLocal,
settingsSignatures,
settingsOnTheMap,
settingsKills,
},
} = useMapRootState();
const [counter, setCounter] = useState(0);
useEffect(
() => setCounter(x => x + 1),
[interfaceSettings, settingsRoutes, settingsLocal, settingsSignatures, settingsOnTheMap, settingsKills],
);
return counter;
};

View File

@@ -1,26 +0,0 @@
import { useMemo } from 'react';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
export type UseLocalCounterProps = {
charactersInSystem: Array<CharacterTypeRaw>;
userCharacters: string[];
};
export const getLocalCharacters = ({ charactersInSystem, userCharacters }: UseLocalCounterProps) => {
return charactersInSystem
.map(char => ({
...char,
compact: true,
isOwn: userCharacters.includes(char.eve_id),
}))
.sort((a, b) => a.name.localeCompare(b.name));
};
export const useLocalCounter = ({ charactersInSystem, userCharacters }: UseLocalCounterProps) => {
const localCounterCharacters = useMemo(
() => getLocalCharacters({ charactersInSystem, userCharacters }),
[charactersInSystem, userCharacters],
);
return { localCounterCharacters };
};

View File

@@ -1,10 +1,4 @@
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import type { PanelPosition } from '@reactflow/core';
import clsx from 'clsx';
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react';
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import ReactFlow, {
Background,
Edge,
@@ -22,6 +16,8 @@ import ReactFlow, {
import 'reactflow/dist/style.css';
import classes from './Map.module.scss';
import { MapProvider, useMapState } from './MapProvider';
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import {
ContextMenuConnection,
ContextMenuRoot,
@@ -30,11 +26,26 @@ import {
useContextMenuRootHandlers,
} from './components';
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import clsx from 'clsx';
import { useBackgroundVars } from './hooks/useBackgroundVars';
import { MapViewport, OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import type { Viewport } from '@reactflow/core/dist/esm/types';
import { usePrevious } from 'primereact/hooks';
import type { PanelPosition } from '@reactflow/core';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
const getViewPortFromStore = () => {
const restored = localStorage.getItem(SESSION_KEY.viewPort);
if (!restored) {
return { ...DEFAULT_VIEW_PORT };
}
return JSON.parse(restored);
};
const initialNodes: Node<SolarSystemRawType>[] = [
// {
@@ -77,7 +88,6 @@ interface MapCompProps {
onConnectionInfoClick?(e: SolarSystemConnection): void;
onAddSystem?: OnMapAddSystemCallback;
onSelectionContextMenu?: NodeSelectionMouseHandler;
onChangeViewport?: (viewport: MapViewport) => void;
minimapClasses?: string;
isShowMinimap?: boolean;
onSystemContextMenu: (event: MouseEvent<Element>, systemId: string) => void;
@@ -89,7 +99,6 @@ interface MapCompProps {
pings: PingData[];
minimapPlacement?: PanelPosition;
localShowShipName?: boolean;
defaultViewport?: Viewport;
}
const MapComp = ({
@@ -110,25 +119,19 @@ const MapComp = ({
pings,
minimapPlacement = 'bottom-right',
localShowShipName = false,
onChangeViewport,
defaultViewport,
}: MapCompProps) => {
const { getNodes, setViewport } = useReactFlow();
const { getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
useMapHandlers(refn, onSelectionChange);
useUpdateNodes(nodes);
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem, onCommand });
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
const { update } = useMapState();
const { variant, gap, size, color } = useBackgroundVars(theme);
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
const refVars = useRef({ onChangeViewport });
refVars.current = { onChangeViewport };
const nodeTypes = useMemo(() => {
return {
custom: nodeComponent,
@@ -184,10 +187,9 @@ const MapComp = ({
[onSelectionChange],
);
const handleMoveEnd: OnMoveEnd = useCallback((_, viewport) => {
// @ts-ignore
refVars.current.onChangeViewport?.(viewport);
}, []);
const handleMoveEnd: OnMoveEnd = (_, viewport) => {
localStorage.setItem(SESSION_KEY.viewPort, JSON.stringify(viewport));
};
const handleNodesChange = useCallback(
(changes: NodeChange[]) => {
@@ -216,19 +218,6 @@ const MapComp = ({
}));
}, [showKSpaceBG, isThickConnections, pings, update, localShowShipName]);
const prevViewport = usePrevious(defaultViewport);
useEffect(() => {
if (defaultViewport == null) {
return;
}
if (prevViewport == null) {
return;
}
setViewport(defaultViewport);
}, [defaultViewport, prevViewport, setViewport]);
return (
<>
<div
@@ -243,7 +232,7 @@ const MapComp = ({
onConnect={onConnect}
// TODO we need save into session all of this
// and on any action do either
defaultViewport={defaultViewport}
defaultViewport={getViewPortFromStore()}
edgeTypes={edgeTypes}
nodeTypes={nodeTypes}
connectionMode={connectionMode}

View File

@@ -11,7 +11,6 @@ export type MapData = MapUnionTypes & {
isThickConnections: boolean;
linkedSigEveId: string;
localShowShipName: boolean;
systemHighlighted: string | undefined;
};
interface MapProviderProps {
@@ -45,7 +44,6 @@ const INITIAL_DATA: MapData = {
userHubs: [],
pings: [],
localShowShipName: false,
systemHighlighted: undefined,
};
export interface MapContextProps {

View File

@@ -1,4 +1,4 @@
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
.ConnectionTimeEOL {
background-image: linear-gradient(207deg, transparent, var(--conn-time-eol));
@@ -8,10 +8,6 @@
background-image: linear-gradient(207deg, transparent, var(--conn-frigate));
}
.ConnectionBridge {
background-image: linear-gradient(207deg, transparent, var(--conn-bridge));
}
.ConnectionSave {
background-image: linear-gradient(207deg, transparent, var(--conn-save));
}
@@ -19,14 +15,3 @@
.SelectedItem {
background-color: var(--selected-item-bg);
}
.FastActions {
:global {
.p-menuitem-content {
background-color: initial !important;
}
.p-menuitem-content:hover {
background-color: initial !important;
}
}
}

View File

@@ -1,3 +1,10 @@
import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import classes from './ContextMenuConnection.module.scss';
import {
MASS_STATE_NAMES,
MASS_STATE_NAMES_ORDER,
@@ -6,25 +13,14 @@ import {
SHIP_SIZES_NAMES_SHORT,
SHIP_SIZES_SIZE,
} from '@/hooks/Mapper/components/map/constants.ts';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { ContextMenu } from 'primereact/contextmenu';
import { MenuItem } from 'primereact/menuitem';
import React, { RefObject, useMemo } from 'react';
import { Edge } from 'reactflow';
import { LifetimeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/LifetimeActionsWrapper.tsx';
import classes from './ContextMenuConnection.module.scss';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { isNullsecSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
export interface ContextMenuConnectionProps {
contextMenuRef: RefObject<ContextMenu>;
onDeleteConnection(): void;
onChangeTimeState(lifetime: TimeStatus): void;
onChangeTimeState(): void;
onChangeMassState(state: MassState): void;
onChangeShipSizeStatus(state: ShipSizeStatus): void;
onChangeType(type: ConnectionType): void;
onToggleMassSave(isLocked: boolean): void;
onHide(): void;
edge?: Edge<SolarSystemConnection>;
@@ -36,7 +32,6 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
onChangeTimeState,
onChangeMassState,
onChangeShipSizeStatus,
onChangeType,
onToggleMassSave,
onHide,
edge,
@@ -46,106 +41,74 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
return [];
}
const sourceInfo = getSystemStaticInfo(edge.data?.source);
const targetInfo = getSystemStaticInfo(edge.data?.target);
const bothNullsec =
sourceInfo && targetInfo && isNullsecSpace(sourceInfo.system_class) && isNullsecSpace(targetInfo.system_class);
const isFrigateSize = edge.data?.ship_size_type === ShipSizeStatus.small;
if (edge.data?.type === ConnectionType.bridge) {
return [
{
label: `Set as Wormhole`,
icon: 'pi hero-arrow-uturn-left',
command: () => onChangeType(ConnectionType.wormhole),
},
{
label: 'Disconnect',
icon: PrimeIcons.TRASH,
command: onDeleteConnection,
},
];
}
if (edge.data?.type === ConnectionType.gate) {
return [
{
label: 'Disconnect',
icon: PrimeIcons.TRASH,
command: onDeleteConnection,
},
];
}
const isWormhole = edge.data?.type !== ConnectionType.gate;
return [
{
className: clsx(classes.FastActions, '!h-[54px]'),
template: () => {
return <LifetimeActionsWrapper lifetime={edge.data?.time_status} onChangeLifetime={onChangeTimeState} />;
},
},
{
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
),
},
{
label: `Save mass`,
className: clsx({
[classes.ConnectionSave]: edge.data?.locked,
}),
icon: PrimeIcons.LOCK,
command: () => onToggleMassSave(!edge.data?.locked),
},
...(!isFrigateSize
...(isWormhole
? [
{
label: `Mass status`,
icon: PrimeIcons.CHART_PIE,
items: MASS_STATE_NAMES_ORDER.map(x => ({
label: MASS_STATE_NAMES[x],
className: clsx({
[classes.SelectedItem]: edge.data?.mass_status === x,
}),
command: () => onChangeMassState(x),
})),
label: `EOL`,
className: clsx({
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
}),
icon: PrimeIcons.CLOCK,
command: onChangeTimeState,
},
]
: []),
{
label: `Ship Size`,
icon: PrimeIcons.CLOUD,
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
label: (
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
<div>{SHIP_SIZES_NAMES[x]}</div>
<div></div>
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
{SHIP_SIZES_SIZE[x]} t.
</div>
</div>
) as unknown as string, // TODO my lovely kostyl
className: clsx({
[classes.SelectedItem]: edge.data?.ship_size_type === x,
}),
command: () => onChangeShipSizeStatus(x),
})),
},
...(bothNullsec
? [
{
label: `Set as Bridge`,
icon: 'pi hero-forward',
command: () => onChangeType(ConnectionType.bridge),
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
),
},
{
label: `Save mass`,
className: clsx({
[classes.ConnectionSave]: edge.data?.locked,
}),
icon: PrimeIcons.LOCK,
command: () => onToggleMassSave(!edge.data?.locked),
},
...(!isFrigateSize
? [
{
label: `Mass status`,
icon: PrimeIcons.CHART_PIE,
items: MASS_STATE_NAMES_ORDER.map(x => ({
label: MASS_STATE_NAMES[x],
className: clsx({
[classes.SelectedItem]: edge.data?.mass_status === x,
}),
command: () => onChangeMassState(x),
})),
},
]
: []),
{
label: `Ship Size`,
icon: PrimeIcons.CLOUD,
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
label: (
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
<div>{SHIP_SIZES_NAMES[x]}</div>
<div></div>
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
{SHIP_SIZES_SIZE[x]} t.
</div>
</div>
) as unknown as string, // TODO my lovely kostyl
className: clsx({
[classes.SelectedItem]: edge.data?.ship_size_type === x,
}),
command: () => onChangeShipSizeStatus(x),
})),
},
]
: []),
@@ -155,19 +118,11 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
command: onDeleteConnection,
},
];
}, [
edge,
onChangeTimeState,
onDeleteConnection,
onChangeType,
onChangeShipSizeStatus,
onToggleMassSave,
onChangeMassState,
]);
}, [edge, onChangeTimeState, onDeleteConnection, onChangeShipSizeStatus, onToggleMassSave, onChangeMassState]);
return (
<>
<ContextMenu model={items} ref={contextMenuRef} onHide={onHide} breakpoint="767px" className="!w-[250px]" />
<ContextMenu model={items} ref={contextMenuRef} onHide={onHide} breakpoint="767px" />
</>
);
};

View File

@@ -1,12 +0,0 @@
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
import { WdLifetimeSelector, WdLifetimeSelectorProps } from '@/hooks/Mapper/components/ui-kit/WdLifetimeSelector.tsx';
export const LifetimeActionsWrapper = (props: WdLifetimeSelectorProps) => {
return (
<LayoutEventBlocker className="flex flex-col gap-1 w-[100%] h-full px-2 pt-[4px]">
<div className="text-[12px] text-stone-500 font-semibold">Life time:</div>
<WdLifetimeSelector {...props} />
</LayoutEventBlocker>
);
};

View File

@@ -30,7 +30,7 @@ export const useContextMenuConnectionHandlers = () => {
setEdge(undefined);
};
const onChangeTimeState = (lifetime: TimeStatus) => {
const onChangeTimeState = () => {
if (!edge || !edge.data) {
return;
}
@@ -40,7 +40,7 @@ export const useContextMenuConnectionHandlers = () => {
data: {
source: edge.source,
target: edge.target,
value: lifetime,
value: edge.data.time_status === TimeStatus.default ? TimeStatus.eol : TimeStatus.default,
},
});
setEdge(undefined);

View File

@@ -2,70 +2,22 @@ import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { PasteSystemsAndConnections } from '@/hooks/Mapper/components/map/components';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { checkPermissions } from '@/hooks/Mapper/components/map/helpers';
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
export interface ContextMenuRootProps {
contextMenuRef: RefObject<ContextMenu>;
pasteSystemsAndConnections: PasteSystemsAndConnections | undefined;
onAddSystem(): void;
onPasteSystemsAnsConnections(): void;
}
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
contextMenuRef,
onAddSystem,
onPasteSystemsAnsConnections,
pasteSystemsAndConnections,
}) => {
const {
data: { options, userPermissions },
} = useMapState();
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({ contextMenuRef, onAddSystem }) => {
const items: MenuItem[] = useMemo(() => {
const allowPaste = checkPermissions(userPermissions, options.allowed_paste_for);
return [
{
label: 'Add System',
icon: PrimeIcons.PLUS,
command: onAddSystem,
},
...(pasteSystemsAndConnections != null
? [
{
icon: 'pi pi-clipboard',
disabled: !allowPaste,
command: onPasteSystemsAnsConnections,
template: () => {
if (allowPaste) {
return (
<WdMenuItem icon="pi pi-clipboard">
Paste
</WdMenuItem>
);
}
return (
<MenuItemWithInfo
infoTitle="Action is blocked because you dont have permission to Paste."
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
tooltipWrapperClassName="flex"
>
<WdMenuItem disabled icon="pi pi-clipboard">
Paste
</WdMenuItem>
</MenuItemWithInfo>
);
},
},
]
: []),
];
}, [userPermissions, options, onAddSystem, pasteSystemsAndConnections, onPasteSystemsAnsConnections]);
}, [onAddSystem]);
return (
<>

View File

@@ -1,76 +1,36 @@
import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts';
import { recenterSystemsByBounds } from '@/hooks/Mapper/helpers/recenterSystems.ts';
import { OutCommand, OutCommandHandler, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { decodeUriBase64ToJson } from '@/hooks/Mapper/utils';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { ContextMenu } from 'primereact/contextmenu';
import React, { useCallback, useRef, useState } from 'react';
import { useReactFlow, XYPosition } from 'reactflow';
export type PasteSystemsAndConnections = {
systems: SolarSystemRawType[];
connections: SolarSystemConnection[];
};
import React, { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts';
type UseContextMenuRootHandlers = {
onAddSystem?: OnMapAddSystemCallback;
onCommand?: OutCommandHandler;
};
export const useContextMenuRootHandlers = ({ onAddSystem, onCommand }: UseContextMenuRootHandlers = {}) => {
export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHandlers = {}) => {
const rf = useReactFlow();
const contextMenuRef = useRef<ContextMenu | null>(null);
const [position, setPosition] = useState<XYPosition | null>(null);
const [pasteSystemsAndConnections, setPasteSystemsAndConnections] = useState<PasteSystemsAndConnections>();
const handleRootContext = async (e: React.MouseEvent<HTMLDivElement>) => {
const handleRootContext = (e: React.MouseEvent<HTMLDivElement>) => {
setPosition(rf.project({ x: e.clientX, y: e.clientY }));
e.preventDefault();
ctxManager.next('ctxRoot', contextMenuRef.current);
contextMenuRef.current?.show(e);
try {
const text = await navigator.clipboard.readText();
const result = decodeUriBase64ToJson(text);
setPasteSystemsAndConnections(result as PasteSystemsAndConnections);
} catch (err) {
setPasteSystemsAndConnections(undefined);
// do nothing
}
};
const ref = useRef({ onAddSystem, position, pasteSystemsAndConnections, onCommand });
ref.current = { onAddSystem, position, pasteSystemsAndConnections, onCommand };
const ref = useRef({ onAddSystem, position });
ref.current = { onAddSystem, position };
const onAddSystemCallback = useCallback(() => {
ref.current.onAddSystem?.({ coordinates: position });
}, [position]);
const onPasteSystemsAnsConnections = useCallback(async () => {
const { pasteSystemsAndConnections, onCommand, position } = ref.current;
if (!position || !onCommand || !pasteSystemsAndConnections) {
return;
}
const { systems } = recenterSystemsByBounds(pasteSystemsAndConnections.systems);
await onCommand({
type: OutCommand.manualPasteSystemsAndConnections,
data: {
systems: systems.map(({ position: srcPos, ...rest }) => ({
position: { x: Math.round(srcPos.x + position.x), y: Math.round(srcPos.y + position.y) },
...rest,
})),
connections: pasteSystemsAndConnections.connections,
},
});
}, []);
return {
handleRootContext,
pasteSystemsAndConnections,
contextMenuRef,
onAddSystem: onAddSystemCallback,
onPasteSystemsAnsConnections,
};
};

View File

@@ -56,8 +56,7 @@ export const KillsCounter = ({
className={className}
tooltipClassName="!px-0"
size={size}
interactive
smallPaddings
interactive={true}
>
{children}
</WdTooltipWrapper>

View File

@@ -5,8 +5,8 @@
}
.hoverTarget {
padding: 2px;
margin: -2px;
padding: 0.5rem;
margin: -0.5rem;
display: inline-block;
}

View File

@@ -13,19 +13,9 @@ interface LocalCounterProps {
localCounterCharacters: Array<CharItemProps>;
hasUserCharacters: boolean;
showIcon?: boolean;
disableInteractive?: boolean;
className?: string;
contentClassName?: string;
}
export const LocalCounter = ({
className,
contentClassName,
localCounterCharacters,
hasUserCharacters,
showIcon = true,
disableInteractive,
}: LocalCounterProps) => {
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
const {
data: { localShowShipName },
} = useMapState();
@@ -52,30 +42,16 @@ export const LocalCounter = ({
return (
<div
className={clsx(
classes.TooltipActive,
{
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
},
className,
)}
className={clsx(classes.TooltipActive, {
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
})}
>
<WdTooltipWrapper
content={pilotTooltipContent}
position={TooltipPosition.right}
offset={0}
interactive={!disableInteractive}
smallPaddings
>
<WdTooltipWrapper content={pilotTooltipContent} position={TooltipPosition.right} offset={0} interactive={true}>
<div className={clsx(classes.hoverTarget)}>
<div
className={clsx(
classes.localCounter,
{
[classes.hasUserCharacters]: hasUserCharacters,
},
contentClassName,
)}
className={clsx(classes.localCounter, {
[classes.hasUserCharacters]: hasUserCharacters,
})}
>
{showIcon && <i className="pi pi-users" />}
<span>{localCounterCharacters.length}</span>

View File

@@ -1,20 +1,10 @@
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
.EdgePathBack {
fill: none;
stroke: #80a5c5;
stroke-width: 3px;
&.time1 {
stroke: #f11ab2;
stroke-width: 4px;
}
&.time4 {
stroke: #a654e3;
stroke-width: 4px;
}
&.TimeCrit {
stroke: #f11ab2;
stroke-width: 4px;
@@ -39,13 +29,6 @@
&.Gate {
stroke: #9aff40;
}
&.Bridge {
stroke: #9aff40;
stroke-dasharray: 10 5;
stroke-linecap: round;
}
}
.EdgePathFront {

View File

@@ -9,7 +9,6 @@ import { PrimeIcons } from 'primereact/api';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SHIP_SIZES_DESCRIPTION, SHIP_SIZES_NAMES_SHORT } from '@/hooks/Mapper/components/map/constants.ts';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
const MAP_TRANSLATES: Record<string, string> = {
[Position.Top]: 'translate(-48%, 0%)',
@@ -43,9 +42,7 @@ export const SHIP_SIZES_COLORS = {
export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: EdgeProps<SolarSystemConnection>) => {
const sourceNode = useStore(useCallback(store => store.nodeInternals.get(source), [source]));
const targetNode = useStore(useCallback(store => store.nodeInternals.get(target), [target]));
const isWormhole = data?.type === ConnectionType.wormhole;
const isGate = data?.type === ConnectionType.gate;
const isBridge = data?.type === ConnectionType.bridge;
const isWormhole = data?.type !== ConnectionType.gate;
const {
data: { isThickConnections },
@@ -58,7 +55,9 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const [edgePath, labelX, labelY] = getBezierPath({
const method = isWormhole ? getBezierPath : getBezierPath;
const [edgePath, labelX, labelY] = method({
sourceX: sx - offset.x,
sourceY: sy - offset.y,
sourcePosition: sourcePos,
@@ -68,7 +67,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
});
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
}, [isThickConnections, sourceNode, targetNode]);
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
if (!sourceNode || !targetNode || !data) {
return null;
@@ -80,11 +79,9 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
id={`back_${id}`}
className={clsx(classes.EdgePathBack, {
[classes.Tick]: isThickConnections,
[classes.time1]: isWormhole && data.time_status === TimeStatus._1h,
[classes.time4]: isWormhole && data.time_status === TimeStatus._4h,
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
[classes.Hovered]: hovered,
[classes.Gate]: isGate,
[classes.Bridge]: isBridge,
[classes.Gate]: !isWormhole,
})}
d={path}
markerEnd={markerEnd}
@@ -98,8 +95,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
[classes.Gate]: isGate,
[classes.Bridge]: isBridge,
[classes.Gate]: !isWormhole,
})}
d={path}
markerEnd={markerEnd}
@@ -151,19 +147,6 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
</WdTooltipWrapper>
)}
{isBridge && (
<WdTooltipWrapper
content="Ansiblex Jump Bridge"
position={TooltipPosition.top}
className={clsx(
classes.LinkLabel,
'pointer-events-auto bg-lime-300 rounded opacity-100 cursor-auto text-neutral-900',
)}
>
B
</WdTooltipWrapper>
)}
{isWormhole && data.ship_size_type !== ShipSizeStatus.large && (
<WdTooltipWrapper
content={SHIP_SIZES_DESCRIPTION[data.ship_size_type]}

View File

@@ -1,6 +1,13 @@
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@use '@/hooks/Mapper/components/map/styles/solar-system-node' as v;
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
$pastel-blue: #5a7d9a;
$pastel-pink: rgb(30, 161, 255);
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
$neon-color-1: rgb(27, 132, 236);
$neon-color-3: rgba(27, 132, 236, 0.40);
@keyframes move-stripes {
from {
@@ -26,8 +33,8 @@
background-color: var(--rf-node-bg-color, #202020) !important;
color: var(--rf-text-color, #ffffff);
box-shadow: 0 0 5px rgba(v.$dark-bg, 0.5);
border: 1px solid color.adjust(v.$pastel-blue, $lightness: -10%);
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
border: 1px solid darken($pastel-blue, 10%);
border-radius: 5px;
position: relative;
z-index: 3;
@@ -99,7 +106,7 @@
}
&.selected {
border-color: v.$pastel-pink;
border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;
}
@@ -113,11 +120,11 @@
bottom: 0;
z-index: -1;
border-color: v.$neon-color-1;
border-color: $neon-color-1;
background: repeating-linear-gradient(
45deg,
v.$neon-color-3 0px,
v.$neon-color-3 8px,
$neon-color-3 0px,
$neon-color-3 8px,
transparent 8px,
transparent 21px
);
@@ -146,7 +153,7 @@
border: 1px solid var(--eve-solar-system-status-color-lookingFor-dark15);
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
&.selected {
border-color: v.$pastel-pink;
border-color: $pastel-pink;
}
}
@@ -347,13 +354,13 @@
.Handle {
min-width: initial;
min-height: initial;
border: 1px solid v.$pastel-blue;
border: 1px solid $pastel-blue;
width: 5px;
height: 5px;
pointer-events: auto;
&.selected {
border-color: v.$pastel-pink;
border-color: $pastel-pink;
}
&.HandleTop {

View File

@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeDefault.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,
@@ -17,12 +17,10 @@ import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-
import { Tag } from 'primereact/tag';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
import { useLocalCounter } from '@/hooks/Mapper/components/hooks/useLocalCounter.ts';
// let render = 0;
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
nodeVars.solarSystemId,
@@ -141,26 +139,12 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center gap-0.5 justify-end">
<div className={clsx('flex items-center gap-0.5')}>
<div className="flex items-center gap-1 justify-end">
<div className={clsx('flex items-center gap-1')}>
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
)}
{nodeVars.description != null && nodeVars.description !== '' && (
<WdTooltipWrapper
className="h-[15px] transform -translate-y-[6%]"
position={TooltipPosition.top}
content={`System have description`}
>
<i
className={clsx(
'pi hero-chat-bubble-bottom-center-text w-[10px] h-[10px]',
'text-[8px] relative top-[1px]',
)}
/>
</WdTooltipWrapper>
)}
</div>
<LocalCounter
@@ -193,17 +177,6 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</>
)}
{nodeVars.systemHighlighted === nodeVars.solarSystemId && (
<div
className={clsx('absolute top-[-4px] left-[-4px]', 'w-[calc(100%+8px)] h-[calc(100%+8px)]', 'animate-pulse')}
>
<div className="absolute left-0 top-0 w-3 h-2 border-t-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 top-0 w-3 h-2 border-t-2 border-r-2 border-sky-300"></div>
<div className="absolute left-0 bottom-0 w-3 h-2 border-b-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 bottom-0 w-3 h-2 border-b-2 border-r-2 border-sky-300"></div>
</div>
)}
<div className={classes.Handlers}>
<Handle
type="source"

View File

@@ -1,4 +1,4 @@
@use './SolarSystemNodeDefault.module.scss';
@import './SolarSystemNodeDefault.module.scss';
/* ---------------------------------------------
Only override what's different from the base

View File

@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeTheme.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,
@@ -16,15 +16,12 @@ import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
import { useLocalCounter } from '@/hooks/Mapper/components/hooks/useLocalCounter.ts';
// let render = 0;
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
nodeVars.solarSystemId,
);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(nodeVars.solarSystemId);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
@@ -173,17 +170,6 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
</>
)}
{nodeVars.systemHighlighted === nodeVars.solarSystemId && (
<div
className={clsx('absolute top-[-4px] left-[-4px]', 'w-[calc(100%+8px)] h-[calc(100%+8px)]', 'animate-pulse')}
>
<div className="absolute left-0 top-0 w-3 h-2 border-t-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 top-0 w-3 h-2 border-t-2 border-r-2 border-sky-300"></div>
<div className="absolute left-0 bottom-0 w-3 h-2 border-b-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 bottom-0 w-3 h-2 border-b-2 border-r-2 border-sky-300"></div>
</div>
)}
<div className={classes.Handlers}>
<Handle
type="source"

View File

@@ -1,4 +1,4 @@
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
.Signature {
position: relative;

View File

@@ -1,16 +1,15 @@
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
import classes from './UnsplashedSignature.module.scss';
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
import { useMemo } from 'react';
import clsx from 'clsx';
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { TimeStatus } from '@/hooks/Mapper/types';
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
import clsx from 'clsx';
import { useMemo } from 'react';
import classes from './UnsplashedSignature.module.scss';
interface UnsplashedSignatureProps {
signature: SystemSignature;
@@ -36,7 +35,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
}, [customInfo]);
const isEOL = useMemo(() => {
return customInfo?.time_status === TimeStatus._1h;
return customInfo?.isEOL;
}, [customInfo]);
const whClassStyle = useMemo(() => {
@@ -59,7 +58,6 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
</InfoDrawer>
</div>
}
smallPaddings
>
<div className={clsx(classes.Box, whClassStyle)}>
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">

View File

@@ -716,12 +716,11 @@ export const STATUS_CLASSES: Record<number, string> = {
[STATUSES.dangerous]: 'eve-system-status-dangerous',
};
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate, ConnectionType.bridge];
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate];
export const TYPE_NAMES = {
[ConnectionType.wormhole]: 'Wormhole',
[ConnectionType.gate]: 'Gate',
[ConnectionType.bridge]: 'Jumpgate',
};
export const MASS_STATE_NAMES_ORDER = [MassState.verge, MassState.half, MassState.normal];

View File

@@ -1,5 +0,0 @@
import { UserPermission, UserPermissions } from '@/hooks/Mapper/types';
export const checkPermissions = (permissions: Partial<UserPermissions>, targetPermission: UserPermission) => {
return targetPermission != null && permissions[targetPermission];
};

View File

@@ -4,4 +4,3 @@ export * from './getSystemClassStyles';
export * from './getShapeClass';
export * from './getBackgroundClass';
export * from './prepareUnsplashedChunks';
export * from './checkPermissions';

View File

@@ -15,12 +15,3 @@ export const isKnownSpace = (wormholeClassID: number) => {
export const isPossibleSpace = (spaces: number[], wormholeClassID: number) => {
return spaces.includes(wormholeClassID);
};
export const isNullsecSpace = (wormholeClassID: number) => {
switch (wormholeClassID) {
case SOLAR_SYSTEM_CLASS_IDS.ns:
return true;
}
return false;
};

View File

@@ -6,5 +6,5 @@ export * from './useCommandsCharacters';
export * from './useCommandsConnections';
export * from './useCommandsConnections';
export * from './useCenterSystem';
export * from './useSelectSystems';
export * from './useSelectSystem';
export * from './useMapCommands';

View File

@@ -1,18 +1,12 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandCenterSystem } from '@/hooks/Mapper/types';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SYSTEM_FOCUSED_LIFETIME } from '@/hooks/Mapper/constants.ts';
export const useCenterSystem = () => {
const rf = useReactFlow();
const { update } = useMapState();
const ref = useRef({ rf, update });
ref.current = { rf, update };
const highlightTimeout = useRef<number>();
const ref = useRef({ rf });
ref.current = { rf };
return useCallback((systemId: CommandCenterSystem) => {
const systemNode = ref.current.rf.getNodes().find(x => x.data.id === systemId);
@@ -20,16 +14,5 @@ export const useCenterSystem = () => {
return;
}
ref.current.rf.setCenter(systemNode.position.x, systemNode.position.y, { duration: 1000 });
ref.current.update({ systemHighlighted: systemId });
if (highlightTimeout.current !== undefined) {
clearTimeout(highlightTimeout.current);
}
highlightTimeout.current = setTimeout(() => {
highlightTimeout.current = undefined;
ref.current.update({ systemHighlighted: undefined });
}, SYSTEM_FOCUSED_LIFETIME);
}, []);
};

View File

@@ -14,27 +14,8 @@ export const useCommandsCharacters = () => {
const ref = useRef({ update });
ref.current = { update };
const charactersUpdated = useCallback((updatedCharacters: CommandCharactersUpdated) => {
ref.current.update(state => {
const existing = state.characters ?? [];
// Put updatedCharacters into a map keyed by ID
const updatedMap = new Map(updatedCharacters.map(c => [c.eve_id, c]));
// 1. Update existing characters when possible
const merged = existing.map(character => {
const updated = updatedMap.get(character.eve_id);
if (updated) {
updatedMap.delete(character.eve_id); // Mark as processed
return { ...character, ...updated };
}
return character;
});
// 2. Any remaining items in updatedMap are NEW characters → add them
const newCharacters = Array.from(updatedMap.values());
return { characters: [...merged, ...newCharacters] };
});
const charactersUpdated = useCallback((characters: CommandCharactersUpdated) => {
ref.current.update(() => ({ characters: characters.slice() }));
}, []);
const characterAdded = useCallback((value: CommandCharacterAdded) => {

View File

@@ -1,6 +1,4 @@
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useEventBuffer } from '@/hooks/Mapper/hooks';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
@@ -13,20 +11,6 @@ export const useMapInit = () => {
const ref = useRef({ rf, data, update });
ref.current = { update, data, rf };
const updateSystems = useCallback((systems: SolarSystemRawType[]) => {
const { rf } = ref.current;
rf.setNodes(systems.map(convertSystem2Node));
}, []);
const { handleEvent: handleUpdateSystems } = useEventBuffer<any>(updateSystems);
const updateEdges = useCallback((connections: SolarSystemConnection[]) => {
const { rf } = ref.current;
rf.setEdges(connections.map(convertConnection2Edge));
}, []);
const { handleEvent: handleUpdateConnections } = useEventBuffer<any>(updateEdges);
return useCallback(
({
systems,
@@ -38,10 +22,9 @@ export const useMapInit = () => {
user_characters,
present_characters,
hubs,
options,
user_permissions,
}: CommandInit) => {
const { update } = ref.current;
const { rf } = ref.current;
const updateData: Partial<MapData> = {};
@@ -65,14 +48,6 @@ export const useMapInit = () => {
updateData.hubs = hubs;
}
if (options) {
updateData.options = options;
}
if (options) {
updateData.userPermissions = user_permissions;
}
if (systems) {
updateData.systems = systems;
}
@@ -88,13 +63,11 @@ export const useMapInit = () => {
update(updateData);
if (systems) {
handleUpdateSystems(systems);
// rf.setNodes(systems.map(convertSystem2Node));
rf.setNodes(systems.map(convertSystem2Node));
}
if (connections) {
handleUpdateConnections(connections);
// rf.setEdges(connections.map(convertConnection2Edge));
rf.setEdges(connections.map(convertConnection2Edge));
}
},
[],

View File

@@ -0,0 +1,21 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandSelectSystem } from '@/hooks/Mapper/types';
export const useSelectSystem = () => {
const rf = useReactFlow();
const ref = useRef({ rf });
ref.current = { rf };
return useCallback((systemId: CommandSelectSystem) => {
ref.current.rf.setNodes(nds =>
nds.map(node => {
return {
...node,
selected: node.id === systemId,
};
}),
);
}, []);
};

View File

@@ -1,31 +0,0 @@
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import { CommandSelectSystems } from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
const rf = useReactFlow();
const ref = useRef({ rf, onSelectionChange });
ref.current = { rf, onSelectionChange };
return useCallback(({ systems, delay }: CommandSelectSystems) => {
const run = () => {
ref.current.rf.setNodes(nds =>
nds.map(node => {
return {
...node,
selected: systems.includes(node.id),
};
}),
);
};
if (delay == null || delay === 0) {
run();
return;
}
setTimeout(run, delay);
}, []);
};

View File

@@ -1,3 +1,4 @@
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
import {
CommandAddConnections,
CommandAddSystems,
@@ -13,16 +14,12 @@ import {
CommandRemoveSystems,
Commands,
CommandSelectSystem,
CommandSelectSystems,
CommandUpdateConnection,
CommandUpdateSystems,
MapHandlers,
} from '@/hooks/Mapper/types/mapHandlers.ts';
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import {
useCenterSystem,
useCommandsCharacters,
useCommandsConnections,
useMapAddSystems,
@@ -30,8 +27,10 @@ import {
useMapInit,
useMapRemoveSystems,
useMapUpdateSystems,
useSelectSystems,
useCenterSystem,
useSelectSystem,
} from './api';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange: OnMapSelectionChange) => {
const mapInit = useMapInit();
@@ -39,7 +38,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
const mapUpdateSystems = useMapUpdateSystems();
const removeSystems = useMapRemoveSystems(onSelectionChange);
const centerSystem = useCenterSystem();
const selectSystems = useSelectSystems(onSelectionChange);
const selectSystem = useSelectSystem();
const selectRef = useRef({ onSelectionChange });
selectRef.current = { onSelectionChange };
@@ -106,11 +105,14 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
break;
case Commands.selectSystem:
selectSystems({ systems: [data as string], delay: 500 });
break;
case Commands.selectSystems:
selectSystems(data as CommandSelectSystems);
setTimeout(() => {
const systemId = `${data}`;
selectRef.current.onSelectionChange({
systems: [systemId],
connections: [],
});
selectSystem(systemId as CommandSelectSystem);
}, 500);
break;
case Commands.pingAdded:

View File

@@ -5,7 +5,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick';
import { Regions, REGIONS_MAP, SPACE_TO_CLASS } from '@/hooks/Mapper/constants';
import { Regions, REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
@@ -50,9 +50,27 @@ export interface SolarSystemNodeVars {
isRally: boolean;
classTitle: string | null;
temporaryName?: string | null;
description: string | null;
comments_count: number | null;
systemHighlighted: string | undefined;
}
const SpaceToClass: Record<string, string> = {
[Spaces.Caldari]: 'Caldaria',
[Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente',
[Spaces.Pochven]: 'Pochven',
};
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
const localCounterCharacters = useMemo(() => {
return nodeVars.charactersInSystem
.map(char => ({
...char,
compact: true,
isOwn: nodeVars.userCharacters.includes(char.eve_id),
}))
.sort((a, b) => a.name.localeCompare(b.name));
}, [nodeVars.charactersInSystem, nodeVars.userCharacters]);
return { localCounterCharacters };
}
export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars => {
@@ -66,8 +84,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
labels,
temporary_name,
linked_sig_eve_id: linkedSigEveId = '',
description,
comments_count,
} = data;
const {
@@ -109,7 +125,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
showKSpaceBG,
isThickConnections,
pings,
systemHighlighted,
},
outCommand,
} = useMapState();
@@ -154,7 +169,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const showHandlers = isConnecting || hoverNodeId === id;
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SPACE_TO_CLASS[space] || null : null;
const regionClass = showKSpaceBG ? SpaceToClass[space] || null : null;
const { systemName, computedTemporaryName, customName } = useSystemName({
isTempSystemNameEnabled,
@@ -217,9 +232,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
regionName,
solarSystemName: solar_system_name,
isRally,
description,
comments_count,
systemHighlighted,
};
return nodeVars;

View File

@@ -1,7 +1,7 @@
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { useCallback, useEffect, useRef } from 'react';
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
const useThrottle = () => {
const throttleSeed = useRef<number | null>(null);

View File

@@ -10,5 +10,3 @@ export type OnMapSelectionChange = (event: {
}) => void;
export type OnMapAddSystemCallback = (props: { coordinates: XYPosition | null }) => void;
export type MapViewport = { zoom: 1; x: 0; y: 0 };

View File

@@ -1,5 +1,5 @@
@use './eve-common-variables';
@use './eve-common';
@import './eve-common-variables';
@import './eve-common';
.default-theme {
--rf-bg-color: #0C0A09;

View File

@@ -1,19 +1,18 @@
@use "sass:color";
$friendlyBase: #3bbd39;
$friendlyAlpha: #3bbd3952;
$friendlyDark20: color.adjust($friendlyBase, $lightness: -20%);
$friendlyDark30: color.adjust($friendlyBase, $lightness: -30%);
$friendlyDark5: color.adjust($friendlyBase, $lightness: -5%);
$friendlyDark20: darken($friendlyBase, 20%);
$friendlyDark30: darken($friendlyBase, 30%);
$friendlyDark5: darken($friendlyBase, 5%);
$lookingForBase: #43c2fd;
$lookingForAlpha: rgba(67, 176, 253, 0.48);
$lookingForDark15: color.adjust($lookingForBase, $lightness: -15%);
$lookingForDark15: darken($lookingForBase, 15%);
$homeBase: rgb(179, 253, 67);
$homeAlpha: rgba(186, 248, 48, 0.32);
$homeBackground: #a0fa5636;
$homeDark30: color.adjust($homeBase, $lightness: -30%);
$homeDark30: darken($homeBase, 30%);
:root {
--pastel-blue: #5a7d9a;
@@ -118,7 +117,6 @@ $homeDark30: color.adjust($homeBase, $lightness: -30%);
--conn-time-eol: #7452c3e3;
--conn-frigate: #325d88;
--conn-bridge: rgba(135, 185, 93, 0.85);
--conn-save: rgba(155, 102, 45, 0.85);
--selected-item-bg: rgba(98, 98, 98, 0.33);
}

View File

@@ -1,4 +1,4 @@
@use './eve-common-variables';
@import './eve-common-variables';
.eve-wh-effect-color-pulsar {

View File

@@ -1,2 +1,2 @@
@use './default-theme.scss';
@use './pathfinder-theme.scss';
@import './default-theme.scss';
@import './pathfinder-theme.scss';

View File

@@ -1,11 +1,10 @@
@use "sass:color";
@use './eve-common-variables';
@use './eve-common';
@import './eve-common-variables';
@import './eve-common';
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
$homeBase: rgb(197, 253, 67);
$homeAlpha: rgba(197, 253, 67, 0.32);
$homeDark30: color.adjust($homeBase, $lightness: -30%);
$homeDark30: darken($homeBase, 30%);
.pathfinder-theme {
/* -- Override values from the default theme -- */

View File

@@ -1,8 +0,0 @@
$pastel-blue: #5a7d9a;
$pastel-pink: rgb(30, 161, 255);
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
$neon-color-1: rgb(27, 132, 236);
$neon-color-3: rgba(27, 132, 236, 0.40);

View File

@@ -4,13 +4,10 @@ import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constant
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const MapInterface = () => {
// const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
const { windowsSettings, updateWidgetSettings } = useMapRootState();
const items = useMemo(() => {
if (Object.keys(windowsSettings).length === 0) {
return [];
}
return windowsSettings.windows
.map(x => {
const content = DEFAULT_WIDGETS.find(y => y.id === x.id)?.content;

View File

@@ -1,15 +1,16 @@
import { SystemViewStandalone, WdButton, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
import { AutoComplete } from 'primereact/autocomplete';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { IconField } from 'primereact/iconfield';
import { AutoComplete } from 'primereact/autocomplete';
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
import { SystemViewStandalone, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import classes from './AddSystemDialog.module.scss';
import clsx from 'clsx';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import clsx from 'clsx';
export type SearchOnSubmitCallback = (item: SearchSystemItem) => void;
@@ -33,7 +34,6 @@ export const AddSystemDialog = ({
data: { wormholesData },
} = useMapRootState();
// TODO fix it
const inputRef = useRef<any>();
const onShow = useCallback(() => {
inputRef.current?.focus();
@@ -62,7 +62,6 @@ export const AddSystemDialog = ({
},
});
// TODO fix it
let prepared = (result.systems as SearchSystemItem[]).sort((a, b) => {
const amatch = a.label.indexOf(query);
const bmatch = b.label.indexOf(query);
@@ -115,93 +114,90 @@ export const AddSystemDialog = ({
setVisible(false);
}}
>
<form onSubmit={handleSubmit}>
<div className="flex flex-col gap-3 px-1.5">
<div className="flex flex-col gap-2 py-3.5">
<div className="flex flex-col gap-1">
<IconField>
<AutoComplete
ref={inputRef}
multiple
showEmptyMessage
scrollHeight="300px"
value={selectedItem}
suggestions={filteredItems}
completeMethod={searchItems}
onChange={e => {
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
}}
emptyMessage="Not found any system..."
placeholder="Type here..."
field="label"
id="value"
className="w-full"
itemTemplate={(item: SearchSystemItem) => {
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
<div className="flex flex-col gap-3 px-1.5">
<div className="flex flex-col gap-2 py-3.5">
<div className="flex flex-col gap-1">
<IconField>
<AutoComplete
ref={inputRef}
multiple
showEmptyMessage
scrollHeight="300px"
value={selectedItem}
suggestions={filteredItems}
completeMethod={searchItems}
onChange={e => {
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
}}
emptyMessage="Not found any system..."
placeholder="Type here..."
field="label"
id="value"
className="w-full"
itemTemplate={(item: SearchSystemItem) => {
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
return (
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
<SystemViewStandalone
security={security}
system_class={system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
return (
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
<SystemViewStandalone
security={security}
system_class={system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
/>
)}
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
/>
)}
{isWH && (
<div className="flex gap-1 grow justify-between">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
</div>
{isWH && (
<div className="flex gap-1 grow justify-between">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
</div>
)}
</div>
);
}}
selectedItemTemplate={(item: SearchSystemItem) => (
<SystemViewStandalone
security={item.system_static_info.security}
system_class={item.system_static_info.system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
/>
</IconField>
</div>
)}
</div>
);
}}
selectedItemTemplate={(item: SearchSystemItem) => (
<SystemViewStandalone
security={item.system_static_info.security}
system_class={item.system_static_info.system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
/>
</IconField>
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
</div>
</div>
<div className="flex gap-2 justify-end">
<WdButton
type="submit"
onClick={handleSubmit}
outlined
disabled={!selectedItem || selectedItem.length !== 1}
size="small"
label="Submit"
/>
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
</div>
</div>
</form>
<div className="flex gap-2 justify-end">
<Button
onClick={handleSubmit}
outlined
disabled={!selectedItem || selectedItem.length !== 1}
size="small"
label="Submit"
/>
</div>
</div>
</Dialog>
);
};

View File

@@ -1,5 +1,8 @@
.MarkdownTextViewer {
.MarkdownCommentRoot {
border-left-width: 3px;
@apply text-[12px] leading-[1.2] text-stone-300 break-words;
@apply bg-gradient-to-r from-stone-600/40 via-stone-600/10 to-stone-600/0;
.h1 {
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
@@ -53,10 +56,6 @@
@apply font-bold text-green-400 break-words whitespace-normal;
}
strong {
font-weight: bold;
}
i, em {
@apply italic text-pink-400 break-words whitespace-normal;
}

View File

@@ -1,3 +1,4 @@
import classes from './MarkdownComment.module.scss';
import clsx from 'clsx';
import {
InfoDrawer,
@@ -13,7 +14,6 @@ import { PrimeIcons } from 'primereact/api';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
const TOOLTIP_PROPS = { content: 'Remove comment', position: TooltipPosition.top };
@@ -28,7 +28,8 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
const char = useGetCacheCharacter(characterEveId);
const [hovered, setHovered] = useState(false);
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const { outCommand } = useMapRootState();
const ref = useRef({ outCommand, id });
@@ -44,15 +45,14 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
const handleMouseEnter = useCallback(() => setHovered(true), []);
const handleMouseLeave = useCallback(() => setHovered(false), []);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
return (
<>
<InfoDrawer
labelClassName="mb-[3px]"
className={clsx(
'p-1 bg-stone-700/20',
'text-[12px] leading-[1.2] text-stone-300 break-words',
'bg-gradient-to-r from-stone-600/40 via-stone-600/10 to-stone-600/0',
)}
className={clsx(classes.MarkdownCommentRoot, 'p-1 bg-stone-700/20 ')}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
title={
@@ -68,11 +68,11 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
{!hovered && <TimeAgo timestamp={time} />}
{hovered && (
// @ts-ignore
<div ref={cfRef}>
<div ref={cpRemoveBtnRef}>
<WdImgButton
className={clsx(PrimeIcons.TRASH, 'hover:text-red-400')}
tooltip={TOOLTIP_PROPS}
onClick={cfShow}
onClick={handleShowCP}
/>
</div>
)}
@@ -85,9 +85,9 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
</InfoDrawer>
<ConfirmPopup
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
message="Are you sure you want to delete?"
icon="pi pi-exclamation-triangle"
accept={handleDelete}

View File

@@ -1,9 +0,0 @@
.CERoot {
@apply border border-stone-400/30 rounded-[2px];
:global {
.cm-content {
@apply bg-stone-600/40;
}
}
}

View File

@@ -6,7 +6,6 @@ import { useHotkey } from '@/hooks/Mapper/hooks';
import { useCallback, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import classes from './CommentsEditor.module.scss';
export interface CommentsEditorProps {}
@@ -49,7 +48,6 @@ export const CommentsEditor = ({}: CommentsEditorProps) => {
return (
<MarkdownEditor
className={classes.CERoot}
value={textVal}
onChange={setTextVal}
overlayContent={

View File

@@ -1,9 +1,9 @@
.CERoot {
@apply border border-stone-500/30 rounded-[2px];
@apply border border-stone-400/30 rounded-[2px];
:global {
.cm-content {
@apply bg-stone-950/70;
@apply bg-stone-600/40;
}
.cm-scroller {

View File

@@ -44,17 +44,9 @@ export interface MarkdownEditorProps {
overlayContent?: ReactNode;
value: string;
onChange: (value: string) => void;
height?: string;
className?: string;
}
export const MarkdownEditor = ({
value,
onChange,
overlayContent,
height = '70px',
className,
}: MarkdownEditorProps) => {
export const MarkdownEditor = ({ value, onChange, overlayContent }: MarkdownEditorProps) => {
const [hasShift, setHasShift] = useState(false);
const refData = useRef({ onChange });
@@ -74,9 +66,9 @@ export const MarkdownEditor = ({
<div className={clsx(classes.MarkdownEditor, 'relative')}>
<CodeMirror
value={value}
height={height}
height="70px"
extensions={CODE_MIRROR_EXTENSIONS}
className={clsx(classes.CERoot, className)}
className={classes.CERoot}
theme={oneDark}
onChange={handleOnChange}
placeholder="Start typing..."

View File

@@ -4,7 +4,6 @@ import {
SystemView,
TimeAgo,
TooltipPosition,
WdButton,
WdImgButton,
WdImgButtonTooltip,
} from '@/hooks/Mapper/components/ui-kit';
@@ -14,11 +13,11 @@ import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { Toast } from 'primereact/toast';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useRefState from 'react-usestateref';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
const PING_PLACEMENT_MAP = {
[PingsPlacement.rightTop]: 'top-right',
@@ -79,7 +78,9 @@ export interface PingsInterfaceProps {
export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
const toast = useRef<Toast>(null);
const [isShow, setIsShow, isShowRef] = useRefState(false);
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const {
storedSettings: { interfaceSettings },
@@ -97,6 +98,9 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
const ping = useMemo(() => (pings.length === 1 ? pings[0] : null), [pings]);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
const navigateTo = useCallback(() => {
if (!ping) {
return;
@@ -238,11 +242,11 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
/>
{/*@ts-ignore*/}
<div ref={cfRef}>
<div ref={cpRemoveBtnRef}>
<WdImgButton
className={clsx('pi-trash', 'text-red-400 hover:text-red-300')}
tooltip={DELETE_TOOLTIP_PROPS}
onClick={cfShow}
onClick={handleShowCP}
/>
</div>
{/* TODO ADD solar system menu*/}
@@ -256,7 +260,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
)}
></Toast>
<WdButton
<Button
icon="pi pi-bell"
severity="warning"
aria-label="Notification"
@@ -268,9 +272,9 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
/>
<ConfirmPopup
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
message="Are you sure you want to delete ping?"
icon="pi pi-exclamation-triangle text-orange-400"
accept={removePing}

View File

@@ -1,12 +1,13 @@
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { InputText } from 'primereact/inputtext';
import { Dialog } from 'primereact/dialog';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { InputText } from 'primereact/inputtext';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { IconField } from 'primereact/iconfield';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { WdImageSize, WdImgButton, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
interface SystemCustomLabelDialog {
systemId: string;
@@ -125,7 +126,7 @@ export const SystemCustomLabelDialog = ({ systemId, visible, setVisible }: Syste
</div>
<div className="flex gap-2 justify-end">
<WdButton type="submit" onClick={handleSave} outlined size="small" label="Save"></WdButton>
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
</div>
</div>
</form>

View File

@@ -9,12 +9,12 @@ import {
} from '@/hooks/Mapper/components/map/constants.ts';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useSystemSignaturesData } from '../../widgets/SystemSignatures/hooks/useSystemSignaturesData';
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;
@@ -116,14 +116,14 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
);
const handleSelect = useCallback(
(signature: SystemSignature) => {
async (signature: SystemSignature) => {
if (!signature) {
return;
}
const { outCommand } = ref.current;
outCommand({
await outCommand({
type: OutCommand.linkSignatureToSystem,
data: {
...data,
@@ -131,16 +131,34 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
},
});
if (parseSignatureCustomInfo(signature.custom_info).isEOL === true) {
await outCommand({
type: OutCommand.updateConnectionTimeStatus,
data: {
source: data.solar_system_source,
target: data.solar_system_target,
value: TimeStatus.eol,
},
});
}
const whShipSize = getWhSize(wormholes, signature.type);
if (whShipSize !== undefined && whShipSize !== null) {
await outCommand({
type: OutCommand.updateConnectionShipSizeType,
data: {
source: data.solar_system_source,
target: data.solar_system_target,
value: whShipSize,
},
});
}
setVisible(false);
},
[data, setVisible],
[data, setVisible, wormholes],
);
const { signatures } = useSystemSignaturesData({
systemId: `${data.solar_system_source}`,
settings: LINK_SIGNTATURE_SETTINGS,
});
useEffect(() => {
if (!targetSystemDynamicInfo) {
handleHide();
@@ -158,12 +176,10 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
>
<SystemSignaturesContent
systemId={`${data.solar_system_source}`}
signatures={signatures}
hasUnsupportedLanguage={false}
settings={LINK_SIGNTATURE_SETTINGS}
hideLinkedSignatures
selectable
settings={LINK_SIGNTATURE_SETTINGS}
onSelect={handleSelect}
selectable={true}
filterSignature={filterSignature}
/>
</Dialog>

View File

@@ -1,11 +1,12 @@
import { SystemView, WdButton } from '@/hooks/Mapper/components/ui-kit';
import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import { SystemView } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
import { Dialog } from 'primereact/dialog';
import { InputTextarea } from 'primereact/inputtextarea';
import { useCallback, useRef, useState } from 'react';
const PING_TITLES = {
[PingType.Rally]: 'RALLY',
@@ -62,7 +63,7 @@ export const SystemPingDialog = ({ systemId, type, visible, setVisible }: System
</div>
}
visible={visible}
draggable={true}
draggable={false}
style={{ width: '450px' }}
onShow={onShow}
onHide={() => {
@@ -91,7 +92,7 @@ export const SystemPingDialog = ({ systemId, type, visible, setVisible }: System
</div>
<div className="flex gap-2 justify-end">
<WdButton type="submit" onClick={handleSave} size="small" severity="danger" label="Ping!" />
<Button onClick={handleSave} size="small" severity="danger" label="Ping!"></Button>
</div>
</div>
</form>

View File

@@ -1,15 +1,16 @@
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
import { OutCommand } from '@/hooks/Mapper/types';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { InputText } from 'primereact/inputtext';
import { useCallback, useEffect, useRef, useState } from 'react';
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { IconField } from 'primereact/iconfield';
import { TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
interface SystemSettingsDialog {
systemId: string;
@@ -113,7 +114,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
<Dialog
header="System settings"
visible={visible}
draggable={true}
draggable={false}
style={{ width: '450px' }}
onShow={onShow}
onHide={() => {
@@ -125,7 +126,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
}}
>
<form onSubmit={handleSave}>
<div className="flex flex-col gap-3 px-2">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
<label htmlFor="username">Custom name</label>
@@ -214,14 +215,18 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
<div className="flex flex-col gap-1">
<label htmlFor="username">Description</label>
<div className="h-[200px]">
<MarkdownEditor value={description} onChange={e => setDescription(e)} height="180px" />
</div>
<InputTextarea
autoResize
rows={5}
cols={30}
value={description}
onChange={e => setDescription(e.target.value)}
/>
</div>
</div>
<div className="flex gap-2 justify-end">
<WdButton onClick={handleSave} outlined size="small" label="Save" type="submit" />
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
</div>
</div>
</form>

View File

@@ -1,9 +1,11 @@
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import {
RoutesType,
useRouteProvider,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
interface RoutesSettingsDialog {
visible: boolean;
@@ -81,7 +83,7 @@ export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDial
</div>
<div className="flex gap-2 justify-end">
<WdButton onClick={handleSave} outlined size="small" label="Apply"></WdButton>
<Button onClick={handleSave} outlined size="small" label="Apply"></Button>
</div>
</div>
</Dialog>

View File

@@ -3,7 +3,6 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { flattenValues } from '@/hooks/Mapper/utils/flattenValues.ts';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
@@ -65,8 +64,12 @@ export const useLoadRoutes = ({
systems?.length,
connections,
hubs,
// we need make it flat recursively
...flattenValues(routesSettings),
routesSettings,
...Object.keys(routesSettings)
.sort()
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
.map(x => routesSettings[x]),
...deps,
]);

View File

@@ -1,12 +1,12 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemInfoContent } from './SystemInfoContent';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useState } from 'react';
import { SystemInfoContent } from './SystemInfoContent';
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
export const SystemInfo = () => {
const [visible, setVisible] = useState(false);
@@ -48,7 +48,7 @@ export const SystemInfo = () => {
</div>
<LayoutEventBlocker className="flex gap-1 items-center">
<a href={`https://zkillboard.com/system/${systemId}/`} rel="noreferrer" target="_blank">
<a href={`https://zkillboard.com/system/${systemId}`} rel="noreferrer" target="_blank">
<img src={ZKB_ICON} width="14" height="14" className="external-icon" />
</a>
<a href={`http://anoik.is/systems/${solarSystemName}`} rel="noreferrer" target="_blank">

View File

@@ -2,7 +2,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { useMemo } from 'react';
import { getSystemById, sortWHClasses } from '@/hooks/Mapper/helpers';
import { InfoDrawer, MarkdownTextViewer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { InfoDrawer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
interface SystemInfoContentProps {
@@ -51,7 +51,7 @@ export const SystemInfoContent = ({ systemId }: SystemInfoContentProps) => {
</div>
}
>
<MarkdownTextViewer>{description}</MarkdownTextViewer>
<div className="break-words">{description}</div>
</InfoDrawer>
)}
</div>

View File

@@ -1,5 +1,6 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useState } from 'react';
import { Button } from 'primereact/button';
import { TabPanel, TabView } from 'primereact/tabview';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { Dropdown } from 'primereact/dropdown';
@@ -9,7 +10,6 @@ import {
SIGNATURE_SETTINGS,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
interface SystemSignatureSettingsDialogProps {
settings: SignatureSettingsType;
@@ -92,7 +92,7 @@ export const SystemSignatureSettingsDialog = ({
</div>
<div className="flex gap-2 justify-end">
<WdButton onClick={handleSave} outlined size="small" label="Save" />
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
</div>
</div>
</Dialog>

View File

@@ -1,16 +1,120 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useMemo, useState } from 'react';
import { useSignatureUndo } from './hooks/useSignatureUndo';
import { useSystemSignaturesData } from './hooks/useSystemSignaturesData';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
/**
* Custom hook for managing pending signature deletions and undo countdown.
*/
function useSignatureUndo(
systemId: string | undefined,
settings: SignatureSettingsType,
outCommand: OutCommandHandler,
) {
const [countdown, setCountdown] = useState<number>(0);
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
const [deletedSignatures, setDeletedSignatures] = useState<ExtendedSystemSignature[]>([]);
const intervalRef = useRef<number | null>(null);
const addDeleted = useCallback((signatures: ExtendedSystemSignature[]) => {
const newIds = signatures.map(sig => sig.eve_id);
setPendingIds(prev => {
const next = new Set(prev);
newIds.forEach(id => next.add(id));
return next;
});
setDeletedSignatures(prev => [...prev, ...signatures]);
}, []);
// Clear deleted signatures when system changes
useEffect(() => {
if (systemId) {
setDeletedSignatures([]);
setPendingIds(new Set());
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, [systemId]);
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
// clear any existing timer
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (pendingIds.size === 0) {
setCountdown(0);
setDeletedSignatures([]);
return;
}
// determine timeout from settings
const timeoutMs = getDeletionTimeoutMs(settings);
setCountdown(Math.ceil(timeoutMs / 1000));
// start new interval
intervalRef.current = window.setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
setPendingIds(new Set());
setDeletedSignatures([]);
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.size === 0) return;
await outCommand({
type: OutCommand.undoDeleteSignatures,
data: { system_id: systemId, eve_ids: Array.from(pendingIds) },
});
setPendingIds(new Set());
setDeletedSignatures([]);
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [systemId, pendingIds, outCommand]);
return {
pendingIds,
countdown,
deletedSignatures,
addDeleted,
handleUndo,
};
}
export const SystemSignatures = () => {
const [showSettings, setShowSettings] = useState(false);
const [visible, setVisible] = useState(false);
const [sigCount, setSigCount] = useState(0);
const {
data: { selectedSystems },
@@ -20,6 +124,31 @@ export const SystemSignatures = () => {
const [systemId] = selectedSystems;
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
systemId,
settingsSignatures,
outCommand,
);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (pendingIds.size > 0 && countdown > 0) {
event.preventDefault();
event.stopPropagation();
handleUndo();
}
});
const handleCountChange = useCallback((count: number) => {
setSigCount(count);
}, []);
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
settingsSignaturesUpdate(newSettings);
setVisible(false);
},
[settingsSignaturesUpdate],
);
const handleLazyDeleteToggle = useCallback(
(value: boolean) => {
@@ -31,42 +160,7 @@ export const SystemSignatures = () => {
[settingsSignaturesUpdate],
);
const {
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
hasUnsupportedLanguage,
} = useSystemSignaturesData({
systemId,
settings: settingsSignatures,
onLazyDeleteChange: handleLazyDeleteToggle,
});
const sigCount = useMemo(() => signatures.length, [signatures]);
const deletedSignatures = useMemo(() => signatures.filter(s => s.deleted), [signatures]);
const { countdown, handleUndo } = useSignatureUndo(systemId, settingsSignatures, deletedSignatures, outCommand);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (deletedSignatures.length > 0 && countdown > 0) {
event.preventDefault();
event.stopPropagation();
handleUndo();
}
});
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
settingsSignaturesUpdate(newSettings);
setShowSettings(false);
},
[settingsSignaturesUpdate],
);
const openSettings = useCallback(() => setShowSettings(true), []);
const openSettings = useCallback(() => setVisible(true), []);
return (
<Widget
@@ -74,7 +168,7 @@ export const SystemSignatures = () => {
<SystemSignaturesHeader
sigCount={sigCount}
lazyDeleteValue={settingsSignatures[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
pendingCount={deletedSignatures.length}
pendingCount={pendingIds.size}
undoCountdown={countdown}
onLazyDeleteChange={handleLazyDeleteToggle}
onUndoClick={handleUndo}
@@ -90,21 +184,18 @@ export const SystemSignatures = () => {
) : (
<SystemSignaturesContent
systemId={systemId}
signatures={signatures}
selectedSignatures={selectedSignatures}
onSelectSignatures={setSelectedSignatures}
onDeleteSelected={handleDeleteSelected}
onSelectAll={handleSelectAll}
onPaste={handlePaste}
hasUnsupportedLanguage={hasUnsupportedLanguage}
settings={settingsSignatures}
deletedSignatures={deletedSignatures}
onLazyDeleteChange={handleLazyDeleteToggle}
onCountChange={handleCountChange}
onSignatureDeleted={addDeleted}
/>
)}
{showSettings && (
{visible && (
<SystemSignatureSettingsDialog
settings={settingsSignatures}
onCancel={() => setShowSettings(false)}
onCancel={() => setVisible(false)}
onSave={handleSettingsSave}
/>
)}

View File

@@ -28,44 +28,39 @@ import {
renderInfoColumn,
renderUpdatedTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getSignatureRowClass } from '../helpers/rowStyles';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
interface SystemSignaturesContentProps {
systemId: string;
signatures: ExtendedSystemSignature[];
selectedSignatures?: ExtendedSystemSignature[];
onSelectSignatures?: (s: ExtendedSystemSignature[]) => void;
onDeleteSelected?: () => Promise<void>;
onSelectAll?: () => void;
onPaste?: (clipboardString: string) => void;
settings: SignatureSettingsType;
hideLinkedSignatures?: boolean;
hasUnsupportedLanguage?: boolean;
selectable?: boolean;
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void;
filterSignature?: (signature: SystemSignature) => boolean;
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
deletedSignatures?: ExtendedSystemSignature[];
}
export const SystemSignaturesContent = ({
systemId,
signatures,
selectedSignatures,
onSelectSignatures,
onDeleteSelected,
onSelectAll,
onPaste,
settings,
hideLinkedSignatures,
hasUnsupportedLanguage,
selectable,
onSelect,
onLazyDeleteChange,
onCountChange,
filterSignature,
onSignatureDeleted,
deletedSignatures = [],
}: SystemSignaturesContentProps) => {
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
@@ -84,18 +79,32 @@ export const SystemSignaturesContent = ({
const { clipboardContent, setClipboardContent } = useClipboard();
const deletedSignatures = useMemo(() => signatures.filter(s => s.deleted), [signatures]);
const {
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
hasUnsupportedLanguage,
} = useSystemSignaturesData({
systemId,
settings,
onCountChange,
onLazyDeleteChange,
onSignatureDeleted,
});
useEffect(() => {
if (selectable) return;
if (!clipboardContent?.text) return;
onPaste?.(clipboardContent.text);
handlePaste(clipboardContent.text);
setClipboardContent(null);
}, [selectable, clipboardContent, onPaste, setClipboardContent]);
}, [selectable, clipboardContent, handlePaste, setClipboardContent]);
useHotkey(true, ['a'], () => onSelectAll?.());
useHotkey(true, ['a'], handleSelectAll);
useHotkey(false, ['Backspace', 'Delete'], (event: KeyboardEvent) => {
const targetWindow = (event.target as HTMLHtmlElement)?.closest(`[data-window-id="${SIGNATURE_WINDOW_ID}"]`);
@@ -108,7 +117,7 @@ export const SystemSignaturesContent = ({
event.stopPropagation();
// Delete key should always immediately delete, never show pending deletions
onDeleteSelected?.();
handleDeleteSelected();
});
const handleResize = useCallback(() => {
@@ -143,23 +152,14 @@ export const SystemSignaturesContent = ({
selectable
? onSelect?.(selectableSignatures[0])
: onSelectSignatures?.(selectableSignatures as ExtendedSystemSignature[]);
: setSelectedSignatures(selectableSignatures as ExtendedSystemSignature[]);
},
[onSelect, selectable, onSelectSignatures, deletedSignatures],
[onSelect, selectable, setSelectedSignatures, deletedSignatures],
);
const {
showGroupColumn,
showDescriptionColumn,
showAddedColumn,
showUpdatedColumn,
showCharacterColumn,
showCharacterPortrait,
} = useMemo(
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
() => ({
showGroupColumn: settings[SETTINGS_KEYS.SHOW_GROUP_COLUMN] as boolean,
showDescriptionColumn: settings[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN] as boolean,
showAddedColumn: settings[SETTINGS_KEYS.SHOW_ADDED_COLUMN] as boolean,
showUpdatedColumn: settings[SETTINGS_KEYS.SHOW_UPDATED_COLUMN] as boolean,
showCharacterColumn: settings[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN] as boolean,
showCharacterPortrait: settings[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT] as boolean,
@@ -168,6 +168,9 @@ export const SystemSignaturesContent = ({
);
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
// Get the set of deleted signature IDs for quick lookup
const deletedIds = new Set(deletedSignatures.map(sig => sig.eve_id));
// Common filter function
const shouldShowSignature = (sig: ExtendedSystemSignature): boolean => {
if (filterSignature && !filterSignature(sig)) {
@@ -201,8 +204,24 @@ export const SystemSignaturesContent = ({
return settings[sig.kind] as boolean;
};
return signatures.filter(sig => shouldShowSignature(sig));
}, [signatures, hideLinkedSignatures, settings, filterSignature]);
// Filter active signatures, excluding any that are in the deleted list
const activeSignatures = signatures.filter(sig => {
// Skip if this signature is in the deleted list
if (deletedIds.has(sig.eve_id)) {
return false;
}
return shouldShowSignature(sig);
});
// Add deleted signatures with pending deletion flag, applying the same filters
const deletedWithPendingFlag = deletedSignatures.filter(shouldShowSignature).map(sig => ({
...sig,
pendingDeletion: true,
}));
return [...activeSignatures, ...deletedWithPendingFlag];
}, [signatures, hideLinkedSignatures, settings, filterSignature, deletedSignatures]);
const onRowMouseEnter = useCallback((e: DataTableRowMouseEvent) => {
setHoveredSignature(e.data as SystemSignature);
@@ -225,18 +244,20 @@ export const SystemSignaturesContent = ({
return getSignatureRowClass(
rowData as ExtendedSystemSignature,
refVars.current.selectedSignatures || [],
refVars.current.selectedSignatures,
refVars.current.settings[SETTINGS_KEYS.COLOR_BY_TYPE] as boolean,
);
}, []);
const handleSortSettings = useCallback((e: DataTableStateEvent) => {
refVars.current.settingsSignaturesUpdate({
...refVars.current.settingsSignatures,
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
});
}, []);
const handleSortSettings = useCallback(
(e: DataTableStateEvent) =>
refVars.current.settingsSignaturesUpdate({
...refVars.current.settingsSignatures,
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
}),
[],
);
return (
<div ref={tableRef} className="h-full">
@@ -257,7 +278,7 @@ export const SystemSignaturesContent = ({
value={filteredSignatures}
size="small"
selectionMode="multiple"
selection={selectedSignatures || []}
selection={selectedSignatures}
metaKeySelection
onSelectionChange={handleSelectSignatures}
dataKey="eve_id"
@@ -288,17 +309,15 @@ export const SystemSignaturesContent = ({
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
sortable
/>
{showGroupColumn && (
<Column
field="group"
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
body={sig => sig.group ?? ''}
hidden={isCompact}
sortable
/>
)}
<Column
field="group"
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
body={sig => sig.group ?? ''}
hidden={isCompact}
sortable
/>
<Column
field="info"
header="Info"
@@ -306,8 +325,6 @@ export const SystemSignaturesContent = ({
style={{ maxWidth: nameColumnWidth }}
hidden={isCompact || isMedium}
body={renderInfoColumn}
sortable
sortField="name"
/>
{showDescriptionColumn && (
<Column
@@ -319,17 +336,15 @@ export const SystemSignaturesContent = ({
sortable
/>
)}
{showAddedColumn && (
<Column
field="inserted_at"
header="Added"
dataType="date"
body={renderAddedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
)}
<Column
field="inserted_at"
header="Added"
dataType="date"
body={renderAddedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
{showUpdatedColumn && (
<Column
field="updated_at"

View File

@@ -1,4 +1,3 @@
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import {
GroupType,
SignatureGroup,
@@ -12,6 +11,7 @@ import {
SignatureKindFR,
SignatureKindRU,
} from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
export const TIME_ONE_MINUTE = 1000 * 60;
export const TIME_TEN_MINUTES = TIME_ONE_MINUTE * 10;
@@ -130,8 +130,6 @@ export const SIGNATURE_SETTINGS = {
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.COMBAT_SITE, name: 'Show Combat Sites' },
],
uiFlags: [
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_GROUP_COLUMN, name: 'Show Group Column' },
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_ADDED_COLUMN, name: 'Show Added Column' },
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_UPDATED_COLUMN, name: 'Show Updated Column' },
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN, name: 'Show Description Column' },
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_CHARACTER_COLUMN, name: 'Show Character Column' },

View File

@@ -1,5 +1,5 @@
import { ExtendedSystemSignature, SignatureGroup } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { ExtendedSystemSignature, SignatureGroup } from '@/hooks/Mapper/types';
import { getRowBackgroundColor } from './getRowBackgroundColor';
import classes from './rowStyles.module.scss';
@@ -20,7 +20,7 @@ export function getSignatureRowClass(
return clsx([...baseCls, 'bg-violet-400/40 hover:bg-violet-300/40']);
}
if (row.deleted) {
if (row.pendingDeletion) {
return clsx([...baseCls, 'bg-red-400/40 hover:bg-red-400/50']);
}

View File

@@ -1,20 +1,24 @@
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
export interface UseSystemSignaturesDataProps {
systemId: string;
settings: SignatureSettingsType;
hideLinkedSignatures?: boolean;
onCountChange?: (count: number) => void;
onPendingChange?: (
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
undo: () => void,
) => void;
onLazyDeleteChange?: (value: boolean) => void;
deletionTiming?: number;
}
export interface UseFetchingParams {
systemId: string;
settings: SignatureSettingsType;
signaturesRef: React.MutableRefObject<ExtendedSystemSignature[]>;
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
pendingDeletionMapRef: React.MutableRefObject<Record<string, ExtendedSystemSignature>>;
}
export interface UsePendingDeletionParams {

View File

@@ -0,0 +1,42 @@
import { useCallback, useRef } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { prepareUpdatePayload } from '../helpers';
import { UsePendingDeletionParams } from './types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
export function usePendingDeletions({
systemId,
setSignatures,
onPendingChange,
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) {
const { outCommand } = useMapRootState();
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
const processRemovedSignatures = useCallback(
async (
removed: ExtendedSystemSignature[],
added: ExtendedSystemSignature[],
updated: ExtendedSystemSignature[],
) => {
if (!removed.length) return;
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
},
[systemId, outCommand],
);
const clearPendingDeletions = useCallback(() => {
pendingDeletionMapRef.current = {};
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
}, []);
return {
pendingDeletionMapRef,
processRemovedSignatures,
clearPendingDeletions,
};
}

View File

@@ -1,27 +1,21 @@
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback } from 'react';
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useMemo } from 'react';
import { getDeletionTimeoutMs } from '../constants';
import { getActualSigs, prepareUpdatePayload } from '../helpers';
import { prepareUpdatePayload, getActualSigs, mergeLocalPending } from '../helpers';
import { UseFetchingParams } from './types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const useSignatureFetching = ({ systemId, settings, signaturesRef, setSignatures }: UseFetchingParams) => {
export const useSignatureFetching = ({
systemId,
signaturesRef,
setSignatures,
pendingDeletionMapRef,
}: UseFetchingParams) => {
const {
data: { characters },
outCommand,
} = useMapRootState();
const deleteTimeout = useMemo(() => {
const lazyDelete = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean;
if (!lazyDelete) {
return 0;
}
return getDeletionTimeoutMs(settings);
}, [settings]);
const handleGetSignatures = useCallback(async () => {
if (!systemId) {
setSignatures([]);
@@ -38,23 +32,24 @@ export const useSignatureFetching = ({ systemId, settings, signaturesRef, setSig
character_name: characters.find(c => c.eve_id === s.character_eve_id)?.name,
})) as ExtendedSystemSignature[];
setSignatures(() => extended);
setSignatures(() => mergeLocalPending(pendingDeletionMapRef, extended));
}, [characters, systemId, outCommand]);
const handleUpdateSignatures = useCallback(
async (newList: ExtendedSystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => {
const actualSigs = getActualSigs(signaturesRef.current, newList, updateOnly, skipUpdateUntouched);
const { added, updated, removed } = getActualSigs(
signaturesRef.current,
newList,
updateOnly,
skipUpdateUntouched,
);
const { added, updated, removed } = actualSigs;
if (updated.length !== 0 || added.length !== 0 || removed.length !== 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: { ...prepareUpdatePayload(systemId, added, updated, removed), deleteTimeout },
});
}
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
},
[systemId, deleteTimeout, outCommand, signaturesRef],
[systemId, outCommand, signaturesRef],
);
return {

View File

@@ -1,89 +0,0 @@
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import { ExtendedSystemSignature, OutCommandHandler } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useRef, useState } from 'react';
import { getDeletionTimeoutMs } from '../constants';
/**
* Custom hook for managing pending signature deletions and undo countdown.
*/
export function useSignatureUndo(
systemId: string | undefined,
settings: SignatureSettingsType,
deletedSignatures: ExtendedSystemSignature[],
outCommand: OutCommandHandler,
) {
const [countdown, setCountdown] = useState<number>(0);
const intervalRef = useRef<number | null>(null);
// Clear deleted signatures when system changes
useEffect(() => {
if (systemId) {
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, [systemId]);
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
// clear any existing timer
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (deletedSignatures.length === 0) {
setCountdown(0);
return;
}
// determine timeout from settings
const timeoutMs = getDeletionTimeoutMs(settings);
// Ensure a minimum of 1 second for immediate deletion so the UI shows
const effectiveTimeoutMs = timeoutMs === 0 ? 1000 : timeoutMs;
setCountdown(Math.ceil(effectiveTimeoutMs / 1000));
// start new interval
intervalRef.current = window.setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
return 0;
}
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [deletedSignatures, settings]);
// undo handler
const handleUndo = useCallback(async () => {
if (!systemId || deletedSignatures.length === 0) return;
await outCommand({
type: OutCommand.undoDeleteSignatures,
data: { system_id: systemId, eve_ids: deletedSignatures.map(s => s.eve_id) },
});
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [systemId, deletedSignatures, outCommand]);
return {
countdown,
handleUndo,
};
}

View File

@@ -1,29 +1,44 @@
import { useMapEventListener } from '@/hooks/Mapper/events';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { Commands, ExtendedSystemSignature, SignatureKind } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useState } from 'react';
import useRefState from 'react-usestateref';
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures.ts';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getActualSigs } from '../helpers';
import { UseSystemSignaturesDataProps } from './types';
import { usePendingDeletions } from './usePendingDeletions';
import { useSignatureFetching } from './useSignatureFetching';
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures.ts';
export const useSystemSignaturesData = ({
systemId,
settings,
onCountChange,
onPendingChange,
onLazyDeleteChange,
onSignatureDeleted,
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & {
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
}) => {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
const [hasUnsupportedLanguage, setHasUnsupportedLanguage] = useState<boolean>(false);
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
systemId,
setSignatures,
onPendingChange,
});
const { handleGetSignatures, handleUpdateSignatures } = useSignatureFetching({
systemId,
settings,
signaturesRef,
setSignatures,
pendingDeletionMapRef,
});
const handlePaste = useCallback(
@@ -52,14 +67,45 @@ export const useSystemSignaturesData = ({
setHasUnsupportedLanguage(false);
}
await handleUpdateSignatures(incomingSignatures, !lazyDeleteValue, false);
const currentNonPending = lazyDeleteValue
? signaturesRef.current.filter(sig => !sig.pendingDeletion)
: signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition);
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, false);
if (removed.length > 0) {
await processRemovedSignatures(removed, added, updated);
// Only show pending deletions if:
// 1. Lazy deletion is enabled AND
// 2. Deletion timing is not immediate (> 0)
if (onSignatureDeleted && lazyDeleteValue) {
const timeoutMs = getDeletionTimeoutMs(settings);
if (timeoutMs > 0) {
onSignatureDeleted(removed);
}
}
}
if (updated.length !== 0 || added.length !== 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: {
system_id: systemId,
added,
updated,
removed: [],
},
});
}
const keepLazy = settings[SETTINGS_KEYS.KEEP_LAZY_DELETE] as boolean;
if (lazyDeleteValue && !keepLazy) {
onLazyDeleteChange?.(false);
}
},
[settings, handleUpdateSignatures, onLazyDeleteChange],
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted],
);
const handleDeleteSelected = useCallback(async () => {
@@ -68,15 +114,23 @@ export const useSystemSignaturesData = ({
const selectedIds = selectedSignatures.map(s => s.eve_id);
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
setSelectedSignatures([]);
// IMPORTANT: Send deletion to server BEFORE updating local state
// Otherwise signaturesRef.current will be updated and getActualSigs won't detect removals
await handleUpdateSignatures(finalList, false, true);
}, [handleUpdateSignatures, selectedSignatures, signatures]);
// Update local state after server call
setSignatures(finalList);
setSelectedSignatures([]);
}, [handleUpdateSignatures, selectedSignatures, signatures, setSignatures]);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
}, [signatures]);
const undoPending = useCallback(() => {
clearPendingDeletions();
}, [clearPendingDeletions]);
useMapEventListener(event => {
if (event.name === Commands.signaturesUpdated && String(event.data) === String(systemId)) {
handleGetSignatures();
@@ -87,13 +141,18 @@ export const useSystemSignaturesData = ({
useEffect(() => {
if (!systemId) {
setSignatures([]);
undoPending();
return;
}
handleGetSignatures();
}, [systemId]);
useEffect(() => {
onCountChange?.(signatures.length);
}, [signatures]);
return {
signatures,
signatures: signatures.filter(sig => !sig.deleted),
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,

View File

@@ -1,14 +1,14 @@
import { SystemViewStandalone, TooltipPosition, WHClassView } from '@/hooks/Mapper/components/ui-kit';
import { SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { PrimeIcons } from 'primereact/api';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { SystemViewStandalone, TooltipPosition, WHClassView } from '@/hooks/Mapper/components/ui-kit';
import { renderK162Type } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
import clsx from 'clsx';
import { renderName } from './renderName.tsx';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
export const renderInfoColumn = (row: SystemSignature) => {
if (!row.group || row.group === SignatureGroup.Wormhole) {
@@ -18,9 +18,7 @@ export const renderInfoColumn = (row: SystemSignature) => {
return (
<div className="flex justify-start items-center gap-[4px]">
{row.temporary_name && <span className={clsx('text-[12px]')}>{row.temporary_name}</span>}
{customInfo.time_status === TimeStatus._1h && (
{customInfo.isEOL && (
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as EOL">
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
</WdTooltipWrapper>

View File

@@ -30,14 +30,10 @@ export const SystemStructures: React.FC = () => {
const processClipboard = useCallback(
(text: string) => {
if (!systemId) {
console.warn('Cannot update structures: no system selected');
return;
}
const updated = processSnippetText(text, structures);
handleUpdateStructures(updated);
},
[systemId, structures, handleUpdateStructures],
[structures, handleUpdateStructures],
);
const handlePaste = useCallback(

View File

@@ -1,13 +1,13 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { AutoComplete } from 'primereact/autocomplete';
import { Calendar } from 'primereact/calendar';
import clsx from 'clsx';
import { formatToISO, statusesRequiringTimer, StructureItem, StructureStatus } from '../helpers';
import { StructureItem, StructureStatus, statusesRequiringTimer, formatToISO } from '../helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
interface StructuresEditDialogProps {
visible: boolean;
@@ -30,6 +30,9 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
const { outCommand } = useMapRootState();
const [prevQuery, setPrevQuery] = useState('');
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
useEffect(() => {
if (structure) {
setEditData(structure);
@@ -43,24 +46,35 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// Searching corporation owners via auto-complete
const searchOwners = useCallback(
async (e: { query: string }) => {
const query = e.query.trim();
if (!query) {
const newQuery = e.query.trim();
if (!newQuery) {
setOwnerSuggestions([]);
return;
}
// If user typed more text but we have partial match in prevResults
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
const filtered = prevResults.filter(item =>
item.label.toLowerCase().includes(newQuery.toLowerCase()),
);
setOwnerSuggestions(filtered);
return;
}
try {
const { results = [] } = await outCommand({
type: OutCommand.getCorporationNames,
data: { search: query },
data: { search: newQuery },
});
setOwnerSuggestions(results);
setPrevQuery(newQuery);
setPrevResults(results);
} catch (err) {
console.error('Failed to fetch owners:', err);
setOwnerSuggestions([]);
}
},
[outCommand],
[prevQuery, prevResults, outCommand],
);
const handleChange = (field: keyof StructureItem, val: string | Date) => {
@@ -82,7 +96,9 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// when user picks a corp from auto-complete
const handleSelectOwner = (selected: { label: string; value: string }) => {
setOwnerInput(selected.label);
setEditData(prev => (prev ? { ...prev, ownerName: selected.label, ownerId: selected.value } : null));
setEditData(prev =>
prev ? { ...prev, ownerName: selected.label, ownerId: selected.value } : null,
);
};
const handleStatusChange = (val: string) => {
@@ -141,7 +157,11 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
<div className="flex flex-col gap-2 text-[14px]">
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Type:</span>
<input readOnly className="p-inputtext p-component cursor-not-allowed" value={editData.structureType ?? ''} />
<input
readOnly
className="p-inputtext p-component cursor-not-allowed"
value={editData.structureType ?? ''}
/>
</label>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Name:</span>
@@ -184,12 +204,10 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
{statusesRequiringTimer.includes(editData.status) && (
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>
Timer <br /> (Eve Time):
</span>
<span>Timer <br /> (Eve Time):</span>
<Calendar
value={editData.endTime ? new Date(editData.endTime) : undefined}
onChange={e => handleChange('endTime', e.value ?? '')}
onChange={(e) => handleChange('endTime', e.value ?? '')}
showTime
hourFormat="24"
dateFormat="yy-mm-dd"
@@ -209,8 +227,8 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
</div>
<div className="flex justify-end items-center gap-2 mt-4">
<WdButton label="Delete" severity="danger" className="p-button-sm" onClick={handleDeleteClick} />
<WdButton label="Save" className="p-button-sm" onClick={handleSaveClick} />
<Button label="Delete" severity="danger" className="p-button-sm" onClick={handleDeleteClick} />
<Button label="Save" className="p-button-sm" onClick={handleSaveClick} />
</div>
</Dialog>
);

View File

@@ -56,11 +56,6 @@ export function useSystemStructures({ systemId, outCommand }: UseSystemStructure
const handleUpdateStructures = useCallback(
async (newList: StructureItem[]) => {
if (!systemId) {
console.warn('Cannot update structures: systemId is undefined');
return;
}
const { added, updated, removed } = getActualStructures(structures, newList);
const sanitizedAdded = added.map(sanitizeIds);

View File

@@ -16,21 +16,8 @@ const SystemKillsContent = () => {
} = useMapRootState();
const [systemId] = selectedSystems || [];
const whCacheRef = useMemo(() => new Map<number, boolean>(), []);
const isWormholeSystem = useCallback(
(systemId: number): boolean => {
const cached = whCacheRef.get(systemId);
if (cached !== undefined) return cached;
const info = getSystemStaticInfo(systemId);
const isWH = info?.system_class != null ? isWormholeSpace(Number(info.system_class)) : false;
whCacheRef.set(systemId, isWH);
return isWH;
},
[whCacheRef],
);
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const { kills, isLoading, error } = useSystemKills({
systemId,
@@ -43,9 +30,15 @@ const SystemKillsContent = () => {
const showLoading = isLoading && kills.length === 0;
const filteredKills = useMemo(() => {
if (!settingsKills.whOnly) return kills;
return kills.filter(kill => isWormholeSystem(Number(kill.solar_system_id)));
}, [kills, settingsKills.whOnly, isWormholeSystem]);
if (!settingsKills.whOnly || !settingsKills.showAll) return kills;
return kills.filter(kill => {
if (!systemStaticInfo) {
console.warn(`System with id ${kill.solar_system_id} not found.`);
return false;
}
return isWormholeSpace(systemStaticInfo.system_class);
});
}, [kills, settingsKills.whOnly, systemStaticInfo, settingsKills.showAll]);
if (!isSubscriptionActive) {
return (

View File

@@ -1,11 +1,13 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { SystemView, TooltipPosition, WdButton, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { Button } from 'primereact/button';
import { WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { PrimeIcons } from 'primereact/api';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface KillsSettingsDialogProps {
@@ -156,7 +158,7 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
</div>
<div className="flex gap-2 justify-end mt-4">
<WdButton onClick={handleApply} label="Apply" outlined size="small" />
<Button onClick={handleApply} label="Apply" outlined size="small" />
</div>
</div>

View File

@@ -1,8 +1,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
import debounce from 'lodash.debounce';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useStableValue } from '@/hooks/Mapper/hooks';
interface UseSystemKillsProps {
systemId?: string;
@@ -30,8 +30,9 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
update,
storedSettings: { settingsKills },
} = useMapRootState();
const { excludedSystems } = settingsKills;
const excludedSystems = useStableValue(settingsKills.excludedSystems);
const effectiveSinceHours = sinceHours;
const effectiveSystemIds = useMemo(() => {
if (showAllVisible) {
@@ -75,13 +76,13 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
eventType = OutCommand.getSystemsKills;
requestData = {
system_ids: effectiveSystemIds,
since_hours: sinceHours,
since_hours: effectiveSinceHours,
};
} else if (systemId) {
eventType = OutCommand.getSystemKills;
requestData = {
system_id: systemId,
since_hours: sinceHours,
since_hours: effectiveSinceHours,
};
} else {
setIsLoading(false);
@@ -109,7 +110,16 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
setIsLoading(false);
}
},
[showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal],
[showAllVisible, systemId, outCommand, effectiveSystemIds, effectiveSinceHours, mergeKillsIntoGlobal],
);
const debouncedFetchKills = useMemo(
() =>
debounce(fetchKills, 500, {
leading: true,
trailing: false,
}),
[fetchKills],
);
const finalKills = useMemo(() => {
@@ -131,22 +141,27 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
useEffect(() => {
if (!systemId && !showAllVisible && !didFallbackFetch.current) {
didFallbackFetch.current = true;
debouncedFetchKills.cancel();
fetchKills(true);
}
}, [systemId, showAllVisible, fetchKills]);
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills]);
useEffect(() => {
if (effectiveSystemIds.length === 0) return;
if (showAllVisible || systemId) {
// Cancel any pending debounced fetch
debouncedFetchKills.cancel();
// Fetch kills immediately
fetchKills();
return;
return () => debouncedFetchKills.cancel();
}
}, [showAllVisible, systemId, effectiveSystemIds, fetchKills]);
}, [showAllVisible, systemId, effectiveSystemIds, debouncedFetchKills, fetchKills]);
const refetch = useCallback(() => {
debouncedFetchKills.cancel();
fetchKills();
}, [fetchKills]);
}, [debouncedFetchKills, fetchKills]);
return {
kills: finalKills,

Some files were not shown because too many files have changed in this diff Show More