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 */

Reply via email to