From 25d479a4f151e313da26d9752e2c40dac6ff0075 Mon Sep 17 00:00:00 2001 From: VargaJoe Date: Thu, 2 Jul 2026 11:37:38 +0200 Subject: [PATCH 1/5] feat(admin-ui): improve mobile layout --- .../src/components/ContentBreadcrumbs.tsx | 14 ++- .../src/components/appbar/desktop-app-bar.tsx | 16 ++- .../components/content/AUIApplicationView.tsx | 5 +- .../src/components/content/Explore.tsx | 114 ++++++++++++++---- apps/sensenet/src/components/grid/Grid.tsx | 10 +- .../src/components/layout/DesktopLayout.tsx | 6 +- .../components/view-controls/common/styles.ts | 8 ++ .../view-controls/common/view-title.tsx | 10 ++ apps/sensenet/src/style.css | 37 ++++++ 9 files changed, 186 insertions(+), 34 deletions(-) diff --git a/apps/sensenet/src/components/ContentBreadcrumbs.tsx b/apps/sensenet/src/components/ContentBreadcrumbs.tsx index e94150c7d..9e1d07a9b 100644 --- a/apps/sensenet/src/components/ContentBreadcrumbs.tsx +++ b/apps/sensenet/src/components/ContentBreadcrumbs.tsx @@ -9,12 +9,24 @@ import { getPrimaryActionUrl } from '../services' import { BatchActions } from './BatchActions' import { BreadcrumbItem, Breadcrumbs } from './Breadcrumbs' -const useStyles = makeStyles(() => { +const useStyles = makeStyles((theme) => { return createStyles({ buttonsWrapper: { display: 'flex', alignItems: 'center', marginLeft: '10px', + minWidth: 0, + overflow: 'hidden', + [theme.breakpoints.down('sm')]: { + marginLeft: 0, + width: '100%', + '& .MuiBreadcrumbs-ol': { + flexWrap: 'nowrap', + overflowX: 'auto', + overflowY: 'hidden', + maxWidth: '100%', + }, + }, }, }) }) diff --git a/apps/sensenet/src/components/appbar/desktop-app-bar.tsx b/apps/sensenet/src/components/appbar/desktop-app-bar.tsx index ed95f6fb2..d828dbcb1 100644 --- a/apps/sensenet/src/components/appbar/desktop-app-bar.tsx +++ b/apps/sensenet/src/components/appbar/desktop-app-bar.tsx @@ -7,12 +7,12 @@ import { clsx } from 'clsx' import React, { useContext, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import logo from '../../assets/sensenet_white.png' -import { ResponsivePersonalSettings } from '../../context' +import { ResponsiveContext, ResponsivePersonalSettings } from '../../context' import { globals, useGlobalStyles } from '../../globalStyles' import { CommandPalette } from '../command-palette/CommandPalette' import { DesktopNavMenu } from './desktop-nav-menu' -const useStyles = makeStyles(() => { +const useStyles = makeStyles((theme) => { return createStyles({ appBar: { position: 'relative', @@ -38,9 +38,18 @@ const useStyles = makeStyles(() => { fontSize: '18px', fontWeight: 500, fontFamily: 'Roboto,Helvetica,Arial,sans-serif', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + maxWidth: '36vw', '&:hover': { cursor: 'pointer', }, + [theme.breakpoints.down('sm')]: { + maxWidth: '28vw', + marginRight: '0.5rem', + fontSize: '13px', + }, }, }) }) @@ -49,6 +58,7 @@ const PORTAL_SETTING_PATH = '/Root/System/Settings/Portal.settings' export const DesktopAppBar: React.FunctionComponent<{ openDrawer?: () => void }> = (props) => { const personalSettings = useContext(ResponsivePersonalSettings) + const device = useContext(ResponsiveContext) const classes = useStyles() const globalClasses = useGlobalStyles() const repository = useRepository() @@ -110,7 +120,7 @@ export const DesktopAppBar: React.FunctionComponent<{ openDrawer?: () => void }> logo - {personalSettings.drawer.type === 'temporary' ? ( + {personalSettings.drawer.enabled && (personalSettings.drawer.type === 'temporary' || device === 'mobile') ? ( { props.openDrawer && props.openDrawer() diff --git a/apps/sensenet/src/components/content/AUIApplicationView.tsx b/apps/sensenet/src/components/content/AUIApplicationView.tsx index bf85ab40d..05a326886 100644 --- a/apps/sensenet/src/components/content/AUIApplicationView.tsx +++ b/apps/sensenet/src/components/content/AUIApplicationView.tsx @@ -362,10 +362,7 @@ export const AUIApplicationView: React.FC = () => { const applicationHtml = content?.Html?.trim() ?? '' const adminUiUrl = window.location.origin - const bridgeLocation = useMemo( - () => createBridgeLocation(adminUiUrl, routerLocation), - [adminUiUrl, routerLocation.hash, routerLocation.pathname, routerLocation.search], - ) + const bridgeLocation = useMemo(() => createBridgeLocation(adminUiUrl, routerLocation), [adminUiUrl, routerLocation]) const bridgeTheme = useMemo(() => createBridgeTheme(theme.palette.type), [theme.palette.type]) const srcDoc = useMemo( () => diff --git a/apps/sensenet/src/components/content/Explore.tsx b/apps/sensenet/src/components/content/Explore.tsx index 95ae92436..dc5a581db 100644 --- a/apps/sensenet/src/components/content/Explore.tsx +++ b/apps/sensenet/src/components/content/Explore.tsx @@ -1,4 +1,5 @@ -import { createStyles, makeStyles, Theme, useTheme } from '@material-ui/core' +import { createStyles, IconButton, makeStyles, SwipeableDrawer, Theme, Tooltip, useTheme } from '@material-ui/core' +import MenuIcon from '@material-ui/icons/Menu' import { ODataFieldParameter, ODataParams } from '@sensenet/client-core' import { PathHelper } from '@sensenet/client-utils' import { GenericContent } from '@sensenet/default-content-types' @@ -16,8 +17,8 @@ import { clsx } from 'clsx' 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' -import { useGlobalStyles } from '../../globalStyles' +import { ResponsiveContext, ResponsivePersonalSettings } from '../../context' +import { globals, useGlobalStyles } from '../../globalStyles' import { useQuery, useSelectionService, useSnRoute } from '../../hooks' import { getPrimaryActionUrl, navigateToAction } from '../../services' import { ContentBreadcrumbs } from '../ContentBreadcrumbs' @@ -72,21 +73,34 @@ const useStyles = makeStyles((theme) => boxSizing: 'border-box', borderBottom: theme.palette.type === 'light' ? '1px solid #DBDBDB' : '1px solid rgba(255, 255, 255, 0.11)', justifyContent: 'start', + minHeight: globals.common.drawerItemHeight, + overflow: 'hidden', + }, + breadcrumbsContent: { + minWidth: 0, + overflow: 'hidden', + flexGrow: 1, }, treeAndDatagridWrapper: { display: 'flex', width: '100%', height: '100%', position: 'relative', - overflow: 'auto', + overflow: 'hidden', + minWidth: 0, }, exploreContainer: { display: 'flex', flexFlow: 'column', width: '100%', + minWidth: 0, + flexGrow: 1, position: 'relative', overflow: 'hidden', borderLeft: theme.palette.type === 'light' ? '1px solid #DBDBDB' : '1px solid rgba(255, 255, 255, 0.11)', + [theme.breakpoints.down('sm')]: { + borderLeft: 'none', + }, }, simpleTree: { width: ({ width }) => `${width}px`, @@ -121,6 +135,30 @@ const useStyles = makeStyles((theme) => borderLeft: '1px dashed #cececeff', }, }, + mobileTreeButton: { + display: 'none', + flexShrink: 0, + marginLeft: '4px', + [theme.breakpoints.down('sm')]: { + display: 'inline-flex', + }, + }, + mobileTreePaper: { + width: '86vw', + maxWidth: 360, + backgroundColor: theme.palette.background.default, + overflow: 'hidden', + }, + mobileTreeHeader: { + height: globals.common.drawerItemHeight, + borderBottom: theme.palette.type === 'light' ? '1px solid #DBDBDB' : '1px solid rgba(255, 255, 255, 0.11)', + padding: '0 12px', + fontWeight: 500, + }, + mobileTreeContent: { + height: `calc(100% - ${globals.common.drawerItemHeight}px)`, + overflow: 'auto', + }, resizeButton: { position: 'sticky', top: 0, @@ -247,6 +285,9 @@ export function Explore({ const classes = useStyles({ width }) const globalClasses = useGlobalStyles() const isResizing = useRef(false) + const device = useContext(ResponsiveContext) + const [mobileTreeOpened, setMobileTreeOpened] = useState(false) + const isMobile = device === 'mobile' const handleMouseDown = (e: React.MouseEvent) => { isResizing.current = true @@ -370,6 +411,22 @@ export function Explore({ ) } + const handleTreeNavigate = (item: GenericContent) => { + onNavigate(item) + setMobileTreeOpened(false) + } + + const renderTree = () => ( + + ) + return (
- { - onNavigate(i.content) - }} - batchActions={true} - /> + {hasTree && isMobile ? ( + + setMobileTreeOpened(true)}> + + + + ) : null} +
+ { + onNavigate(i.content) + }} + batchActions={true} + /> +
- {hasTree && ( + {hasTree && !isMobile && (
- { - onNavigate(item) - }} - parentPath={PathHelper.isAncestorOf(rootPath, currentPath) ? rootPath : currentPath} - activeItemPath={currentPath} - loadSettings={loadTreeSettings} - onNavigate={onNavigate} - rootLoaded={false} - /> + {renderTree()}
)}
{renderContent()}
+ {hasTree && isMobile ? ( + setMobileTreeOpened(true)} + onClose={() => setMobileTreeOpened(false)} + ModalProps={{ keepMounted: true }} + PaperProps={{ className: classes.mobileTreePaper }}> +
Content tree
+
{renderTree()}
+
+ ) : null}
diff --git a/apps/sensenet/src/components/grid/Grid.tsx b/apps/sensenet/src/components/grid/Grid.tsx index d325217d8..d1a7fb12d 100644 --- a/apps/sensenet/src/components/grid/Grid.tsx +++ b/apps/sensenet/src/components/grid/Grid.tsx @@ -21,6 +21,7 @@ import { GridProps } from './Props/GridProps' import { useGridLoading } from './Providers/GridLoadingProvider' const SMALL_SCREEN_COL_FILTER = ['Id', 'Actions'] +const MOBILE_SCREEN_COL_FIELDS = ['0', 'Icon', 'DisplayName', 'Name', 'Actions'] export function Grid(props: GridProps) { const { isGridLoading, setIsGridLoading } = useGridLoading() @@ -167,7 +168,10 @@ export function Grid(props: GridProps const updateColumnDefsBasedOnWindowSize = useCallback(() => { const width = window.innerWidth - if (width < 1536) { + if (width < 600) { + const mobileCols = props.colDef.filter((col) => MOBILE_SCREEN_COL_FIELDS.includes(col.field || '')) + setColumnDefs(mobileCols.length ? mobileCols : props.colDef.slice(0, 3)) + } else if (width < 1536) { const filteredCols = props.colDef.filter((col) => !SMALL_SCREEN_COL_FILTER.includes(col.field || '')) setColumnDefs(filteredCols) } else { @@ -196,9 +200,9 @@ export function Grid(props: GridProps useEffect(() => { if (gridApi.current) { - gridApi.current.setColumnDefs([...props.colDef]) + updateColumnDefsBasedOnWindowSize() } - }, [props.colDef]) + }, [props.colDef, updateColumnDefsBasedOnWindowSize]) const onSortChanged = useCallback(() => { if (columnApi.current) { diff --git a/apps/sensenet/src/components/layout/DesktopLayout.tsx b/apps/sensenet/src/components/layout/DesktopLayout.tsx index b47a5858d..a2174c1de 100644 --- a/apps/sensenet/src/components/layout/DesktopLayout.tsx +++ b/apps/sensenet/src/components/layout/DesktopLayout.tsx @@ -2,7 +2,7 @@ import { createStyles, makeStyles, Theme } from '@material-ui/core' import { useInjector, useRepository } from '@sensenet/hooks-react' import { clsx } from 'clsx' import React, { useContext, useEffect, useState } from 'react' -import { ResponsivePersonalSettings } from '../../context' +import { ResponsiveContext, ResponsivePersonalSettings } from '../../context' import { globals, useGlobalStyles } from '../../globalStyles' import { CustomActionCommandProvider } from '../../services/CommandProviders/CustomActionCommandProvider' import { DesktopAppBar } from '../appbar/desktop-app-bar' @@ -47,12 +47,14 @@ const useStyles = makeStyles((theme: Theme) => { export const DesktopLayout: React.FunctionComponent = (props) => { const settings = useContext(ResponsivePersonalSettings) + const device = useContext(ResponsiveContext) const repo = useRepository() const { openDialog, closeLastDialog } = useDialog() const customActionService = useInjector().getInstance(CustomActionCommandProvider) const [tempDrawerOpened, setTempDrawerOpened] = useState(false) const classes = useStyles() const globalClasses = useGlobalStyles() + const useTemporaryDrawer = settings.drawer.type === 'temporary' || device === 'mobile' useEffect(() => { const observables = [ @@ -99,7 +101,7 @@ export const DesktopLayout: React.FunctionComponent = (props) => {
{settings.drawer.enabled ? ( <> - {settings.drawer.type === 'temporary' ? ( + {useTemporaryDrawer ? ( setTempDrawerOpened(false)} onOpen={() => setTempDrawerOpened(true)} diff --git a/apps/sensenet/src/components/view-controls/common/styles.ts b/apps/sensenet/src/components/view-controls/common/styles.ts index 925ae0149..22b045061 100644 --- a/apps/sensenet/src/components/view-controls/common/styles.ts +++ b/apps/sensenet/src/components/view-controls/common/styles.ts @@ -8,12 +8,20 @@ const useStyles = makeStyles((theme) => { overflowY: 'auto', maxWidth: '100%', maxHeight: `calc(100% - ${globals.common.formActionButtonsHeight}px - ${globals.common.formTitleHeight}px)`, + [theme.breakpoints.down('sm')]: { + padding: '12px', + maxHeight: `calc(100% - ${globals.common.formActionButtonsHeight}px - ${globals.common.formTitleHeight}px)`, + }, }, actionButtonWrapper: { padding: '8px', paddingRight: '39px', borderTop: theme.palette.type === 'light' ? '1px solid #DBDBDB' : '1px solid rgba(255, 255, 255, 0.11)', marginTop: 'auto', + [theme.breakpoints.down('sm')]: { + paddingRight: '8px', + flexWrap: 'wrap', + }, }, }) }) diff --git a/apps/sensenet/src/components/view-controls/common/view-title.tsx b/apps/sensenet/src/components/view-controls/common/view-title.tsx index c57f54151..470697666 100644 --- a/apps/sensenet/src/components/view-controls/common/view-title.tsx +++ b/apps/sensenet/src/components/view-controls/common/view-title.tsx @@ -24,14 +24,21 @@ const useStyles = makeStyles((theme) => { borderBottom: theme.palette.type === 'light' ? '1px solid #DBDBDB' : '1px solid rgba(255, 255, 255, 0.11)', display: 'flex', flexDirection: 'column', + minWidth: 0, + [theme.breakpoints.down('sm')]: { + fontSize: '16px', + }, }, textBolder: { fontWeight: 500, textAlign: 'center', + overflowWrap: 'anywhere', }, actionBar: { display: 'flex', marginBottom: '6px', + flexWrap: 'wrap', + justifyContent: 'center', }, typeinfo: { fontSize: '12px', @@ -44,6 +51,9 @@ const useStyles = makeStyles((theme) => { }, viewTitle: { padding: '6px', + maxWidth: '100%', + boxSizing: 'border-box', + overflowWrap: 'anywhere', }, }) }) diff --git a/apps/sensenet/src/style.css b/apps/sensenet/src/style.css index aef2ff9ca..5bb0073d1 100644 --- a/apps/sensenet/src/style.css +++ b/apps/sensenet/src/style.css @@ -193,6 +193,43 @@ span.icon svg { .buttonPanel .MuiButton-label { flex-direction: column; } + +@media (max-width: 600px) { + .gridTopPanel { + padding: 8px; + padding-left: 44px; + align-items: flex-start; + } + + .gridTopPanel .iconArea { + width: 36px; + } + + .gridTopPanel h1 { + font-size: 14px; + overflow-wrap: anywhere; + } + + .gridTopPanel label, + .gridTopPanel a, + .gridTopPanel span { + overflow-wrap: anywhere; + } + + .buttonPanel { + display: flex; + width: 100%; + height: auto; + margin-left: -36px; + overflow-x: auto; + } + + .buttonPanel .MuiButton-root { + min-width: 64px; + flex: 0 0 auto; + } +} + .MuiListItemIcon-root .svgicon { width: 24px; height: 24px; From 5687b0459ea47c71bbbe716458d1d0949ce90b9e Mon Sep 17 00:00:00 2001 From: VargaJoe Date: Thu, 2 Jul 2026 13:28:17 +0200 Subject: [PATCH 2/5] fix(admin-ui): stabilize mobile dev view --- .../src/components/ContentBreadcrumbs.tsx | 118 +++++++++++++----- .../grid/Formatters/ActionFormatter.tsx | 10 +- apps/sensenet/src/components/grid/Grid.tsx | 10 ++ .../src/components/tree/StyledTreeItem.tsx | 33 ++++- apps/sensenet/webpack.dev.js | 4 + docker-compose.dev.yml | 1 + 6 files changed, 139 insertions(+), 37 deletions(-) diff --git a/apps/sensenet/src/components/ContentBreadcrumbs.tsx b/apps/sensenet/src/components/ContentBreadcrumbs.tsx index 9e1d07a9b..5b5ab33af 100644 --- a/apps/sensenet/src/components/ContentBreadcrumbs.tsx +++ b/apps/sensenet/src/components/ContentBreadcrumbs.tsx @@ -1,9 +1,10 @@ -import { createStyles, makeStyles } from '@material-ui/core' +import { Button, createStyles, IconButton, makeStyles, Tooltip } from '@material-ui/core' +import ArrowUpward from '@material-ui/icons/ArrowUpward' import { GenericContent } from '@sensenet/default-content-types' import { CurrentAncestorsContext, CurrentContentContext, useRepository } from '@sensenet/hooks-react' import React, { useContext } from 'react' import { useHistory } from 'react-router-dom' -import { ResponsivePersonalSettings } from '../context' +import { ResponsiveContext, ResponsivePersonalSettings } from '../context' import { useSelectionService } from '../hooks' import { getPrimaryActionUrl } from '../services' import { BatchActions } from './BatchActions' @@ -20,14 +21,43 @@ const useStyles = makeStyles((theme) => { [theme.breakpoints.down('sm')]: { marginLeft: 0, width: '100%', - '& .MuiBreadcrumbs-ol': { - flexWrap: 'nowrap', - overflowX: 'auto', - overflowY: 'hidden', - maxWidth: '100%', - }, }, }, + desktopBreadcrumbs: { + minWidth: 0, + overflow: 'hidden', + [theme.breakpoints.down('sm')]: { + display: 'none', + }, + }, + mobileLocation: { + display: 'none', + [theme.breakpoints.down('sm')]: { + display: 'flex', + alignItems: 'center', + minWidth: 0, + flex: '1 1 auto', + }, + }, + mobileLocationButton: { + minWidth: 0, + maxWidth: '100%', + paddingLeft: 4, + paddingRight: 4, + textTransform: 'none', + justifyContent: 'flex-start', + overflow: 'hidden', + '& .MuiButton-label': { + display: 'block', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + }, + mobileUpButton: { + flex: '0 0 auto', + padding: 6, + }, }) }) @@ -45,31 +75,59 @@ export const ContentBreadcrumbs = (pr const { location } = history const classes = useStyles() const selectionService = useSelectionService() + const device = useContext(ResponsiveContext) + + const items = [ + ...ancestors.map((content) => ({ + displayName: content.DisplayName || content.Name, + title: content.Path, + url: getPrimaryActionUrl({ content, repository, uiSettings, location }), + content, + })), + { + displayName: parent.DisplayName || parent.Name, + title: parent.Path, + url: getPrimaryActionUrl({ content: parent, repository, uiSettings, location }), + content: parent, + }, + ] + + const handleItemClick = (item: BreadcrumbItem) => { + selectionService.activeContent.setValue(item.content) + props.onItemClick + ? props.onItemClick(item) + : history.push(getPrimaryActionUrl({ content: item.content, repository, uiSettings, location })) + } + + const parentItem = items[items.length - 1] + const ancestorItem = items[items.length - 2] return (
- - items={[ - ...ancestors.map((content) => ({ - displayName: content.DisplayName || content.Name, - title: content.Path, - url: getPrimaryActionUrl({ content, repository, uiSettings, location }), - content, - })), - { - displayName: parent.DisplayName || parent.Name, - title: parent.Path, - url: getPrimaryActionUrl({ content: parent, repository, uiSettings, location }), - content: parent, - }, - ]} - onItemClick={(_ev, item) => { - selectionService.activeContent.setValue(item.content) - props.onItemClick - ? props.onItemClick(item) - : history.push(getPrimaryActionUrl({ content: item.content, repository, uiSettings, location })) - }} - /> + {device === 'mobile' ? ( +
+ {ancestorItem ? ( + + handleItemClick(ancestorItem)}> + + + + ) : null} + + + +
+ ) : ( +
+ items={items} onItemClick={(_ev, item) => handleItemClick(item)} /> +
+ )} {props.batchActions && }
) diff --git a/apps/sensenet/src/components/grid/Formatters/ActionFormatter.tsx b/apps/sensenet/src/components/grid/Formatters/ActionFormatter.tsx index c8839354e..06e8212fc 100644 --- a/apps/sensenet/src/components/grid/Formatters/ActionFormatter.tsx +++ b/apps/sensenet/src/components/grid/Formatters/ActionFormatter.tsx @@ -60,6 +60,8 @@ export function ActionFormatter(props: { data: Content }) { } }, [content, setActionsWopi]) const handleClick = (event: any) => { + event.preventDefault() + event.stopPropagation() setAnchorEl(event.currentTarget) } @@ -80,7 +82,10 @@ export function ActionFormatter(props: { data: Content }) { key="ODataActions" disableRipple={true} data-test="content-context-menu-odata-actions" - onClick={runODataActions}> + onClick={(ev) => { + ev.stopPropagation() + runODataActions() + }}> {getIcon('odataactions')}
{oDataActionsTitle}
@@ -91,7 +96,8 @@ export function ActionFormatter(props: { data: Content }) { disableRipple={true} disabled={DISABLED_ACTIONS.includes(action.Name)} data-test={`content-context-menu-${action.Name.replace(/\s+/g, '-').toLowerCase()}`} - onClick={() => { + onClick={(ev) => { + ev.stopPropagation() setAnchorEl(null) runAction(action.Name) }}> diff --git a/apps/sensenet/src/components/grid/Grid.tsx b/apps/sensenet/src/components/grid/Grid.tsx index d1a7fb12d..3c46808b5 100644 --- a/apps/sensenet/src/components/grid/Grid.tsx +++ b/apps/sensenet/src/components/grid/Grid.tsx @@ -13,6 +13,7 @@ import { } from 'ag-grid-community' import { AgGridReact } from 'ag-grid-react' import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { ResponsiveContext } from '../../context' import { useLocalization, usePersonalSettings, useSelectionService } from '../../hooks' import { ContentContextMenu } from '../context-menu/content-context-menu' import { DropFileArea } from '../DropFileArea' @@ -28,6 +29,7 @@ export function Grid(props: GridProps const selectionService = useSelectionService() const localization = useLocalization().common const personalSettings = usePersonalSettings() + const device = useContext(ResponsiveContext) const parentContent = useContext(CurrentContentContext) const currentChildren = useContext(CurrentChildrenContext) as GenericContent[] const isCurrentChildrenLoading = useContext(CurrentChildrenIsLoadingContext) @@ -92,6 +94,14 @@ export function Grid(props: GridProps const onRowClicked = (item: RowClickedEvent) => { if (item.data) { props.onActiveItemChange?.(item.data) + + const target = item.event?.target as HTMLElement | null + const isInteractiveTarget = Boolean(target?.closest('button, a, [role="button"], .simpleContextMenu')) + + if (device === 'mobile' && !isInteractiveTarget) { + setLoadingWithMinDuration(true) + props.onParentChange(item.data) + } } } diff --git a/apps/sensenet/src/components/tree/StyledTreeItem.tsx b/apps/sensenet/src/components/tree/StyledTreeItem.tsx index 2dfcb65d2..2a08e4d91 100644 --- a/apps/sensenet/src/components/tree/StyledTreeItem.tsx +++ b/apps/sensenet/src/components/tree/StyledTreeItem.tsx @@ -1,4 +1,4 @@ -import { ListItemIcon, ListItemText } from '@material-ui/core' +import { createStyles, ListItemIcon, ListItemText, makeStyles } from '@material-ui/core' import TreeItem from '@material-ui/lab/TreeItem' import { GenericContent } from '@sensenet/default-content-types' import { useRepository } from '@sensenet/hooks-react' @@ -14,6 +14,27 @@ import { useTreeLoading } from './Contexts/TreeLoadingProvider' import StyledTreeItemProps from './Props/StyledTreeItemProps' import { compareTreeItems, getTreeItemLabel } from './tree-helpers' +const useStyles = makeStyles(() => + createStyles({ + label: { + display: 'flex', + alignItems: 'center', + minWidth: 0, + width: '100%', + overflow: 'hidden', + whiteSpace: 'nowrap', + }, + labelIcon: { + flex: '0 0 auto', + }, + labelText: { + minWidth: 0, + overflow: 'hidden', + color: ({ isDisabled }: { isDisabled: boolean }) => (isDisabled ? 'grey' : undefined), + }, + }), +) + export const StyledTreeItem = ({ contentvalue, activeitempath, @@ -46,6 +67,7 @@ export const StyledTreeItem = ({ const path = contentvalue.Path const isDisabled = !path.includes(enabledPath) + const classes = useStyles({ isDisabled }) const itemId = String(contentvalue.Id) // Track mount state @@ -117,15 +139,16 @@ export const StyledTreeItem = ({ }, [enabledPath, expandItems, itemId, contentvalue.Path, setExpandItems]) const getLabel = () => ( - <> - +
+ - +
) const onIconClick: MouseEventHandler = (event) => { diff --git a/apps/sensenet/webpack.dev.js b/apps/sensenet/webpack.dev.js index 83f1ca650..b0efdf93e 100644 --- a/apps/sensenet/webpack.dev.js +++ b/apps/sensenet/webpack.dev.js @@ -4,6 +4,8 @@ const { customizeArray, mergeWithCustomize } = require('webpack-merge') const common = require('./webpack.common.js') module.exports = (env) => { + const disableDevReload = process.env.DISABLE_DEV_RELOAD === 'true' + return mergeWithCustomize({ customizeArray: customizeArray({ entry: 'replace', @@ -14,6 +16,8 @@ module.exports = (env) => { devtool: 'cheap-module-source-map', devServer: { historyApiFallback: true, + hot: disableDevReload ? false : 'only', + liveReload: !disableDevReload, open: true, allowedHosts: process.env.ALLOWED_HOSTS || 'auto', }, diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e12154e57..967996a92 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -12,6 +12,7 @@ services: - CHOKIDAR_USEPOLLING=true - WATCHPACK_POLLING=true - ALLOWED_HOSTS=all + - DISABLE_DEV_RELOAD=true volumes: - .:/app - sensenet-root-node-modules:/app/node_modules From cf172e1c84bd43ec35b0835ed86059fde87238c9 Mon Sep 17 00:00:00 2001 From: VargaJoe Date: Thu, 2 Jul 2026 13:44:35 +0200 Subject: [PATCH 3/5] fix(admin-ui): tighten mobile tree spacing --- apps/sensenet/src/components/tree/StyledTreeItem.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sensenet/src/components/tree/StyledTreeItem.tsx b/apps/sensenet/src/components/tree/StyledTreeItem.tsx index 2a08e4d91..66777958f 100644 --- a/apps/sensenet/src/components/tree/StyledTreeItem.tsx +++ b/apps/sensenet/src/components/tree/StyledTreeItem.tsx @@ -26,6 +26,8 @@ const useStyles = makeStyles(() => }, labelIcon: { flex: '0 0 auto', + minWidth: 24, + marginRight: 4, }, labelText: { minWidth: 0, From 865b75cccf14d18d84c7016d29b6b89716ca0f80 Mon Sep 17 00:00:00 2001 From: VargaJoe Date: Thu, 2 Jul 2026 16:21:06 +0200 Subject: [PATCH 4/5] fix(admin-ui): improve mobile header controls --- .env.example | 4 +- .../src/components/appbar/desktop-app-bar.tsx | 16 +- .../components/appbar/desktop-nav-menu.tsx | 34 +++- .../command-palette/CommandPalette.tsx | 172 +++++++++++------- .../CommandPaletteHitsContainer.tsx | 6 +- apps/sensenet/webpack.common.js | 1 + apps/sensenet/webpack.dev.js | 4 + docker-compose.dev.yml | 6 +- 8 files changed, 157 insertions(+), 86 deletions(-) diff --git a/.env.example b/.env.example index b4ef6d10b..69d4a88f8 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,8 @@ SENSENET_DEV_PORT=3000 # Development Settings (for hot reload) CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true +DISABLE_VIEW_OPTIONS_MENU=false +WEBPACK_DEV_SERVER_CLIENT_WEB_SOCKET_URL=auto://0.0.0.0:0/ws # Database Settings (if using database service) # DB_PASSWORD=YourPassword123! @@ -28,4 +30,4 @@ WATCHPACK_POLLING=true # Backend API URL (if connecting to external backend) # REACT_APP_SERVICE_URL=https://dev.demo.sensenet.com -# REACT_APP_IDENTITY_SERVER_URL=https://is.demo.sensenet.com \ No newline at end of file +# REACT_APP_IDENTITY_SERVER_URL=https://is.demo.sensenet.com diff --git a/apps/sensenet/src/components/appbar/desktop-app-bar.tsx b/apps/sensenet/src/components/appbar/desktop-app-bar.tsx index d828dbcb1..06a91caa2 100644 --- a/apps/sensenet/src/components/appbar/desktop-app-bar.tsx +++ b/apps/sensenet/src/components/appbar/desktop-app-bar.tsx @@ -25,9 +25,20 @@ const useStyles = makeStyles((theme) => { minHeight: '42px', paddingLeft: '6px', paddingRight: 0, + [theme.breakpoints.down('sm')]: { + paddingLeft: '2px', + }, }, logo: { marginRight: '21px', + [theme.breakpoints.down('sm')]: { + marginRight: '4px', + }, + }, + drawerButton: { + padding: '8px', + marginRight: '2px', + flex: '0 0 auto', }, commandPaletteReplacement: { flex: 1, @@ -46,9 +57,7 @@ const useStyles = makeStyles((theme) => { cursor: 'pointer', }, [theme.breakpoints.down('sm')]: { - maxWidth: '28vw', - marginRight: '0.5rem', - fontSize: '13px', + display: 'none', }, }, }) @@ -122,6 +131,7 @@ export const DesktopAppBar: React.FunctionComponent<{ openDrawer?: () => void }> {personalSettings.drawer.enabled && (personalSettings.drawer.type === 'temporary' || device === 'mobile') ? ( { props.openDrawer && props.openDrawer() }}> diff --git a/apps/sensenet/src/components/appbar/desktop-nav-menu.tsx b/apps/sensenet/src/components/appbar/desktop-nav-menu.tsx index a901d9e1f..49ec68b97 100644 --- a/apps/sensenet/src/components/appbar/desktop-nav-menu.tsx +++ b/apps/sensenet/src/components/appbar/desktop-nav-menu.tsx @@ -22,11 +22,24 @@ const useStyles = makeStyles((theme: Theme) => cursor: 'pointer', marginRight: '16px', padding: '7px', + [theme.breakpoints.down('sm')]: { + marginRight: '6px', + }, }, navMenu: { height: '100%', width: '140px', background: theme.palette.background.default, + flex: '0 0 auto', + [theme.breakpoints.down('sm')]: { + width: '112px', + }, + }, + navMenuCompact: { + width: '76px', + [theme.breakpoints.down('sm')]: { + width: '70px', + }, }, paper: { marginRight: theme.spacing(2), @@ -77,6 +90,7 @@ const useStyles = makeStyles((theme: Theme) => ) export const DesktopNavMenu: FunctionComponent = () => { + const isViewOptionsMenuDisabled = process.env.DISABLE_VIEW_OPTIONS_MENU === 'true' const personalSettings = usePersonalSettings() const injector = useInjector() const classes = useStyles() @@ -134,15 +148,17 @@ export const DesktopNavMenu: FunctionComponent = () => { } return ( -
+
<> - handleToggle(setOpenViewOptions)}> - - + {!isViewOptionsMenuDisabled ? ( + handleToggle(setOpenViewOptions)}> + + + ) : null} {
) : null} - {openViewOptions ? ( + {!isViewOptionsMenuDisabled && openViewOptions ? (
handleClose(setOpenViewOptions)}> diff --git a/apps/sensenet/src/components/command-palette/CommandPalette.tsx b/apps/sensenet/src/components/command-palette/CommandPalette.tsx index f4b093381..bf3ac6a35 100644 --- a/apps/sensenet/src/components/command-palette/CommandPalette.tsx +++ b/apps/sensenet/src/components/command-palette/CommandPalette.tsx @@ -1,4 +1,4 @@ -import { createStyles, IconButton, makeStyles, Tooltip } from '@material-ui/core' +import { createStyles, IconButton, makeStyles } from '@material-ui/core' import Clear from '@material-ui/icons/Clear' import Search from '@material-ui/icons/Search' import { debounce } from '@sensenet/client-utils' @@ -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, useSelectionService, useSnRoute, useTheme } from '../../hooks' +import { useLocalization, useSelectionService, useSnRoute } from '../../hooks' import { CommandProviderManager } from '../../services' import { ContentContextMenu } from '../context-menu/content-context-menu' import { CommandPaletteHitsContainer } from './CommandPaletteHitsContainer' @@ -26,7 +26,7 @@ export interface CommandPaletteItem { parameters?: string[] } -const useStyles = makeStyles(() => { +const useStyles = makeStyles((theme) => { return createStyles({ buttonWrapper: { display: 'flex', @@ -37,6 +37,9 @@ const useStyles = makeStyles(() => { '& .MuiIconButton-root': { color: globals.common.headerText, }, + [theme.breakpoints.down('sm')]: { + flex: '0 0 auto', + }, }, iconButton: { color: globals.common.headerText, @@ -48,6 +51,23 @@ const useStyles = makeStyles(() => { width: '50%', marginRight: '9px', }, + mobileComboBox: { + position: 'absolute', + top: globals.common.headerHeight, + left: 0, + right: 0, + zIndex: theme.zIndex.appBar + 1, + width: 'auto', + marginRight: 0, + padding: '6px 8px', + boxSizing: 'border-box', + backgroundColor: globals.common.headerBackground, + boxShadow: theme.shadows[2], + }, + mobileOpenButton: { + padding: '8px', + marginRight: '2px', + }, input: { color: 'white', backgroundColor: '#016ea5ff', @@ -94,7 +114,6 @@ export const CommandPalette = () => { const [items, setItems] = useState([]) const [inputValue, setInputValue] = useState('') const localization = useLocalization().commandPalette - const theme = useTheme() const history = useHistory() const classes = useStyles() const device = useContext(ResponsiveContext) @@ -237,6 +256,8 @@ export const CommandPalette = () => { } const actionMode = inputValue.startsWith('>') + const isMobile = device === 'mobile' + const showCollapsedMobileSearch = isMobile && !isOpened const actionContextHeader = actionMode ? (
{activeContent ? ( @@ -255,72 +276,87 @@ export const CommandPalette = () => { return (
-
- - theme={{ - suggestionsList: { - listStyle: 'none', - margin: 0, - padding: 0, - }, - inputFocused: { - outlineWidth: 0, - }, - }} - alwaysRenderSuggestions={isOpened} - suggestions={items} - highlightFirstSuggestion={true} - onSuggestionSelected={handleSelectSuggestion} - onSuggestionsFetchRequested={handleSuggestionsFetchRequested} - onSuggestionsClearRequested={() => setItems([])} - getSuggestionValue={(suggestion) => suggestion.primaryText} - renderSuggestion={(suggestion, params) => ( - + {showCollapsedMobileSearch ? ( + setIsOpened(true)} + data-test="command-palette-mobile-open"> + + + ) : ( +
+ + theme={{ + suggestionsList: { + listStyle: 'none', + margin: 0, + padding: 0, + }, + inputFocused: { + outlineWidth: 0, + }, + }} + alwaysRenderSuggestions={isOpened} + suggestions={items} + highlightFirstSuggestion={true} + onSuggestionSelected={handleSelectSuggestion} + onSuggestionsFetchRequested={handleSuggestionsFetchRequested} + onSuggestionsClearRequested={() => setItems([])} + getSuggestionValue={(suggestion) => suggestion.primaryText} + renderSuggestion={(suggestion, params) => ( + + )} + renderSuggestionsContainer={(params) => ( + + )} + inputProps={{ + className: `${classes.input} ${inputValue ? classes.inputOpened : ''}`, + value: inputValue, + placeholder: localization.title, + onChange: (_ev, changeEvent) => { + setInputValue(changeEvent.newValue) + }, + id: 'CommandBoxInput', + spellCheck: false, + onBlur: () => { + if (!isContextMenuInteractingRef.current) { + setIsOpened(false) + } + }, + }} + /> + {!inputValue && ( + ev.preventDefault()}> + + )} - renderSuggestionsContainer={(params) => ( - + {inputValue && ( + { + setInputValue('') + setItems([]) + handleSuggestionsFetchRequested({ value: '', reason: 'input-changed' }) + }} + onMouseDown={(ev) => ev.preventDefault()}> + + )} - inputProps={{ - className: `${classes.input} ${inputValue ? classes.inputOpened : ''}`, - value: inputValue, - placeholder: 'Search', - onChange: (_ev, changeEvent) => { - setInputValue(changeEvent.newValue) - }, - id: 'CommandBoxInput', - spellCheck: false, - onBlur: () => { - if (!isContextMenuInteractingRef.current) { - setIsOpened(false) - } - }, - }} - /> - {!inputValue && ( - - - - )} - {inputValue && ( - { - setInputValue('') - setItems([]) - handleSuggestionsFetchRequested({ value: '', reason: 'input-changed' }) - }} - onMouseDown={(ev) => ev.preventDefault()}> - - - )} -
+
+ )} {contextMenuContent ? ( <> {options.header} diff --git a/apps/sensenet/webpack.common.js b/apps/sensenet/webpack.common.js index 5fbcf7c31..54b6befa5 100644 --- a/apps/sensenet/webpack.common.js +++ b/apps/sensenet/webpack.common.js @@ -60,6 +60,7 @@ module.exports = { new webpack.EnvironmentPlugin({ APP_VERSION: require('./package.json').version, AUTH_TYPE: process.env.AUTH_TYPE || 'SNAuth', // Default to SNAuth if not specified + DISABLE_VIEW_OPTIONS_MENU: process.env.DISABLE_VIEW_OPTIONS_MENU || 'false', }), new MonacoWebpackPlugin({ languages: ['json', 'xml', 'html', 'javascript', 'markdown'], diff --git a/apps/sensenet/webpack.dev.js b/apps/sensenet/webpack.dev.js index b0efdf93e..841f6e6f2 100644 --- a/apps/sensenet/webpack.dev.js +++ b/apps/sensenet/webpack.dev.js @@ -5,6 +5,7 @@ const common = require('./webpack.common.js') module.exports = (env) => { const disableDevReload = process.env.DISABLE_DEV_RELOAD === 'true' + const clientWebSocketURL = process.env.WEBPACK_DEV_SERVER_CLIENT_WEB_SOCKET_URL || 'auto://0.0.0.0:0/ws' return mergeWithCustomize({ customizeArray: customizeArray({ @@ -20,6 +21,9 @@ module.exports = (env) => { liveReload: !disableDevReload, open: true, allowedHosts: process.env.ALLOWED_HOSTS || 'auto', + client: { + webSocketURL: clientWebSocketURL, + }, }, output: { filename: 'static/js/[name].js', diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 967996a92..70086c419 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -5,7 +5,7 @@ services: dockerfile: Dockerfile container_name: sensenet-dev ports: - - "4052:8080" + - '4052:8080' environment: - NODE_ENV=development - AUTH_TYPE=SNAuth @@ -13,13 +13,15 @@ services: - WATCHPACK_POLLING=true - ALLOWED_HOSTS=all - DISABLE_DEV_RELOAD=true + - WEBPACK_DEV_SERVER_CLIENT_WEB_SOCKET_URL=${WEBPACK_DEV_SERVER_CLIENT_WEB_SOCKET_URL:-auto://0.0.0.0:0/ws} + - DISABLE_VIEW_OPTIONS_MENU=${DISABLE_VIEW_OPTIONS_MENU:-false} volumes: - .:/app - sensenet-root-node-modules:/app/node_modules - sensenet-app-node-modules:/app/apps/sensenet/node_modules command: sh -c "yarn snapp start" healthcheck: - test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8080/" ] + test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://127.0.0.1:8080/'] interval: 30s timeout: 3s start_period: 120s From eb62ba927f88f1bf010061beda6f84263dbd7d90 Mon Sep 17 00:00:00 2001 From: VargaJoe Date: Thu, 2 Jul 2026 17:39:45 +0200 Subject: [PATCH 5/5] fix(admin-ui): align mobile header actions --- .../sensenet/src/components/command-palette/CommandPalette.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/sensenet/src/components/command-palette/CommandPalette.tsx b/apps/sensenet/src/components/command-palette/CommandPalette.tsx index bf3ac6a35..33df097e5 100644 --- a/apps/sensenet/src/components/command-palette/CommandPalette.tsx +++ b/apps/sensenet/src/components/command-palette/CommandPalette.tsx @@ -37,9 +37,6 @@ const useStyles = makeStyles((theme) => { '& .MuiIconButton-root': { color: globals.common.headerText, }, - [theme.breakpoints.down('sm')]: { - flex: '0 0 auto', - }, }, iconButton: { color: globals.common.headerText,