fix(Map): Copy-Paste restriction: support from FE side

This commit is contained in:
DanSylvest
2025-10-20 12:32:07 +03:00
parent 02b450325e
commit 872f7dcf48
15 changed files with 212 additions and 108 deletions

View File

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

View File

@@ -2,6 +2,10 @@ import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu'; import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api'; import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem'; 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 { export interface ContextMenuSystemMultipleProps {
contextMenuRef: RefObject<ContextMenu>; contextMenuRef: RefObject<ContextMenu>;
@@ -14,20 +18,41 @@ export const ContextMenuSystemMultiple: React.FC<ContextMenuSystemMultipleProps>
onDeleteSystems, onDeleteSystems,
onCopySystems, onCopySystems,
}) => { }) => {
const {
data: { options, userPermissions },
} = useMapRootState();
const items: MenuItem[] = useMemo(() => { const items: MenuItem[] = useMemo(() => {
const allowCopy = checkPermissions(userPermissions, options.allowed_copy_for);
return [ return [
{
label: 'Delete',
icon: clsx(PrimeIcons.TRASH, 'text-red-400'),
command: onDeleteSystems,
},
{ separator: true },
{ {
label: 'Copy', label: 'Copy',
icon: PrimeIcons.COPY, icon: PrimeIcons.COPY,
command: onCopySystems, command: onCopySystems,
}, disabled: !allowCopy,
{ template: () => {
label: 'Delete', return (
icon: PrimeIcons.TRASH, <MenuItemWithInfo
command: onDeleteSystems, 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]); }, [onCopySystems, onDeleteSystems, options, userPermissions]);
return ( return (
<> <>

View File

@@ -3,6 +3,10 @@ import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api'; import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem'; import { MenuItem } from 'primereact/menuitem';
import { PasteSystemsAndConnections } from '@/hooks/Mapper/components/map/components'; 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 { export interface ContextMenuRootProps {
contextMenuRef: RefObject<ContextMenu>; contextMenuRef: RefObject<ContextMenu>;
@@ -17,7 +21,13 @@ export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
onPasteSystemsAnsConnections, onPasteSystemsAnsConnections,
pasteSystemsAndConnections, pasteSystemsAndConnections,
}) => { }) => {
const {
data: { options, userPermissions },
} = useMapState();
const items: MenuItem[] = useMemo(() => { const items: MenuItem[] = useMemo(() => {
const allowPaste = checkPermissions(userPermissions, options.allowed_paste_for);
return [ return [
{ {
label: 'Add System', label: 'Add System',
@@ -27,14 +37,27 @@ export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
...(pasteSystemsAndConnections != null ...(pasteSystemsAndConnections != null
? [ ? [
{ {
label: 'Paste',
icon: 'pi pi-clipboard', icon: 'pi pi-clipboard',
disabled: !allowPaste,
command: onPasteSystemsAnsConnections, command: onPasteSystemsAnsConnections,
template: () => {
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>
);
},
}, },
] ]
: []), : []),
]; ];
}, [onAddSystem, onPasteSystemsAnsConnections, pasteSystemsAndConnections]); }, [userPermissions, options, onAddSystem, pasteSystemsAndConnections, onPasteSystemsAnsConnections]);
return ( return (
<> <>

View File

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

View File

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

View File

@@ -38,6 +38,8 @@ export const useMapInit = () => {
user_characters, user_characters,
present_characters, present_characters,
hubs, hubs,
options,
user_permissions,
}: CommandInit) => { }: CommandInit) => {
const { update } = ref.current; const { update } = ref.current;
@@ -63,6 +65,14 @@ export const useMapInit = () => {
updateData.hubs = hubs; updateData.hubs = hubs;
} }
if (options) {
updateData.options = options;
}
if (options) {
updateData.userPermissions = user_permissions;
}
if (systems) { if (systems) {
updateData.systems = systems; updateData.systems = systems;
} }

View File

@@ -49,87 +49,91 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
const { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated } = const { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated } =
useCommandsCharacters(); useCommandsCharacters();
useImperativeHandle(ref, () => { useImperativeHandle(
return { ref,
command(type, data) { () => {
switch (type) { return {
case Commands.init: command(type, data) {
mapInit(data as CommandInit); switch (type) {
break; case Commands.init:
case Commands.addSystems: mapInit(data as CommandInit);
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100); break;
break; case Commands.addSystems:
case Commands.updateSystems: setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
mapUpdateSystems(data as CommandUpdateSystems); break;
break; case Commands.updateSystems:
case Commands.removeSystems: mapUpdateSystems(data as CommandUpdateSystems);
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100); break;
break; case Commands.removeSystems:
case Commands.addConnections: setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
setTimeout(() => addConnections(data as CommandAddConnections), 100); break;
break; case Commands.addConnections:
case Commands.removeConnections: setTimeout(() => addConnections(data as CommandAddConnections), 100);
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100); break;
break; case Commands.removeConnections:
case Commands.charactersUpdated: setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
charactersUpdated(data as CommandCharactersUpdated); break;
break; case Commands.charactersUpdated:
case Commands.characterAdded: charactersUpdated(data as CommandCharactersUpdated);
characterAdded(data as CommandCharacterAdded); break;
break; case Commands.characterAdded:
case Commands.characterRemoved: characterAdded(data as CommandCharacterAdded);
characterRemoved(data as CommandCharacterRemoved); break;
break; case Commands.characterRemoved:
case Commands.characterUpdated: characterRemoved(data as CommandCharacterRemoved);
characterUpdated(data as CommandCharacterUpdated); break;
break; case Commands.characterUpdated:
case Commands.presentCharacters: characterUpdated(data as CommandCharacterUpdated);
presentCharacters(data as CommandPresentCharacters); break;
break; case Commands.presentCharacters:
case Commands.updateConnection: presentCharacters(data as CommandPresentCharacters);
updateConnection(data as CommandUpdateConnection); break;
break; case Commands.updateConnection:
case Commands.mapUpdated: updateConnection(data as CommandUpdateConnection);
mapUpdated(data as CommandMapUpdated); break;
break; case Commands.mapUpdated:
case Commands.killsUpdated: mapUpdated(data as CommandMapUpdated);
killsUpdated(data as CommandKillsUpdated); break;
break; case Commands.killsUpdated:
killsUpdated(data as CommandKillsUpdated);
break;
case Commands.centerSystem: case Commands.centerSystem:
setTimeout(() => { setTimeout(() => {
const systemId = `${data}`; const systemId = `${data}`;
centerSystem(systemId as CommandSelectSystem); centerSystem(systemId as CommandSelectSystem);
}, 100); }, 100);
break; break;
case Commands.selectSystem: case Commands.selectSystem:
selectSystems({ systems: [data as string], delay: 500 }); selectSystems({ systems: [data as string], delay: 500 });
break; break;
case Commands.selectSystems: case Commands.selectSystems:
selectSystems(data as CommandSelectSystems); selectSystems(data as CommandSelectSystems);
break; break;
case Commands.pingAdded: case Commands.pingAdded:
case Commands.pingCancelled: case Commands.pingCancelled:
case Commands.routes: case Commands.routes:
case Commands.signaturesUpdated: case Commands.signaturesUpdated:
case Commands.linkSignatureToSystem: case Commands.linkSignatureToSystem:
case Commands.detailedKillsUpdated: case Commands.detailedKillsUpdated:
case Commands.characterActivityData: case Commands.characterActivityData:
case Commands.trackingCharactersData: case Commands.trackingCharactersData:
case Commands.updateActivity: case Commands.updateActivity:
case Commands.updateTracking: case Commands.updateTracking:
case Commands.userSettingsUpdated: case Commands.userSettingsUpdated:
// do nothing // do nothing
break; break;
default: default:
console.warn(`Map handlers: Unknown command: ${type}`, data); console.warn(`Map handlers: Unknown command: ${type}`, data);
break; break;
} }
}, },
}; };
}, []); },
[],
);
}; };

View File

@@ -4,8 +4,17 @@ import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrap
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip'; import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
import clsx from 'clsx'; import clsx from 'clsx';
type MenuItemWithInfoProps = { infoTitle: ReactNode; infoClass?: string } & WithChildren; type MenuItemWithInfoProps = {
export const MenuItemWithInfo = ({ children, infoClass, infoTitle }: MenuItemWithInfoProps) => { infoTitle: ReactNode;
infoClass?: string;
tooltipWrapperClassName?: string;
} & WithChildren;
export const MenuItemWithInfo = ({
children,
infoClass,
infoTitle,
tooltipWrapperClassName,
}: MenuItemWithInfoProps) => {
return ( return (
<div className="flex justify-between w-full h-full items-center"> <div className="flex justify-between w-full h-full items-center">
{children} {children}
@@ -13,6 +22,7 @@ export const MenuItemWithInfo = ({ children, infoClass, infoTitle }: MenuItemWit
content={infoTitle} content={infoTitle}
position={TooltipPosition.top} position={TooltipPosition.top}
className="!opacity-100 !pointer-events-auto" className="!opacity-100 !pointer-events-auto"
wrapperClassName={tooltipWrapperClassName}
> >
<div className={clsx('pi text-orange-400', infoClass)} /> <div className={clsx('pi text-orange-400', infoClass)} />
</WdTooltipWrapper> </WdTooltipWrapper>

View File

@@ -1,13 +1,18 @@
import { WithChildren } from '@/hooks/Mapper/types/common.ts'; import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
import clsx from 'clsx'; import clsx from 'clsx';
type WdMenuItemProps = { icon?: string; disabled?: boolean } & WithChildren; type WdMenuItemProps = { icon?: string; disabled?: boolean } & WithChildren & WithClassName;
export const WdMenuItem = ({ children, icon, disabled }: WdMenuItemProps) => { export const WdMenuItem = ({ children, icon, disabled, className }: WdMenuItemProps) => {
return ( return (
<a <a
className={clsx('flex gap-[6px] w-full h-full items-center px-[12px] !py-0 ml-[-2px]', 'p-menuitem-link', { className={clsx(
'p-disabled': disabled, 'flex gap-[6px] w-full h-full items-center px-[12px] !py-0',
})} 'p-menuitem-link',
{
'p-disabled': disabled,
},
className,
)}
> >
{icon && <div className={clsx('min-w-[20px]', icon)}></div>} {icon && <div className={clsx('min-w-[20px]', icon)}></div>}
<div className="w-full">{children}</div> <div className="w-full">{children}</div>

View File

@@ -10,6 +10,7 @@ export type WdTooltipWrapperProps = {
interactive?: boolean; interactive?: boolean;
smallPaddings?: boolean; smallPaddings?: boolean;
tooltipClassName?: string; tooltipClassName?: string;
wrapperClassName?: string;
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> & } & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
Omit<TooltipProps, 'content'>; Omit<TooltipProps, 'content'>;
@@ -26,6 +27,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
smallPaddings, smallPaddings,
size, size,
tooltipClassName, tooltipClassName,
wrapperClassName,
...props ...props
}, },
forwardedRef, forwardedRef,
@@ -36,7 +38,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
return ( return (
<div className={clsx(classes.WdTooltipWrapperRoot, className)} {...props}> <div className={clsx(classes.WdTooltipWrapperRoot, className)} {...props}>
{targetSelector ? <>{children}</> : <div className={autoClass}>{children}</div>} {targetSelector ? <>{children}</> : <div className={clsx(autoClass, wrapperClassName)}>{children}</div>}
<WdTooltip <WdTooltip
ref={forwardedRef} ref={forwardedRef}

View File

@@ -1,11 +1,5 @@
import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts'; import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { UserPermission } from '@/hooks/Mapper/types';
export enum SESSION_KEY {
viewPort = 'viewPort',
windows = 'windows',
windowsVisible = 'windowsVisible',
routes = 'routes',
}
export const SYSTEM_FOCUSED_LIFETIME = 10000; export const SYSTEM_FOCUSED_LIFETIME = 10000;
@@ -161,3 +155,9 @@ export const SPACE_TO_CLASS: Record<string, string> = {
[Spaces.Gallente]: 'Gallente', [Spaces.Gallente]: 'Gallente',
[Spaces.Pochven]: 'Pochven', [Spaces.Pochven]: 'Pochven',
}; };
export const PERMISSIONS_POWER_MAP = {
[UserPermission.ADD_SYSTEM]: [UserPermission.ADD_SYSTEM, UserPermission.MANAGE_MAP, UserPermission.ADMIN_MAP],
[UserPermission.MANAGE_MAP]: [UserPermission.MANAGE_MAP, UserPermission.ADMIN_MAP],
[UserPermission.ADMIN_MAP]: [UserPermission.ADMIN_MAP],
};

View File

@@ -9,3 +9,4 @@ export * from './connectionPassages';
export * from './permissions'; export * from './permissions';
export * from './comment'; export * from './comment';
export * from './ping'; export * from './ping';
export * from './options';

View File

@@ -1,4 +1,4 @@
import { CommentType, PingData, SystemSignature, UserPermissions } from '@/hooks/Mapper/types'; import { CommentType, MapOptions, PingData, SystemSignature, UserPermissions } from '@/hooks/Mapper/types';
import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Mapper/types/character.ts'; import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Mapper/types/character.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts'; import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts'; import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
@@ -94,7 +94,7 @@ export type CommandInit = {
hubs: string[]; hubs: string[];
user_hubs: string[]; user_hubs: string[];
routes: RoutesList; routes: RoutesList;
options: Record<string, string | boolean>; options: MapOptions;
reset?: boolean; reset?: boolean;
is_subscription_active?: boolean; is_subscription_active?: boolean;
main_character_eve_id?: string | null; main_character_eve_id?: string | null;

View File

@@ -4,7 +4,7 @@ import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
import { SolarSystemRawType } from '@/hooks/Mapper/types/system.ts'; import { SolarSystemRawType } from '@/hooks/Mapper/types/system.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts'; import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts'; import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { PingData, UserPermissions } from '@/hooks/Mapper/types'; import { MapOptions, PingData, UserPermissions } from '@/hooks/Mapper/types';
import { SystemSignature } from '@/hooks/Mapper/types/signatures'; import { SystemSignature } from '@/hooks/Mapper/types/signatures';
export type MapUnionTypes = { export type MapUnionTypes = {
@@ -23,7 +23,7 @@ export type MapUnionTypes = {
kills: Record<number, number>; kills: Record<number, number>;
connections: SolarSystemConnection[]; connections: SolarSystemConnection[];
userPermissions: Partial<UserPermissions>; userPermissions: Partial<UserPermissions>;
options: Record<string, string | boolean>; options: MapOptions;
isSubscriptionActive: boolean; isSubscriptionActive: boolean;
mainCharacterEveId: string | null; mainCharacterEveId: string | null;

View File

@@ -0,0 +1,14 @@
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
export type StringBoolean = 'true' | 'false';
export type MapOptions = {
allowed_copy_for: UserPermission;
allowed_paste_for: UserPermission;
layout: string;
restrict_offline_showing: StringBoolean;
show_linked_signature_id: StringBoolean;
show_linked_signature_id_temp_name: StringBoolean;
show_temp_system_name: StringBoolean;
store_custom_labels: StringBoolean;
};