@@ -33,6 +33,8 @@ import {
3333 setMapillaryLabels ,
3434 setEarthdataGisLabels ,
3535 setOpenAerialMapLabels ,
36+ setArcGisHubLabels ,
37+ setOpenDataCatalogLabels ,
3638 setHuggingFaceLabels ,
3739 setSourceCoopLabels ,
3840 setReverseGeocodeLabels ,
@@ -45,6 +47,7 @@ import {
4547 PRECIPITATION_PLUGIN_ID ,
4648 REVERSE_GEOCODE_PLUGIN_ID ,
4749 EFFECTS_PLUGIN_ID ,
50+ openRightPanel ,
4851} from "@geolibre/plugins" ;
4952import { Button , cn , Input } from "@geolibre/ui" ;
5053import {
@@ -82,7 +85,7 @@ import {
8285 ZoomIn ,
8386 ZoomOut ,
8487} from "lucide-react" ;
85- import { useEffect , useMemo , useRef , useState } from "react" ;
88+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
8689import { useTranslation } from "react-i18next" ;
8790import { createAppAPI , getPluginManager , usePluginRegistry } from "../../hooks/usePlugins" ;
8891import { useConsentGatedActions } from "../../hooks/useConsentGatedActions" ;
@@ -172,10 +175,12 @@ interface TopToolbarProps {
172175 // instance — two would not coordinate their in-flight "open recent" aborts.
173176 projectFiles : ProjectFileActions ;
174177 onOpenDiagnostics : ( ) => void ;
178+ onOpenProjectHistory : ( ) => void ;
175179 onToggleThemeMode : ( ) => void ;
176180 // Opens the Offline Basemap Extract panel, mounted in DesktopShell over the
177181 // map so it can stay non-modal (the map is interactive for drawing a bbox).
178182 onOpenBasemapExtract : ( ) => void ;
183+ viewer ?: boolean ;
179184}
180185
181186export function TopToolbar ( {
@@ -189,8 +194,10 @@ export function TopToolbar({
189194 collaboration,
190195 projectFiles,
191196 onOpenDiagnostics,
197+ onOpenProjectHistory,
192198 onToggleThemeMode,
193199 onOpenBasemapExtract,
200+ viewer = false ,
194201} : TopToolbarProps ) {
195202 const { t, i18n } = useTranslation ( ) ;
196203 // The reverse-geocode plugin lives in the framework-agnostic plugins package
@@ -299,6 +306,52 @@ export function TopToolbar({
299306 metaSource : t ( "openAerialMap.metaSource" ) ,
300307 metaRaw : t ( "openAerialMap.metaRaw" ) ,
301308 } ) ;
309+ setArcGisHubLabels ( {
310+ hint : t ( "arcgisHub.hint" ) ,
311+ searchPlaceholder : t ( "arcgisHub.searchPlaceholder" ) ,
312+ search : t ( "arcgisHub.search" ) ,
313+ searchCurrentView : t ( "arcgisHub.searchCurrentView" ) ,
314+ enterKeyword : t ( "arcgisHub.enterKeyword" ) ,
315+ loadMore : t ( "arcgisHub.loadMore" ) ,
316+ searching : t ( "arcgisHub.searching" ) ,
317+ loadingMore : t ( "arcgisHub.loadingMore" ) ,
318+ noResults : t ( "arcgisHub.noResults" ) ,
319+ searchError : t ( "arcgisHub.searchError" ) ,
320+ showing : ( shown , total ) => t ( "arcgisHub.showing" , { shown, total } ) ,
321+ noDescription : t ( "arcgisHub.noDescription" ) ,
322+ add : t ( "arcgisHub.add" ) ,
323+ adding : ( title ) => t ( "arcgisHub.adding" , { title } ) ,
324+ added : ( title ) => t ( "arcgisHub.added" , { title } ) ,
325+ addError : t ( "arcgisHub.addError" ) ,
326+ zoom : t ( "arcgisHub.zoom" ) ,
327+ download : t ( "arcgisHub.download" ) ,
328+ preparing : ( title ) => t ( "arcgisHub.preparing" , { title } ) ,
329+ downloading : ( completed , total , title ) =>
330+ t ( "arcgisHub.downloading" , { completed, total, title } ) ,
331+ downloadStarted : ( title ) => t ( "arcgisHub.downloadStarted" , { title } ) ,
332+ downloadFirstLayer : ( title , layerCount ) =>
333+ t ( "arcgisHub.downloadFirstLayer" , { title, layerCount } ) ,
334+ downloadError : t ( "arcgisHub.downloadError" ) ,
335+ details : t ( "arcgisHub.details" ) ,
336+ } ) ;
337+ setOpenDataCatalogLabels ( {
338+ socrataHint : t ( "openDataCatalogs.socrataHint" ) ,
339+ ckanHint : t ( "openDataCatalogs.ckanHint" ) ,
340+ searchPlaceholder : ( name ) => t ( "openDataCatalogs.searchPlaceholder" , { name } ) ,
341+ search : t ( "openDataCatalogs.search" ) ,
342+ enterKeyword : t ( "openDataCatalogs.enterKeyword" ) ,
343+ loadMore : t ( "openDataCatalogs.loadMore" ) ,
344+ searching : t ( "openDataCatalogs.searching" ) ,
345+ noResults : t ( "openDataCatalogs.noResults" ) ,
346+ showing : ( shown , total ) => t ( "openDataCatalogs.showing" , { shown, total } ) ,
347+ noDescription : t ( "openDataCatalogs.noDescription" ) ,
348+ add : t ( "openDataCatalogs.add" ) ,
349+ details : t ( "openDataCatalogs.details" ) ,
350+ adding : ( title ) => t ( "openDataCatalogs.adding" , { title } ) ,
351+ added : ( title ) => t ( "openDataCatalogs.added" , { title } ) ,
352+ addError : t ( "openDataCatalogs.addError" ) ,
353+ searchError : t ( "openDataCatalogs.searchError" ) ,
354+ } ) ;
302355 setEarthdataGisLabels ( {
303356 hint : t ( "earthdataGis.hint" ) ,
304357 searchPlaceholder : t ( "earthdataGis.searchPlaceholder" ) ,
@@ -964,6 +1017,17 @@ export function TopToolbar({
9641017 ) ,
9651018 ) ;
9661019 const [ addDataKind , setAddDataKind ] = useState < AddDataKind | null > ( null ) ;
1020+ const [ addDataTargetGroupId , setAddDataTargetGroupId ] = useState < string | null > ( null ) ;
1021+ const addDataInitialLayerIdsRef = useRef < Set < string > > ( new Set ( ) ) ;
1022+ // Every path that opens the dialog outside the OPEN_ADD_DATA_EVENT listener
1023+ // (the Add Data menu, the command palette, the 3D-model button) is ungrouped,
1024+ // so it must drop any group target a previous open left behind — otherwise
1025+ // this session's layers would be swept into that stale, unrelated group when
1026+ // the dialog closes. Only the listener sets a target, and it sets both.
1027+ const openAddDataKind = useCallback ( ( kind : AddDataKind ) => {
1028+ setAddDataTargetGroupId ( null ) ;
1029+ setAddDataKind ( kind ) ;
1030+ } , [ ] ) ;
9671031 // PostgreSQL prefill (saved connection / clicked table) from the Browser panel.
9681032 const [ addDataPostgres , setAddDataPostgres ] = useState < OpenAddDataPostgres | undefined > (
9691033 undefined ,
@@ -986,6 +1050,10 @@ export function TopToolbar({
9861050 // open a dialog whose backing service is compiled out.
9871051 if ( detail ?. kind && ! masHidesDataSource ( detail . kind ) ) {
9881052 setAddDataPostgres ( detail . postgres ) ;
1053+ setAddDataTargetGroupId ( detail . groupId ?? null ) ;
1054+ addDataInitialLayerIdsRef . current = new Set (
1055+ useAppStore . getState ( ) . layers . map ( ( layer ) => layer . id ) ,
1056+ ) ;
9891057 setAddDataKind ( detail . kind ) ;
9901058 }
9911059 } ;
@@ -1180,7 +1248,7 @@ export function TopToolbar({
11801248 id : `add.${ kind } ` ,
11811249 title : t ( "toolbar.command.addLayer" , { name : t ( titleKey ) } ) ,
11821250 group : t ( "toolbar.commandGroup.addData" ) ,
1183- run : ( ) => setAddDataKind ( kind ) ,
1251+ run : ( ) => openAddDataKind ( kind ) ,
11841252 } ) ,
11851253 ) ,
11861254 {
@@ -1509,6 +1577,14 @@ export function TopToolbar({
15091577 icon : Crosshair ,
15101578 run : ( ) => setSetViewOpen ( true ) ,
15111579 } ,
1580+ {
1581+ id : "view.comments" ,
1582+ title : "View Comments" ,
1583+ group : t ( "toolbar.commandGroup.view" ) ,
1584+ keywords : "comments review threads notes annotations pins" ,
1585+ icon : MessageSquare ,
1586+ run : ( ) => openRightPanel ( "comments" ) ,
1587+ } ,
15121588 {
15131589 id : "view.theme" ,
15141590 title :
@@ -1629,10 +1705,27 @@ export function TopToolbar({
16291705 } ,
16301706 ] ;
16311707
1708+ // The viewer preset hides every authoring menu, so the surfaces that reach
1709+ // those commands without a menu go with them: the command palette
1710+ // (Ctrl/Cmd+K) and the cheat sheet (?) are not mounted, and the Help menu
1711+ // drops its entries for them (see `viewer` on HelpMenu). Otherwise a
1712+ // `layout=viewer` embed would still answer Ctrl+N with "New Project", or
1713+ // overwrite the host's project on Ctrl+S — exactly what the read-only chrome
1714+ // promises it cannot do.
1715+ //
1716+ // The shortcut layer is narrowed rather than switched off, because the View
1717+ // menu *does* stay visible in this mode: `view.*` is camera and theme work
1718+ // only, so dropping its keys would leave those items clickable but silently
1719+ // keyless. Every command carrying a `shortcut` is either `view.*` or
1720+ // `project.*`, so this is the whole authoring keyboard surface.
1721+ const shortcutCommands = useMemo (
1722+ ( ) => ( viewer ? commands . filter ( ( command ) => command . id . startsWith ( "view." ) ) : commands ) ,
1723+ [ commands , viewer ] ,
1724+ ) ;
16321725 useGlobalShortcuts ( {
1633- commands,
1634- onOpenPalette : ( ) => setCommandPaletteOpen ( true ) ,
1635- onOpenShortcuts : ( ) => setShortcutsOpen ( true ) ,
1726+ commands : shortcutCommands ,
1727+ onOpenPalette : viewer ? undefined : ( ) => setCommandPaletteOpen ( true ) ,
1728+ onOpenShortcuts : viewer ? undefined : ( ) => setShortcutsOpen ( true ) ,
16361729 } ) ;
16371730
16381731 const toolbarButtonSize = compact ? "icon" : "sm" ;
@@ -1672,7 +1765,7 @@ export function TopToolbar({
16721765 < Map className = "h-4 w-4" />
16731766 { showProjectInfo ? < span className = "hidden sm:inline" > { appTitle } </ span > : null }
16741767 </ span >
1675- { isMenuVisible ( uiProfile , "project" ) && (
1768+ { ! viewer && isMenuVisible ( uiProfile , "project" ) && (
16761769 < ProjectMenu
16771770 chrome = { chrome }
16781771 collaborationEnabled = { collaboration . enabled }
@@ -1681,11 +1774,13 @@ export function TopToolbar({
16811774 onOpenFromUrl = { ( ) => projectFiles . setProjectUrlDialogOpen ( true ) }
16821775 onOpenGallery = { ( ) => setGalleryDialogOpen ( true ) }
16831776 onImportQgisProject = { ( ) => void projectFiles . handleImportQgisProject ( ) }
1777+ onImportArcgisProject = { ( ) => void projectFiles . handleImportArcgisProject ( ) }
16841778 onOpenRecent = { ( path ) => {
16851779 void projectFiles . handleOpenRecent ( path ) . then ( ( error ) => {
16861780 if ( error ) projectFiles . setActionError ( error ) ;
16871781 } ) ;
16881782 } }
1783+ onOpenHistory = { onOpenProjectHistory }
16891784 onSave = { ( ) => void projectFiles . handleSave ( ) }
16901785 onSaveAs = { ( ) => void projectFiles . handleSaveAs ( ) }
16911786 onDuplicate = { ( ) => projectFiles . handleDuplicate ( ) }
@@ -1697,7 +1792,7 @@ export function TopToolbar({
16971792 onOpenOfflineBasemap = { onOpenBasemapExtract }
16981793 />
16991794 ) }
1700- { isMenuVisible ( uiProfile , "edit" ) && (
1795+ { ! viewer && isMenuVisible ( uiProfile , "edit" ) && (
17011796 < EditMenu chrome = { chrome } mapControllerRef = { mapControllerRef } />
17021797 ) }
17031798 { isMenuVisible ( uiProfile , "view" ) && (
@@ -1741,20 +1836,20 @@ export function TopToolbar({
17411836 onSaveCurrentProject = { projectFiles . handleSave }
17421837 onProjectCreated = { resetRuntimeControlsForNewProject }
17431838 />
1744- { isMenuVisible ( uiProfile , "addData" ) && (
1839+ { ! viewer && isMenuVisible ( uiProfile , "addData" ) && (
17451840 < AddDataMenu
17461841 chrome = { chrome }
17471842 addLayer = { addLayer }
17481843 osmPbfBusy = { osmPbf . busy }
1749- onSetAddDataKind = { setAddDataKind }
1844+ onSetAddDataKind = { openAddDataKind }
17501845 onAddGltfModel = { ( ) => {
17511846 setAddDataDeckVizKind ( "scenegraph" ) ;
1752- setAddDataKind ( "deckgl-viz" ) ;
1847+ openAddDataKind ( "deckgl-viz" ) ;
17531848 } }
17541849 onOpenOsmPbfDialog = { ( ) => osmPbf . setDialogOpen ( true ) }
17551850 />
17561851 ) }
1757- { isMenuVisible ( uiProfile , "processing" ) && (
1852+ { ! viewer && isMenuVisible ( uiProfile , "processing" ) && (
17581853 < ProcessingMenu
17591854 chrome = { chrome }
17601855 earthEnginePanel = { panels . earthEngine }
@@ -1766,6 +1861,7 @@ export function TopToolbar({
17661861 { isMenuVisible ( uiProfile , "controls" ) && (
17671862 < ControlsMenu
17681863 chrome = { chrome }
1864+ viewer = { viewer }
17691865 controlsVisible = { controlsVisible }
17701866 panels = { panels }
17711867 effectsActive = { isActive ( EFFECTS_PLUGIN_ID ) }
@@ -1790,7 +1886,7 @@ export function TopToolbar({
17901886 onOpenRecordVideo = { ( ) => setRecordVideoOpen ( true ) }
17911887 />
17921888 ) }
1793- { isMenuVisible ( uiProfile , "plugins" ) && (
1889+ { ! viewer && isMenuVisible ( uiProfile , "plugins" ) && (
17941890 < PluginsMenu
17951891 chrome = { chrome }
17961892 appApi = { appApi }
@@ -1805,18 +1901,20 @@ export function TopToolbar({
18051901 { /* Top-level toolbar menus registered by built-in plugins via
18061902 app.registerToolbarMenu(); external plugin menus render after Help
18071903 (below). Renders nothing when none exist. */ }
1808- < PluginToolbarMenus chrome = { chrome } placement = "builtin" />
1809- < SettingsDialog
1810- buttonClassName = { toolbarButtonClass }
1811- buttonSize = { toolbarButtonSize }
1812- iconClassName = { toolbarIconClassName }
1813- mapControllerRef = { mapControllerRef }
1814- showLabels = { showLabels }
1815- onOpenManagePlugins = { ( ) => setManagePluginsOpen ( true ) }
1816- profilePlugins = { profilePlugins }
1817- themeMode = { themeMode }
1818- onToggleThemeMode = { onToggleThemeMode }
1819- />
1904+ { ! viewer ? < PluginToolbarMenus chrome = { chrome } placement = "builtin" /> : null }
1905+ { ! viewer ? (
1906+ < SettingsDialog
1907+ buttonClassName = { toolbarButtonClass }
1908+ buttonSize = { toolbarButtonSize }
1909+ iconClassName = { toolbarIconClassName }
1910+ mapControllerRef = { mapControllerRef }
1911+ showLabels = { showLabels }
1912+ onOpenManagePlugins = { ( ) => setManagePluginsOpen ( true ) }
1913+ profilePlugins = { profilePlugins }
1914+ themeMode = { themeMode }
1915+ onToggleThemeMode = { onToggleThemeMode }
1916+ />
1917+ ) : null }
18201918 { /* No plugin marketplace in the Mac App Store build (all its entry
18211919 points are hidden too; this keeps the install surface out of the
18221920 bundle). */ }
@@ -1832,16 +1930,24 @@ export function TopToolbar({
18321930 onOpenChange = { setPrintLayoutOpen }
18331931 mapControllerRef = { mapControllerRef }
18341932 />
1835- < FieldCollectionDialog
1836- open = { fieldCollectionOpen }
1837- onOpenChange = { setFieldCollectionOpen }
1838- mapControllerRef = { mapControllerRef }
1839- />
1840- < GpsTrackingDialog
1841- open = { gpsTrackingOpen }
1842- onOpenChange = { setGpsTrackingOpen }
1843- mapControllerRef = { mapControllerRef }
1844- />
1933+ { /* Field Collection and GPS Tracking add features and layers to the
1934+ project, so they follow the Controls menu entries that open them out
1935+ of the read-only viewer preset. Record Tour and Record Video below
1936+ only read the map, so they stay. */ }
1937+ { ! viewer && (
1938+ < FieldCollectionDialog
1939+ open = { fieldCollectionOpen }
1940+ onOpenChange = { setFieldCollectionOpen }
1941+ mapControllerRef = { mapControllerRef }
1942+ />
1943+ ) }
1944+ { ! viewer && (
1945+ < GpsTrackingDialog
1946+ open = { gpsTrackingOpen }
1947+ onOpenChange = { setGpsTrackingOpen }
1948+ mapControllerRef = { mapControllerRef }
1949+ />
1950+ ) }
18451951 < RecordTourDialog
18461952 open = { recordTourOpen }
18471953 onOpenChange = { setRecordTourOpen }
@@ -1896,6 +2002,7 @@ export function TopToolbar({
18962002 { isMenuVisible ( uiProfile , "help" ) && (
18972003 < HelpMenu
18982004 chrome = { chrome }
2005+ viewer = { viewer }
18992006 diagnosticsErrorCount = { diagnosticsErrorCount }
19002007 onOpenCommandPalette = { ( ) => setCommandPaletteOpen ( true ) }
19012008 onOpenShortcuts = { ( ) => setShortcutsOpen ( true ) }
@@ -1909,15 +2016,25 @@ export function TopToolbar({
19092016 ) }
19102017 { /* External plugin toolbar menus render after Help so third-party menus
19112018 sit at the end of the banner, past the built-in menus. */ }
1912- < PluginToolbarMenus chrome = { chrome } placement = "external" />
2019+ { ! viewer ? < PluginToolbarMenus chrome = { chrome } placement = "external" /> : null }
19132020 < AddDataDialog
19142021 kind = { addDataKind }
19152022 mapControllerRef = { mapControllerRef }
19162023 initialDeckVizKind = { addDataDeckVizKind }
19172024 initialPostgres = { addDataPostgres }
19182025 onOpenChange = { ( open : boolean ) => {
19192026 if ( ! open ) {
2027+ if ( addDataTargetGroupId ) {
2028+ const state = useAppStore . getState ( ) ;
2029+ const addedIds = state . layers
2030+ . filter ( ( layer ) => ! addDataInitialLayerIdsRef . current . has ( layer . id ) )
2031+ . map ( ( layer ) => layer . id ) ;
2032+ if ( addedIds . length > 0 ) {
2033+ state . moveLayersToGroup ( addedIds , addDataTargetGroupId ) ;
2034+ }
2035+ }
19202036 setAddDataKind ( null ) ;
2037+ setAddDataTargetGroupId ( null ) ;
19212038 setAddDataDeckVizKind ( undefined ) ;
19222039 setAddDataPostgres ( undefined ) ;
19232040 }
@@ -1933,16 +2050,20 @@ export function TopToolbar({
19332050 renderTrigger = { false }
19342051 onOpenChange = { setAboutOpen }
19352052 />
1936- < CommandPalette
1937- open = { commandPaletteOpen }
1938- commands = { commands }
1939- onOpenChange = { setCommandPaletteOpen }
1940- />
1941- < KeyboardShortcutsDialog
1942- open = { shortcutsOpen }
1943- commands = { commands }
1944- onOpenChange = { setShortcutsOpen }
1945- />
2053+ { ! viewer && (
2054+ < CommandPalette
2055+ open = { commandPaletteOpen }
2056+ commands = { commands }
2057+ onOpenChange = { setCommandPaletteOpen }
2058+ />
2059+ ) }
2060+ { ! viewer && (
2061+ < KeyboardShortcutsDialog
2062+ open = { shortcutsOpen }
2063+ commands = { commands }
2064+ onOpenChange = { setShortcutsOpen }
2065+ />
2066+ ) }
19462067 < div className = "ms-auto flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground" >
19472068 < Button
19482069 aria-label = {
0 commit comments