diff --git a/apps/sensenet/src/components/command-palette/CommandPalette.tsx b/apps/sensenet/src/components/command-palette/CommandPalette.tsx index e4000f4aa..f4b093381 100644 --- a/apps/sensenet/src/components/command-palette/CommandPalette.tsx +++ b/apps/sensenet/src/components/command-palette/CommandPalette.tsx @@ -10,7 +10,7 @@ import Autosuggest, { SuggestionSelectedEventData, SuggestionsFetchRequestedPara import { useHistory } from 'react-router-dom' import { ResponsiveContext, ResponsivePersonalSettings } from '../../context' import { globals } from '../../globalStyles' -import { useLocalization, useSnRoute, useTheme } from '../../hooks' +import { useLocalization, useSelectionService, useSnRoute, useTheme } from '../../hooks' import { CommandProviderManager } from '../../services' import { ContentContextMenu } from '../context-menu/content-context-menu' import { CommandPaletteHitsContainer } from './CommandPaletteHitsContainer' @@ -65,6 +65,19 @@ const useStyles = makeStyles(() => { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, }, + actionContextHeader: { + padding: '8px 12px', + borderBottom: '1px solid rgba(0, 0, 0, 0.12)', + color: '#3c4654', + fontSize: '12px', + lineHeight: '16px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + actionContextTarget: { + fontWeight: 600, + }, }) }) @@ -91,6 +104,16 @@ export const CommandPalette = () => { const cpm = useMemo(() => injector.getInstance(CommandProviderManager), [injector]) const snRoute = useSnRoute() const isContextMenuInteractingRef = useRef(false) + const selectionService = useSelectionService() + const [activeContent, setActiveContent] = useState(selectionService.activeContent.getValue()) + + useEffect(() => { + const activeContentObserver = selectionService.activeContent.subscribe((content) => { + setActiveContent(content) + }) + + return () => activeContentObserver.dispose() + }, [selectionService.activeContent]) useEffect(() => { const handleKeyUp = (ev: KeyboardEvent) => { @@ -213,6 +236,23 @@ export const CommandPalette = () => { setIsOpened(false) } + const actionMode = inputValue.startsWith('>') + const actionContextHeader = actionMode ? ( +
+ {activeContent ? ( + <> + {localization.actionContext} + + {activeContent.DisplayName || activeContent.Name} + + {activeContent.Path ? ` (${activeContent.Path})` : ''} + + ) : ( + localization.noActionContext + )} +
+ ) : undefined + return (
@@ -241,7 +281,9 @@ export const CommandPalette = () => { onOpenContextMenu={handleOpenSuggestionContextMenu} /> )} - renderSuggestionsContainer={(params) => } + renderSuggestionsContainer={(params) => ( + + )} inputProps={{ className: `${classes.input} ${inputValue ? classes.inputOpened : ''}`, value: inputValue, diff --git a/apps/sensenet/src/components/command-palette/CommandPaletteHitsContainer.tsx b/apps/sensenet/src/components/command-palette/CommandPaletteHitsContainer.tsx index b1e1c46e3..cb6dbeba8 100644 --- a/apps/sensenet/src/components/command-palette/CommandPaletteHitsContainer.tsx +++ b/apps/sensenet/src/components/command-palette/CommandPaletteHitsContainer.tsx @@ -5,7 +5,9 @@ import { RenderSuggestionsContainerParams } from 'react-autosuggest' import { ResponsiveContext } from '../../context' import { useLocalization } from '../../hooks' -export const CommandPaletteHitsContainer: FunctionComponent = (options) => { +export const CommandPaletteHitsContainer: FunctionComponent< + RenderSuggestionsContainerParams & { header?: React.ReactNode } +> = (options) => { const device = useContext(ResponsiveContext) const localization = useLocalization() @@ -18,15 +20,18 @@ export const CommandPaletteHitsContainer: FunctionComponent - - {options.children} - + <> + {options.header} + + {options.children} + + ) } diff --git a/apps/sensenet/src/components/content/Explore.tsx b/apps/sensenet/src/components/content/Explore.tsx index 90b955386..95ae92436 100644 --- a/apps/sensenet/src/components/content/Explore.tsx +++ b/apps/sensenet/src/components/content/Explore.tsx @@ -13,7 +13,7 @@ import { import { ColumnSetting } from '@sensenet/list-controls-react/src/ContentList/content-list-base-props' import { ColDef } from 'ag-grid-community' import { clsx } from 'clsx' -import React, { useContext, useMemo, useRef, useState } from 'react' +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react' import { useHistory } from 'react-router' import { GridKeyEnum } from '../../../src/components/grid/enums/GridKey.enum' import { ResponsivePersonalSettings } from '../../context' @@ -178,6 +178,21 @@ type ExploreGridOrApplicationProps = { onActivateItem: (activeItem: GenericContent) => Promise } +const ActiveContentRouteSync: React.FC = () => { + const currentContent = useContext(CurrentContentContext) + const selectionService = useSelectionService() + + useEffect(() => { + const activeContent = selectionService.activeContent.getValue() + + if (currentContent && (!activeContent || !PathHelper.isInSubTree(activeContent.Path, currentContent.Path))) { + selectionService.activeContent.setValue(currentContent) + } + }, [currentContent, selectionService.activeContent]) + + return null +} + const ExploreGridOrApplication: React.FC = ({ currentPath, fieldsToDisplay, @@ -360,6 +375,7 @@ export function Explore({ key={JSON.stringify(currentChildrenLoadSettings)} loadChildrenSettings={currentChildrenLoadSettings}> +
diff --git a/apps/sensenet/src/components/grid/Grid.tsx b/apps/sensenet/src/components/grid/Grid.tsx index 7f6d35f86..e9ad7d280 100644 --- a/apps/sensenet/src/components/grid/Grid.tsx +++ b/apps/sensenet/src/components/grid/Grid.tsx @@ -7,6 +7,7 @@ import { ColumnApi, GridApi, GridReadyEvent, + RowClickedEvent, RowDoubleClickedEvent, SelectionChangedEvent, } from 'ag-grid-community' @@ -86,10 +87,19 @@ export function Grid(props: GridProps }, [children, isGridLoading, setLoadingWithMinDuration]) const onRowDoubleClicked = (item: RowDoubleClickedEvent) => { + if (item.data) { + props.onActiveItemChange?.(item.data) + } setLoadingWithMinDuration(true) item.data.isFolder ? props.onParentChange(item.data) : props.onActivateItem(item.data) } + const onRowClicked = (item: RowClickedEvent) => { + if (item.data) { + props.onActiveItemChange?.(item.data) + } + } + const onSelectionChanged = (params: SelectionChangedEvent) => { const selectedIds = params.api.getSelectedRows().map((c) => c.Id) const selectedItems: GenericContent[] = children.filter((item) => selectedIds.includes(item.Id)) @@ -102,6 +112,7 @@ export function Grid(props: GridProps if (!event.node || !event.event) return const mouseEvent = event.event as MouseEvent setContextMenuItem(event.data) + event.data && props.onActiveItemChange?.(event.data) setContextMenuAnchorPos({ top: mouseEvent.clientY, left: mouseEvent.clientX }) setIsContextMenuOpened(true) } @@ -231,6 +242,7 @@ export function Grid(props: GridProps rowSelection={'multiple'} suppressReactUi={true} tooltipShowDelay={100} + onRowClicked={onRowClicked} onRowDoubleClicked={onRowDoubleClicked} preventDefaultOnContextMenu={true} onGridReady={onGridReady} diff --git a/apps/sensenet/src/components/tree/StyledTreeItem.tsx b/apps/sensenet/src/components/tree/StyledTreeItem.tsx index 1629f2234..75282f285 100644 --- a/apps/sensenet/src/components/tree/StyledTreeItem.tsx +++ b/apps/sensenet/src/components/tree/StyledTreeItem.tsx @@ -5,7 +5,7 @@ import { useRepository } from '@sensenet/hooks-react' import React, { MouseEventHandler, useCallback, useContext, useEffect, useRef, useState } from 'react' import { useHistory } from 'react-router' import { ResponsivePersonalSettings } from '../../context' -import { useQuery, useSnRoute } from '../../hooks' +import { useQuery, useSelectionService, useSnRoute } from '../../hooks' import { getPrimaryActionUrl, navigateToAction } from '../../services' import { ContentContextMenu } from '../context-menu/content-context-menu' import { Icon } from '../Icon' @@ -37,6 +37,7 @@ export const StyledTreeItem = ({ const repository = useRepository() const snRoute = useSnRoute() const uiSettings = useContext(ResponsivePersonalSettings) + const selectionService = useSelectionService() const currentPath = useQuery().get('path') const mountedRef = useRef(true) @@ -141,6 +142,7 @@ export const StyledTreeItem = ({ const displayName = contentvalue.DisplayName if (displayName?.endsWith('.settings') || displayName?.endsWith('.xml')) { + selectionService.activeContent.setValue(contentvalue) history.push( getPrimaryActionUrl({ content: contentvalue, repository, uiSettings, location: history.location, snRoute }), ) @@ -148,6 +150,7 @@ export const StyledTreeItem = ({ } if (editMode) { + selectionService.activeContent.setValue(contentvalue) navigateToAction({ history, routeMatch: snRoute.match!, @@ -155,6 +158,7 @@ export const StyledTreeItem = ({ queryParams: { content: contentvalue.Path.replace(snRoute.path, '') }, }) } else { + selectionService.activeContent.setValue(contentvalue) const itemPath = (event.target as HTMLElement).closest('[data-path]')?.getAttribute('data-path') setExpandItems((prev) => { const updated = new Set(prev) @@ -176,11 +180,12 @@ export const StyledTreeItem = ({ if (isDisabled) return event.preventDefault() event.stopPropagation() + selectionService.activeContent.setValue(contentvalue) setContextMenuItem(contentvalue) setContextMenuAnchorPos({ top: event.clientY, left: event.clientX }) setIsContextMenuOpened(true) }, - [contentvalue, isDisabled], + [contentvalue, isDisabled, selectionService.activeContent], ) return ( diff --git a/apps/sensenet/src/hooks/use-tree-navigation.ts b/apps/sensenet/src/hooks/use-tree-navigation.ts index 3ddf0465d..015156412 100644 --- a/apps/sensenet/src/hooks/use-tree-navigation.ts +++ b/apps/sensenet/src/hooks/use-tree-navigation.ts @@ -3,16 +3,19 @@ import { useCallback, useEffect, useState } from 'react' import { useHistory, useRouteMatch } from 'react-router' import { resolvePathParams } from '../application-paths' import { useQuery } from '../hooks/use-query' +import { useSelectionService } from '../hooks/use-selection-service' import { pathWithQueryParams } from '../services/query-string-builder' export const useTreeNavigation = (defaultPath: string) => { const history = useHistory() const match = useRouteMatch<{ browseType: string }>() + const selectionService = useSelectionService() const pathFromQuery = useQuery().get('path') const [currentPath, setCurrentPath] = useState(pathFromQuery ? decodeURIComponent(pathFromQuery) : '') const onNavigate = useCallback( (content: GenericContent) => { + selectionService.activeContent.setValue(content) const searchParams = new URLSearchParams(history.location.search) searchParams.delete('content') const newPath = content.Path.replace(defaultPath, '') @@ -26,7 +29,7 @@ export const useTreeNavigation = (defaultPath: string) => { ) setCurrentPath(newPath) }, - [history, match.path, match.params, defaultPath], + [history, match.path, match.params, defaultPath, selectionService.activeContent], ) useEffect(() => { diff --git a/apps/sensenet/src/localization/default.ts b/apps/sensenet/src/localization/default.ts index 78c43cf1f..74665c28d 100644 --- a/apps/sensenet/src/localization/default.ts +++ b/apps/sensenet/src/localization/default.ts @@ -22,6 +22,8 @@ const values = { title: 'Search', clear: 'Clear', actions: 'Actions', + actionContext: 'Actions for: ', + noActionContext: 'No content selected for actions.', help: { readMeTitle: 'ReadMe', readMeDescription: 'Opens the latest readme.md file from GitHub in a new window', diff --git a/apps/sensenet/src/localization/hungarian.ts b/apps/sensenet/src/localization/hungarian.ts index 31433a747..4ec737fea 100644 --- a/apps/sensenet/src/localization/hungarian.ts +++ b/apps/sensenet/src/localization/hungarian.ts @@ -12,6 +12,8 @@ const values: Localization = { commandPalette: { title: 'Command palette megnyitása', actions: 'Műveletek', + actionContext: 'Műveletek ezen: ', + noActionContext: 'Nincs kiválasztott tartalom a műveletekhez.', searchSuggestionList: 'Keresési javaslatok listája', }, contentInfoDialog: {