diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx index 6fb5c0e790bf..e56d006efee3 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx @@ -24,6 +24,7 @@ import { screen, } from 'spec/helpers/testing-library'; import { FeatureFlag } from '@superset-ui/core'; +import { supersetTheme } from '@apache-superset/core/theme'; import { OPEN_FILTER_BAR_WIDTH, CLOSED_FILTER_BAR_WIDTH, @@ -487,6 +488,47 @@ test('should render ParentSize wrapper with height 100% for tabs', async () => { expect(tabPanels.length).toBeGreaterThan(0); }); +test('should apply min-height to the top-level tab drop target so tabs can be dropped on dashboards with content', () => { + (useStoredSidebarWidth as jest.Mock).mockImplementation(() => [ + 100, + jest.fn(), + ]); + (fetchFaveStar as jest.Mock).mockReturnValue({ type: 'mock-action' }); + (setActiveTab as jest.Mock).mockReturnValue({ type: 'mock-action' }); + + const { getByTestId } = render(, { + useRedux: true, + store: storeWithState({ + ...mockState, + dashboardLayout: undoableDashboardLayout, + dashboardState: { ...mockState.dashboardState, editMode: true }, + }), + useDnd: true, + useTheme: true, + }); + + const headerWrapper = getByTestId('dashboard-header-wrapper'); + + // The Droppable inside the header should have the empty-droptarget class + // when there are no top-level tabs and edit mode is active. Without this + // class (and its associated min-height CSS rule), the drop target has zero + // height and users cannot drag tabs onto dashboards that already have + // content. + const droptarget = headerWrapper.querySelector('.empty-droptarget'); + expect(droptarget).toBeInTheDocument(); + + // Verify the StyledHeader CSS defines a non-zero min-height for + // .empty-droptarget, derived from theme.sizeUnit * 4 to stay in sync + // with the source rule in DashboardBuilder.tsx. + expect(headerWrapper).toHaveStyleRule( + 'min-height', + `${supersetTheme.sizeUnit * 4}px`, + { + target: '.empty-droptarget', + }, + ); +}); + test('should maintain layout when switching between tabs', async () => { (useStoredSidebarWidth as jest.Mock).mockImplementation(() => [ 100, diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index 8906ddb316e4..2013ff074cb5 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -100,6 +100,10 @@ const StyledHeader = styled.div<{ filterBarWidth: number }>` z-index: 99; max-width: calc(100vw - ${filterBarWidth}px); + .empty-droptarget { + min-height: ${theme.sizeUnit * 4}px; + } + .empty-droptarget:before { position: absolute; content: ''; diff --git a/superset-frontend/src/types/emotion-jest.d.ts b/superset-frontend/src/types/emotion-jest.d.ts new file mode 100644 index 000000000000..c6d15cfb2ddd --- /dev/null +++ b/superset-frontend/src/types/emotion-jest.d.ts @@ -0,0 +1,20 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +///