This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch mobile-dashboard-support in repository https://gitbox.apache.org/repos/asf/superset.git
commit 6b84732fcc56e00295a7eb98c2e83a12696325c3 Author: Evan Rusackas <[email protected]> AuthorDate: Tue Jan 13 10:55:51 2026 -0800 feat(mobile): Add filter drawer to Dashboard List page - Add mobile filter drawer that slides in from the left with search/sort options - Extend SubMenu with leftIcon/rightIcon props for mobile header actions - Add mobileFiltersOpen/setMobileFiltersOpen props to ListView component - Increase card grid-gap on mobile for better spacing - Hide kebab menu on cards in mobile consumption mode - Change mobile filters button style to link for consistency Co-Authored-By: Claude Opus 4.5 <[email protected]> --- .../src/components/ListView/CardCollection.tsx | 1 + .../src/components/ListView/ListView.tsx | 96 ++++++++++++++++++++-- .../src/dashboard/components/Header/index.jsx | 3 +- superset-frontend/src/features/home/SubMenu.tsx | 20 +++-- .../src/pages/DashboardList/index.tsx | 27 +++++- superset-frontend/src/views/CRUD/utils.tsx | 7 ++ 6 files changed, 138 insertions(+), 16 deletions(-) diff --git a/superset-frontend/src/components/ListView/CardCollection.tsx b/superset-frontend/src/components/ListView/CardCollection.tsx index 3566cb9182..d610b5d2fe 100644 --- a/superset-frontend/src/components/ListView/CardCollection.tsx +++ b/superset-frontend/src/components/ListView/CardCollection.tsx @@ -46,6 +46,7 @@ const CardContainer = styled.div<{ showThumbnails?: boolean }>` /* Full-width cards on mobile */ @media (max-width: 767px) { grid-template-columns: 1fr; + grid-gap: ${theme.sizeUnit * 4}px; padding-left: ${theme.sizeUnit * 4}px; padding-right: ${theme.sizeUnit * 4}px; } diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index 7fdc5ee705..b39a16884a 100644 --- a/superset-frontend/src/components/ListView/ListView.tsx +++ b/superset-frontend/src/components/ListView/ListView.tsx @@ -25,6 +25,7 @@ import BulkTagModal from 'src/features/tags/BulkTagModal'; import { Button, Checkbox, + Drawer, Icons, EmptyState, Loading, @@ -62,6 +63,14 @@ const ListViewStyles = styled.div` flex-wrap: wrap; column-gap: ${theme.sizeUnit * 7}px; row-gap: ${theme.sizeUnit * 4}px; + + /* Hide desktop filters and sort on mobile when mobile drawer is used */ + @media (max-width: 767px) { + .desktop-filters, + .desktop-sort { + display: none; + } + } } } @@ -196,6 +205,30 @@ const EmptyWrapper = styled.div` `} `; +const MobileFilterDrawerContent = styled.div` + ${({ theme }) => ` + display: flex; + flex-direction: column; + gap: ${theme.sizeUnit * 4}px; + padding: ${theme.sizeUnit * 2}px; + + /* Make filter inputs stack vertically and full-width */ + > * { + width: 100%; + } + + /* Override inline filter styling for vertical layout */ + .filter-container { + width: 100%; + } + + input[type="text"], + .ant-select { + width: 100% !important; + } + `} +`; + const ViewModeToggle = ({ mode, setMode, @@ -261,6 +294,12 @@ export interface ListViewProps<T extends object = any> { columnsForWrapText?: string[]; enableBulkTag?: boolean; bulkTagResourceName?: string; + /** Whether mobile filters drawer is open (controlled externally) */ + mobileFiltersOpen?: boolean; + /** Callback to set mobile filters drawer open state */ + setMobileFiltersOpen?: (open: boolean) => void; + /** Title for the mobile filters drawer */ + mobileFiltersDrawerTitle?: string; } export function ListView<T extends object = any>({ @@ -290,6 +329,9 @@ export function ListView<T extends object = any>({ bulkTagResourceName, addSuccessToast, addDangerToast, + mobileFiltersOpen = false, + setMobileFiltersOpen, + mobileFiltersDrawerTitle, }: ListViewProps<T>) { const { getTableProps, @@ -377,7 +419,8 @@ export function ListView<T extends object = any>({ <ViewModeToggle mode={viewMode} setMode={setViewMode} /> )} <div className="controls" data-test="filters-select"> - {filterable && ( + {/* On mobile, filters are shown in drawer; on desktop, show inline */} + {filterable && !setMobileFiltersOpen && ( <FilterControls ref={filterControlsRef} filters={filters} @@ -385,12 +428,27 @@ export function ListView<T extends object = any>({ updateFilterValue={applyFilterValue} /> )} + {filterable && setMobileFiltersOpen && ( + <> + {/* Desktop: show inline filters */} + <div className="desktop-filters"> + <FilterControls + ref={filterControlsRef} + filters={filters} + internalFilters={internalFilters} + updateFilterValue={applyFilterValue} + /> + </div> + </> + )} {viewMode === 'card' && cardSortSelectOptions && ( - <CardSortSelect - initialSort={sortBy} - onChange={(value: SortColumn[]) => setSortBy(value)} - options={cardSortSelectOptions} - /> + <div className="desktop-sort"> + <CardSortSelect + initialSort={sortBy} + onChange={(value: SortColumn[]) => setSortBy(value)} + options={cardSortSelectOptions} + /> + </div> )} </div> </div> @@ -524,6 +582,32 @@ export function ListView<T extends object = any>({ )} </div> </div> + + {/* Mobile filter drawer */} + {filterable && setMobileFiltersOpen && ( + <Drawer + title={mobileFiltersDrawerTitle || t('Search')} + placement="left" + onClose={() => setMobileFiltersOpen(false)} + open={mobileFiltersOpen} + width={300} + > + <MobileFilterDrawerContent> + <FilterControls + filters={filters} + internalFilters={internalFilters} + updateFilterValue={applyFilterValue} + /> + {cardSortSelectOptions && ( + <CardSortSelect + initialSort={sortBy} + onChange={(value: SortColumn[]) => setSortBy(value)} + options={cardSortSelectOptions} + /> + )} + </MobileFilterDrawerContent> + </Drawer> + )} </ListViewStyles> ); } diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index 86242a1387..77e6352603 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -817,8 +817,7 @@ const Header = ({ onOpenMobileFilters }) => { leftPanelItems={ onOpenMobileFilters && ( <Button - css={menuTriggerStyles} - buttonStyle="tertiary" + buttonStyle="link" aria-label={t('Open filters')} onClick={onOpenMobileFilters} data-test="mobile-filters-trigger" diff --git a/superset-frontend/src/features/home/SubMenu.tsx b/superset-frontend/src/features/home/SubMenu.tsx index b01aa00506..9e851f37c7 100644 --- a/superset-frontend/src/features/home/SubMenu.tsx +++ b/superset-frontend/src/features/home/SubMenu.tsx @@ -106,15 +106,17 @@ const StyledHeader = styled.div<{ backgroundColor?: string }>` padding: 10px 0; } @media (max-width: 767px) { - .header, - .nav-right { + .header { position: relative; - margin-left: ${({ theme }) => theme.sizeUnit * 2}px; + margin-left: 0; + flex: 1; + text-align: center; } - /* Hide add buttons (secondary style) on mobile */ - .nav-right .superset-button-secondary { - display: none; + /* Hide all buttons on mobile */ + .nav-right, + .nav-right-collapse { + display: none !important; } /* Compact horizontal tabs on mobile (segmented-control style) */ @@ -176,6 +178,10 @@ export interface SubMenuProps { color?: string; dropDownLinks?: Array<MenuObjectProps>; backgroundColor?: string; + /** Left icon for mobile - shown before the header */ + leftIcon?: ReactNode; + /** Right icon for mobile - shown after the header */ + rightIcon?: ReactNode; } const { SubMenu } = MainNav; @@ -223,7 +229,9 @@ const SubMenuComponent: FunctionComponent<SubMenuProps> = props => { return ( <StyledHeader backgroundColor={props.backgroundColor}> <Row className="menu" role="navigation"> + {props.leftIcon} {props.name && <div className="header">{props.name}</div>} + {props.rightIcon} <Menu mode={showMenu} disabledOverflow diff --git a/superset-frontend/src/pages/DashboardList/index.tsx b/superset-frontend/src/pages/DashboardList/index.tsx index 84f2cc7f38..68a1f80578 100644 --- a/superset-frontend/src/pages/DashboardList/index.tsx +++ b/superset-frontend/src/pages/DashboardList/index.tsx @@ -22,7 +22,7 @@ import { SupersetClient, t, } from '@superset-ui/core'; -import { styled } from '@apache-superset/core/ui'; +import { styled, css, useTheme } from '@apache-superset/core/ui'; import { useSelector } from 'react-redux'; import { useState, useMemo, useCallback } from 'react'; import { Link } from 'react-router-dom'; @@ -34,6 +34,7 @@ import { } from 'src/views/CRUD/utils'; import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks'; import { + Button, CertifiedBadge, ConfirmStatusChange, DeleteModal, @@ -146,6 +147,8 @@ const DASHBOARD_COLUMNS_TO_FETCH = [ function DashboardList(props: DashboardListProps) { const { addDangerToast, addSuccessToast, user } = props; const { md: isNotMobile } = Grid.useBreakpoint(); + const theme = useTheme(); + const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); const { roles } = useSelector<any, UserWithPermissionsAndRoles>( state => state.user, ); @@ -730,7 +733,24 @@ function DashboardList(props: DashboardListProps) { } return ( <> - <SubMenu name={t('Dashboards')} buttons={subMenuButtons} /> + <SubMenu + name={t('Dashboards')} + buttons={subMenuButtons} + leftIcon={ + !isNotMobile ? ( + <Button + buttonStyle="link" + onClick={() => setMobileFiltersOpen(true)} + css={css` + padding: 0; + margin-right: ${theme.sizeUnit * 2}px; + `} + > + <Icons.SearchOutlined iconSize="l" /> + </Button> + ) : undefined + } + /> <ConfirmStatusChange title={t('Please confirm')} description={t( @@ -822,6 +842,9 @@ function DashboardList(props: DashboardListProps) { forceViewMode={!isNotMobile ? 'card' : undefined} enableBulkTag={enableBulkTag} bulkTagResourceName="dashboard" + mobileFiltersOpen={mobileFiltersOpen} + setMobileFiltersOpen={setMobileFiltersOpen} + mobileFiltersDrawerTitle={t('Search Dashboards')} /> </> ); diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 07f004d9c8..9b2081b315 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -397,6 +397,13 @@ export const CardStyles = styled.div` /* Height is calculated based on 300px width, to keep the same aspect ratio as the 800*450 thumbnails */ height: 168px; } + + /* Hide kebab menu on mobile - consumption mode only */ + @media (max-width: 767px) { + .ant-dropdown-trigger { + display: none; + } + } `; export /* eslint-disable no-underscore-dangle */
