This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch feat-granular-theming in repository https://gitbox.apache.org/repos/asf/superset.git
commit eb77167bae5dc0a274332aa1e11185f6fcec62c1 Author: Evan Rusackas <[email protected]> AuthorDate: Mon Dec 15 16:50:34 2025 -0800 feat(dashboard): add ComponentHeaderControls for consistent component menus Create a generic menu component for dashboard components (Markdown, Row, Column, Tab) that provides a consistent vertical dots menu pattern, matching the existing SliceHeaderControls used for charts. New files: - ComponentHeaderControls/index.tsx: Main component with: - NoAnimationDropdown + Menu pattern from @superset-ui/core - ComponentMenuKeys enum for standard actions (Delete, ApplyTheme, etc.) - Configurable visibility (editMode, showInViewMode) - ComponentHeaderControls/useComponentMenuItems.tsx: Helper hook that: - Builds standard menu items (theme selection, delete) - Supports custom items before/after standard items - Shows applied theme name in menu This is the foundation for adding granular theming to all dashboard components with a consistent UI pattern. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> --- docs/GRANULAR_THEMING_PLAN.md | 19 ++ .../menu/ComponentHeaderControls/index.tsx | 193 +++++++++++++++++++++ .../useComponentMenuItems.tsx | 130 ++++++++++++++ 3 files changed, 342 insertions(+) diff --git a/docs/GRANULAR_THEMING_PLAN.md b/docs/GRANULAR_THEMING_PLAN.md index 53d76df156c..47b0d5e09c9 100644 --- a/docs/GRANULAR_THEMING_PLAN.md +++ b/docs/GRANULAR_THEMING_PLAN.md @@ -344,3 +344,22 @@ _Ongoing notes as we implement..._ - Decided on modal with live preview for theme selection - Clarified color scheme (data) vs theme (UI) separation +### Session 1 - Implementation Started +- Created `ComponentHeaderControls` component at: + `src/dashboard/components/menu/ComponentHeaderControls/index.tsx` + - Generic vertical dots menu matching SliceHeaderControls pattern + - Uses NoAnimationDropdown + Menu from @superset-ui/core + - Configurable menu items, edit mode visibility + - Exports `ComponentMenuKeys` enum for standard actions + +- Created `useComponentMenuItems` hook at: + `src/dashboard/components/menu/ComponentHeaderControls/useComponentMenuItems.tsx` + - Builds standard menu items (theme, delete) + - Supports custom items before/after standard items + - Shows "Change theme (name)" when theme applied + +**Next Steps:** +1. Integrate ComponentHeaderControls into Markdown component +2. Test with simple Edit/Preview + Theme + Delete menu +3. Remove old MarkdownModeDropdown + diff --git a/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/index.tsx new file mode 100644 index 00000000000..0eb91d1fd8f --- /dev/null +++ b/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/index.tsx @@ -0,0 +1,193 @@ +/** + * 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. + */ +import { useState, Key, MouseEvent, KeyboardEvent } from 'react'; +import { t } from '@superset-ui/core'; +import { css, useTheme } from '@apache-superset/core/ui'; +import { Menu, MenuItem } from '@superset-ui/core/components/Menu'; +import { + NoAnimationDropdown, + Button, +} from '@superset-ui/core/components'; +import { Icons } from '@superset-ui/core/components/Icons'; + +/** + * Standard menu keys for dashboard components. + * Components can use these standard keys or define custom ones. + */ +export enum ComponentMenuKeys { + // Common actions + Delete = 'delete', + Edit = 'edit', + + // Theme actions + ApplyTheme = 'apply-theme', + ClearTheme = 'clear-theme', + + // Markdown-specific + EditContent = 'edit-content', + PreviewContent = 'preview-content', + + // Row/Column-specific + BackgroundStyle = 'background-style', + + // Tab-specific + RenameTab = 'rename-tab', +} + +// Re-export MenuItem type for convenience - allows both keyed items and dividers +export type ComponentMenuItem = MenuItem; + +export interface ComponentHeaderControlsProps { + /** Unique identifier for the component */ + componentId: string; + + /** Array of menu items to display */ + menuItems: ComponentMenuItem[]; + + /** Callback when a menu item is clicked */ + onMenuClick: (key: string, domEvent: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>) => void; + + /** Whether the component is in edit mode */ + editMode?: boolean; + + /** Whether to show the menu even in view mode */ + showInViewMode?: boolean; + + /** Z-index for the dropdown overlay */ + zIndex?: number; + + /** Additional CSS class for the trigger button */ + className?: string; + + /** Whether the menu is disabled */ + disabled?: boolean; +} + +const VerticalDotsTrigger = () => { + const theme = useTheme(); + return ( + <Icons.EllipsisOutlined + css={css` + transform: rotate(90deg); + &:hover { + cursor: pointer; + } + `} + iconSize="l" + iconColor={theme.colorTextLabel} + className="component-menu-trigger" + /> + ); +}; + +/** + * A standardized menu component for dashboard components (Markdown, Row, Column, Tab). + * + * Provides a consistent vertical dots menu pattern similar to SliceHeaderControls, + * but generic enough to be used across all dashboard component types. + * + * Usage: + * ```tsx + * <ComponentHeaderControls + * componentId="MARKDOWN-123" + * menuItems={[ + * { key: ComponentMenuKeys.Edit, label: t('Edit') }, + * { key: ComponentMenuKeys.ApplyTheme, label: t('Apply theme') }, + * { type: 'divider' }, + * { key: ComponentMenuKeys.Delete, label: t('Delete'), danger: true }, + * ]} + * onMenuClick={(key) => handleMenuAction(key)} + * editMode={editMode} + * /> + * ``` + */ +const ComponentHeaderControls = ({ + componentId, + menuItems, + onMenuClick, + editMode = false, + showInViewMode = false, + zIndex = 99, + className, + disabled = false, +}: ComponentHeaderControlsProps) => { + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + const theme = useTheme(); + + // Don't render if not in edit mode and showInViewMode is false + if (!editMode && !showInViewMode) { + return null; + } + + const handleMenuClick = ({ + key, + domEvent, + }: { + key: Key; + domEvent: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>; + }) => { + onMenuClick(String(key), domEvent); + setIsDropdownVisible(false); + }; + + const dropdownOverlayStyle = { + zIndex, + animationDuration: '0s', + }; + + return ( + <NoAnimationDropdown + popupRender={() => ( + <Menu + onClick={handleMenuClick} + data-test={`component-menu-${componentId}`} + id={`component-menu-${componentId}`} + selectable={false} + items={menuItems} + /> + )} + overlayStyle={dropdownOverlayStyle} + trigger={['click']} + placement="bottomRight" + open={isDropdownVisible} + onOpenChange={visible => setIsDropdownVisible(visible)} + disabled={disabled} + > + <Button + id={`${componentId}-controls`} + buttonStyle="link" + aria-label={t('More Options')} + aria-haspopup="true" + className={className} + css={css` + padding: ${theme.sizeUnit}px; + opacity: 0.7; + transition: opacity 0.2s; + &:hover { + opacity: 1; + } + `} + > + <VerticalDotsTrigger /> + </Button> + </NoAnimationDropdown> + ); +}; + +export default ComponentHeaderControls; diff --git a/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/useComponentMenuItems.tsx b/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/useComponentMenuItems.tsx new file mode 100644 index 00000000000..de2acfde799 --- /dev/null +++ b/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/useComponentMenuItems.tsx @@ -0,0 +1,130 @@ +/** + * 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. + */ +import { useMemo } from 'react'; +import { t } from '@superset-ui/core'; +import { Icons } from '@superset-ui/core/components/Icons'; +import { css } from '@apache-superset/core/ui'; +import { MenuItem } from '@superset-ui/core/components/Menu'; +import { ComponentMenuKeys } from './index'; + +const dropdownIconsStyles = css` + &&.anticon > .anticon:first-child { + margin-right: 0; + vertical-align: 0; + } +`; + +export interface UseComponentMenuItemsOptions { + /** Whether to include theme menu item */ + includeTheme?: boolean; + + /** Whether to include delete menu item */ + includeDelete?: boolean; + + /** Whether the component has a theme applied */ + hasThemeApplied?: boolean; + + /** Name of the applied theme (for display) */ + appliedThemeName?: string; + + /** Additional custom menu items to include before standard items */ + customItems?: MenuItem[]; + + /** Additional custom menu items to include after standard items */ + customItemsAfter?: MenuItem[]; +} + +/** + * Hook to build standard menu items for dashboard components. + * + * Provides consistent menu item structure across all component types, + * with optional theme selection and delete actions. + */ +export function useComponentMenuItems({ + includeTheme = true, + includeDelete = true, + hasThemeApplied = false, + appliedThemeName, + customItems = [], + customItemsAfter = [], +}: UseComponentMenuItemsOptions = {}): MenuItem[] { + return useMemo(() => { + const items: MenuItem[] = []; + + // Add custom items first + if (customItems.length > 0) { + items.push(...customItems); + } + + // Add theme items + if (includeTheme) { + if (items.length > 0) { + items.push({ type: 'divider' }); + } + + items.push({ + key: ComponentMenuKeys.ApplyTheme, + label: hasThemeApplied + ? t('Change theme (%s)', appliedThemeName || t('Custom')) + : t('Apply theme'), + icon: <Icons.BgColorsOutlined css={dropdownIconsStyles} />, + }); + + if (hasThemeApplied) { + items.push({ + key: ComponentMenuKeys.ClearTheme, + label: t('Clear theme'), + icon: <Icons.ClearOutlined css={dropdownIconsStyles} />, + }); + } + } + + // Add custom items after theme + if (customItemsAfter.length > 0) { + if (items.length > 0) { + items.push({ type: 'divider' }); + } + items.push(...customItemsAfter); + } + + // Add delete as last item + if (includeDelete) { + if (items.length > 0) { + items.push({ type: 'divider' }); + } + items.push({ + key: ComponentMenuKeys.Delete, + label: t('Delete'), + icon: <Icons.DeleteOutlined css={dropdownIconsStyles} />, + danger: true, + }); + } + + return items; + }, [ + includeTheme, + includeDelete, + hasThemeApplied, + appliedThemeName, + customItems, + customItemsAfter, + ]); +} + +export default useComponentMenuItems;
