private shares and revokation works
This commit is contained in:
@@ -31,6 +31,16 @@ export function Layout({ children }: LayoutProps) {
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(user.id);
|
||||
alert('Your User ID has been copied to clipboard!');
|
||||
}}
|
||||
className="px-2 py-1 bg-blue-700 hover:bg-blue-800 rounded text-xs"
|
||||
title="Copy your User ID for sharing"
|
||||
>
|
||||
Copy My ID
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-3 py-1 bg-blue-700 hover:bg-blue-800 rounded text-sm"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useDrawingStore } from '../../stores/drawingStore';
|
||||
interface MapItemsLayerProps {
|
||||
mapId: string;
|
||||
refreshTrigger: number;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
// Custom marker icons for devices using CSS
|
||||
@@ -58,7 +59,7 @@ const outdoorApIcon = new L.DivIcon({
|
||||
iconAnchor: [20, 40],
|
||||
});
|
||||
|
||||
export function MapItemsLayer({ mapId, refreshTrigger }: MapItemsLayerProps) {
|
||||
export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapItemsLayerProps) {
|
||||
const [items, setItems] = useState<MapItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
@@ -145,10 +146,12 @@ export function MapItemsLayer({ mapId, refreshTrigger }: MapItemsLayerProps) {
|
||||
eventHandlers={{
|
||||
contextmenu: (e) => {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
setContextMenu({
|
||||
item,
|
||||
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
|
||||
});
|
||||
if (!readOnly) {
|
||||
setContextMenu({
|
||||
item,
|
||||
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -230,10 +233,12 @@ export function MapItemsLayer({ mapId, refreshTrigger }: MapItemsLayerProps) {
|
||||
eventHandlers={{
|
||||
contextmenu: (e) => {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
setContextMenu({
|
||||
item,
|
||||
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
|
||||
});
|
||||
if (!readOnly) {
|
||||
setContextMenu({
|
||||
item,
|
||||
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -290,10 +295,12 @@ export function MapItemsLayer({ mapId, refreshTrigger }: MapItemsLayerProps) {
|
||||
eventHandlers={{
|
||||
contextmenu: (e) => {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
setContextMenu({
|
||||
item,
|
||||
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
|
||||
});
|
||||
if (!readOnly) {
|
||||
setContextMenu({
|
||||
item,
|
||||
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMapStore } from '../../stores/mapStore';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { mapService } from '../../services/mapService';
|
||||
|
||||
interface MapListSidebarProps {
|
||||
@@ -9,6 +10,7 @@ interface MapListSidebarProps {
|
||||
|
||||
export function MapListSidebar({ onSelectMap, selectedMapId }: MapListSidebarProps) {
|
||||
const { maps, setMaps, addMap, removeMap, setLoading, setError } = useMapStore();
|
||||
const { user } = useAuthStore();
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newMapName, setNewMapName] = useState('');
|
||||
|
||||
@@ -104,36 +106,50 @@ export function MapListSidebar({ onSelectMap, selectedMapId }: MapListSidebarPro
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{maps.map((map) => (
|
||||
<div
|
||||
key={map.id}
|
||||
className={`p-4 cursor-pointer hover:bg-gray-50 ${
|
||||
selectedMapId === map.id ? 'bg-blue-50 border-l-4 border-blue-600' : ''
|
||||
}`}
|
||||
onClick={() => onSelectMap(map.id)}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-gray-900">{map.name}</h3>
|
||||
{map.description && (
|
||||
<p className="text-sm text-gray-600 mt-1">{map.description}</p>
|
||||
{maps.map((map) => {
|
||||
const isOwner = user && map.owner_id === user.id;
|
||||
const isShared = !isOwner;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={map.id}
|
||||
className={`p-4 cursor-pointer hover:bg-gray-50 ${
|
||||
selectedMapId === map.id ? 'bg-blue-50 border-l-4 border-blue-600' : ''
|
||||
}`}
|
||||
onClick={() => onSelectMap(map.id)}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-gray-900">{map.name}</h3>
|
||||
{isShared && (
|
||||
<span className="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded">
|
||||
Shared
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{map.description && (
|
||||
<p className="text-sm text-gray-600 mt-1">{map.description}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{new Date(map.updated_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
{isOwner && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteMap(map.id);
|
||||
}}
|
||||
className="text-red-600 hover:text-red-800 text-sm ml-2"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{new Date(map.updated_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteMap(map.id);
|
||||
}}
|
||||
className="text-red-600 hover:text-red-800 text-sm ml-2"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,11 @@ export function MapView({ mapId }: MapViewProps) {
|
||||
<>
|
||||
{/* Toolbar for drawing tools */}
|
||||
<div style={{ position: 'fixed', left: '220px', top: '70px', zIndex: 9999 }}>
|
||||
<Toolbar mapId={mapId} onShare={() => setShowShareDialog(true)} />
|
||||
<Toolbar
|
||||
mapId={mapId}
|
||||
onShare={() => setShowShareDialog(true)}
|
||||
readOnly={permission === 'read'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Layer switcher */}
|
||||
@@ -124,11 +128,13 @@ export function MapView({ mapId }: MapViewProps) {
|
||||
maxNativeZoom={layer.maxNativeZoom}
|
||||
/>
|
||||
|
||||
{/* Drawing handler for creating new items */}
|
||||
<DrawingHandler mapId={mapId} onItemCreated={handleItemCreated} />
|
||||
{/* Drawing handler for creating new items - disabled for read-only */}
|
||||
{permission !== 'read' && (
|
||||
<DrawingHandler mapId={mapId} onItemCreated={handleItemCreated} />
|
||||
)}
|
||||
|
||||
{/* Render existing map items */}
|
||||
<MapItemsLayer mapId={mapId} refreshTrigger={refreshTrigger} />
|
||||
<MapItemsLayer mapId={mapId} refreshTrigger={refreshTrigger} readOnly={permission === 'read'} />
|
||||
</MapContainer>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export function ShareDialog({ mapId, onClose }: ShareDialogProps) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await mapShareService.shareWithUser(mapId, {
|
||||
user_id: newUserId,
|
||||
user_identifier: newUserId.trim(),
|
||||
permission: newUserPermission,
|
||||
});
|
||||
setNewUserId('');
|
||||
@@ -57,6 +57,7 @@ export function ShareDialog({ mapId, onClose }: ShareDialogProps) {
|
||||
alert('Map shared successfully!');
|
||||
} catch (error: any) {
|
||||
console.error('Share error:', error);
|
||||
// Show detailed error message
|
||||
const message = error.response?.data?.detail || error.message || 'Failed to share map';
|
||||
alert(message);
|
||||
} finally {
|
||||
@@ -158,15 +159,18 @@ export function ShareDialog({ mapId, onClose }: ShareDialogProps) {
|
||||
{/* Add user form */}
|
||||
<form onSubmit={handleShareWithUser} className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Share with User (by User ID)
|
||||
Share with User
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
Enter a username, email, or user ID
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newUserId}
|
||||
onChange={(e) => setNewUserId(e.target.value)}
|
||||
placeholder="Enter user ID"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md"
|
||||
placeholder="Enter username, email, or user ID"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm"
|
||||
disabled={loading}
|
||||
/>
|
||||
<select
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CABLE_COLORS, CABLE_LABELS } from '../../types/mapItem';
|
||||
interface ToolbarProps {
|
||||
mapId: string;
|
||||
onShare: () => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
interface ToolButton {
|
||||
@@ -70,11 +71,18 @@ const TOOLS: ToolButton[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export function Toolbar({ mapId, onShare }: ToolbarProps) {
|
||||
export function Toolbar({ mapId, onShare, readOnly = false }: ToolbarProps) {
|
||||
const { activeTool, setActiveTool } = useDrawingStore();
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow-lg rounded-lg p-2 space-y-1" style={{ minWidth: '150px' }}>
|
||||
{/* Read-only indicator */}
|
||||
{readOnly && (
|
||||
<div className="w-full px-3 py-2 rounded bg-yellow-100 text-yellow-800 text-xs font-medium mb-2 text-center">
|
||||
Read-Only Mode
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Share button */}
|
||||
<button
|
||||
onClick={onShare}
|
||||
@@ -87,26 +95,32 @@ export function Toolbar({ mapId, onShare }: ToolbarProps) {
|
||||
|
||||
<div className="border-t border-gray-200 my-2"></div>
|
||||
|
||||
{TOOLS.map((tool) => (
|
||||
<button
|
||||
key={tool.id}
|
||||
onClick={() => setActiveTool(tool.id)}
|
||||
className={`w-full px-3 py-2 rounded text-left flex items-center gap-2 transition-colors ${
|
||||
activeTool === tool.id
|
||||
? 'bg-blue-100 text-blue-700 font-medium'
|
||||
: 'hover:bg-gray-100 text-gray-700'
|
||||
}`}
|
||||
title={tool.description}
|
||||
>
|
||||
<span
|
||||
className="text-lg"
|
||||
style={tool.color ? { color: tool.color } : undefined}
|
||||
{TOOLS.map((tool) => {
|
||||
const isDisabled = readOnly && tool.id !== 'select';
|
||||
return (
|
||||
<button
|
||||
key={tool.id}
|
||||
onClick={() => !isDisabled && setActiveTool(tool.id)}
|
||||
disabled={isDisabled}
|
||||
className={`w-full px-3 py-2 rounded text-left flex items-center gap-2 transition-colors ${
|
||||
isDisabled
|
||||
? 'opacity-50 cursor-not-allowed text-gray-400'
|
||||
: activeTool === tool.id
|
||||
? 'bg-blue-100 text-blue-700 font-medium'
|
||||
: 'hover:bg-gray-100 text-gray-700'
|
||||
}`}
|
||||
title={isDisabled ? 'Not available in read-only mode' : tool.description}
|
||||
>
|
||||
{tool.icon}
|
||||
</span>
|
||||
<span className="text-sm">{tool.label}</span>
|
||||
</button>
|
||||
))}
|
||||
<span
|
||||
className="text-lg"
|
||||
style={tool.color && !isDisabled ? { color: tool.color } : undefined}
|
||||
>
|
||||
{tool.icon}
|
||||
</span>
|
||||
<span className="text-sm">{tool.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user