diff --git a/react-components/package.json b/react-components/package.json index f1a19f59a87..0bba15913c4 100644 --- a/react-components/package.json +++ b/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@cognite/reveal-react-components", - "version": "0.60.1", + "version": "0.60.2", "exports": { ".": { "import": "./dist/index.js", diff --git a/react-components/src/architecture/base/commands/DividerCommand.ts b/react-components/src/architecture/base/commands/DividerCommand.ts new file mode 100644 index 00000000000..db05b5aaae0 --- /dev/null +++ b/react-components/src/architecture/base/commands/DividerCommand.ts @@ -0,0 +1,10 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { RenderTargetCommand } from './RenderTargetCommand'; + +export class DividerCommand extends RenderTargetCommand { + public override get isVisible(): boolean { + return true; + } +} diff --git a/react-components/src/architecture/base/commands/mocks/MockCheckableCommand.ts b/react-components/src/architecture/base/commands/mocks/MockCheckableCommand.ts index f22f251b5ac..968a1bb1039 100644 --- a/react-components/src/architecture/base/commands/mocks/MockCheckableCommand.ts +++ b/react-components/src/architecture/base/commands/mocks/MockCheckableCommand.ts @@ -28,4 +28,8 @@ export class MockCheckableCommand extends RenderTargetCommand { this.value = !this.value; return true; } + + public override get isToggle(): boolean { + return true; + } } diff --git a/react-components/src/architecture/base/concreteCommands/PointCloudDividerCommand.ts b/react-components/src/architecture/base/concreteCommands/PointCloudDividerCommand.ts new file mode 100644 index 00000000000..99dc3fb336e --- /dev/null +++ b/react-components/src/architecture/base/concreteCommands/PointCloudDividerCommand.ts @@ -0,0 +1,15 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type BaseCommand } from '../commands/BaseCommand'; +import { DividerCommand } from '../commands/DividerCommand'; + +export class PointCloudDividerCommand extends DividerCommand { + public override get isVisible(): boolean { + return this.renderTarget.getPointClouds().next().value !== undefined; + } + + public override equals(other: BaseCommand): boolean { + return this === other; + } +} diff --git a/react-components/src/architecture/base/concreteCommands/SettingsCommand.ts b/react-components/src/architecture/base/concreteCommands/SettingsCommand.ts index 00f1c4fe6ba..4a17a20d603 100644 --- a/react-components/src/architecture/base/concreteCommands/SettingsCommand.ts +++ b/react-components/src/architecture/base/concreteCommands/SettingsCommand.ts @@ -10,6 +10,7 @@ import { SetPointShapeCommand } from './SetPointShapeCommand'; import { PointCloudFilterCommand } from './PointCloudFilterCommand'; import { type TranslateKey } from '../utilities/TranslateKey'; import { type IconName } from '../utilities/IconName'; +import { PointCloudDividerCommand } from './PointCloudDividerCommand'; export class SettingsCommand extends BaseSettingsCommand { // ================================================== @@ -20,6 +21,7 @@ export class SettingsCommand extends BaseSettingsCommand { super(); this.add(new SetQualityCommand()); + this.add(new PointCloudDividerCommand()); this.add(new SetPointSizeCommand()); this.add(new SetPointColorTypeCommand()); this.add(new SetPointShapeCommand()); diff --git a/react-components/src/components/Architecture/DropdownButton.tsx b/react-components/src/components/Architecture/DropdownButton.tsx index 8fa5cb49fd4..6413bd74cd5 100644 --- a/react-components/src/components/Architecture/DropdownButton.tsx +++ b/react-components/src/components/Architecture/DropdownButton.tsx @@ -3,33 +3,27 @@ */ import { Button, Tooltip as CogsTooltip, ChevronDownIcon, ChevronUpIcon } from '@cognite/cogs.js'; -import { Menu } from '@cognite/cogs-lab'; +import { Menu, Option, Select } from '@cognite/cogs-lab'; import { useCallback, useEffect, useMemo, useState, type ReactElement, - type MouseEvent + type SetStateAction, + type Dispatch } from 'react'; import { useTranslation } from '../i18n/I18n'; import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; import { useRenderTarget } from '../RevealCanvas/ViewerContext'; import { type BaseOptionCommand } from '../../architecture/base/commands/BaseOptionCommand'; -import { - getButtonType, - getDefaultCommand, - getFlexDirection, - getTooltipPlacement, - getIcon -} from './utilities'; +import { getButtonType, getDefaultCommand, getTooltipPlacement, getIcon } from './utilities'; import { LabelWithShortcut } from './LabelWithShortcut'; import { type TranslateDelegate } from '../../architecture/base/utilities/TranslateKey'; import { DEFAULT_PADDING, OPTION_MIN_WIDTH } from './constants'; -import { TOOLBAR_HORIZONTAL_PANEL_OFFSET } from '../constants'; -import { offset } from '@floating-ui/dom'; +import styled from 'styled-components'; export const DropdownButton = ({ inputCommand, @@ -41,7 +35,6 @@ export const DropdownButton = ({ usedInSettings?: boolean; }): ReactElement => { const renderTarget = useRenderTarget(); - const { t } = useTranslation(); const command = useMemo( () => getDefaultCommand(inputCommand, renderTarget), [] @@ -66,52 +59,77 @@ export const DropdownButton = ({ }; }, [command]); - if (!isVisible || command.children === undefined) { - return <>; - } - const placement = getTooltipPlacement(isHorizontal); - const label = usedInSettings ? undefined : command.getLabel(t); - const flexDirection = getFlexDirection(false); // Always vertical - const children = command.children; + return usedInSettings ? ( + + ) : ( + + ); +}; + +const DropdownElement = ({ + command, + isVisible, + isOpen, + setOpen, + isEnabled, + isHorizontal, + uniqueId +}: { + command: BaseOptionCommand; + isVisible: boolean; + isOpen: boolean; + setOpen: Dispatch>; + isEnabled: boolean; + isHorizontal: boolean; + uniqueId: number; +}): ReactElement => { + const { t } = useTranslation(); + const label = command.getLabel(t); const selectedLabel = command.selectedChild?.getLabel(t); + const placement = getTooltipPlacement(isHorizontal); + const OpenButtonIcon = isOpen ? ChevronUpIcon : ChevronDownIcon; + if (!isVisible || command.children === undefined) { + return <>; + } + return ( { setOpen(open); }} hideOnSelect={true} appendTo={'parent'} - placement={usedInSettings ? 'bottom-end' : 'auto-start'} + placement={'bottom-start'} renderTrigger={(props: any) => ( } - disabled={usedInSettings || label === undefined} + disabled={label === undefined} appendTo={document.body} placement={placement}> )}> - {children.map((child, _index): ReactElement => { - return createMenuItem(child, t); - })} + {command.children.map((child) => createMenuItem(child, t))} ); }; +const MenuItemWithDropdown = ({ + command, + isVisible +}: { + command: BaseOptionCommand; + isVisible: boolean; +}): ReactElement => { + const { t } = useTranslation(); + const label = command.getLabel(t); + + if (!isVisible || command.children === undefined) { + return <>; + } + + return ( + + + + + ); +}; + function createMenuItem(command: BaseCommand, t: TranslateDelegate): ReactElement { return ( ); } + +const StyledDropdownRow = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 8; + minwidth: ${OPTION_MIN_WIDTH}; + padding: ${DEFAULT_PADDING}; +`; diff --git a/react-components/src/components/Architecture/FilterButton.tsx b/react-components/src/components/Architecture/FilterButton.tsx index 10a0f6ac1c8..3e1a7ec1313 100644 --- a/react-components/src/components/Architecture/FilterButton.tsx +++ b/react-components/src/components/Architecture/FilterButton.tsx @@ -8,30 +8,27 @@ import { useMemo, useState, type ReactElement, - type MouseEvent + type MouseEvent, + type Dispatch, + type SetStateAction } from 'react'; -import { Button, Tooltip as CogsTooltip, ChevronUpIcon, ChevronDownIcon } from '@cognite/cogs.js'; -import { Menu } from '@cognite/cogs-lab'; +import { Button, ChevronDownIcon, ChevronUpIcon, Tooltip as CogsTooltip } from '@cognite/cogs.js'; +import { Menu, SelectPanel } from '@cognite/cogs-lab'; import { useTranslation } from '../i18n/I18n'; import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; import { useRenderTarget } from '../RevealCanvas/ViewerContext'; -import { - getButtonType, - getDefaultCommand, - getFlexDirection, - getTooltipPlacement, - getIcon -} from './utilities'; +import { getButtonType, getDefaultCommand, getTooltipPlacement, getIcon } from './utilities'; import { LabelWithShortcut } from './LabelWithShortcut'; -import styled from 'styled-components'; import { BaseFilterCommand } from '../../architecture/base/commands/BaseFilterCommand'; import { FilterItem } from './FilterItem'; -import { OPTION_MIN_WIDTH, DEFAULT_PADDING } from './constants'; +import { OPTION_MIN_WIDTH, DEFAULT_PADDING, SELECT_DROPDOWN_ICON_COLOR } from './constants'; import { type IconName } from '../../architecture/base/utilities/IconName'; import { IconComponent } from './IconComponentMapper'; import { TOOLBAR_HORIZONTAL_PANEL_OFFSET } from '../constants'; import { offset } from '@floating-ui/dom'; +import styled from 'styled-components'; +import { type PlacementType } from './types'; export const FilterButton = ({ inputCommand, @@ -43,7 +40,6 @@ export const FilterButton = ({ usedInSettings?: boolean; }): ReactElement => { const renderTarget = useRenderTarget(); - const { t } = useTranslation(); const command = useMemo( () => getDefaultCommand(inputCommand, renderTarget), [] @@ -57,8 +53,12 @@ export const FilterButton = ({ const [icon, setIcon] = useState(undefined); const [isOpen, setOpen] = useState(false); const [isAllChecked, setAllChecked] = useState(false); + const [isSomeChecked, setSomeChecked] = useState(false); const [selectedLabel, setSelectedLabel] = useState(''); + const { t } = useTranslation(); + const label = command.getLabel(t); + const update = useCallback( (command: BaseCommand) => { setEnabled(command.isEnabled); @@ -67,6 +67,7 @@ export const FilterButton = ({ setIcon(getIcon(command)); if (command instanceof BaseFilterCommand) { setAllChecked(command.isAllChecked); + setSomeChecked(command.children?.some((child) => child.isChecked) === true); setSelectedLabel(command.getSelectedLabel(t)); } }, @@ -81,92 +82,199 @@ export const FilterButton = ({ }; }, [command]); - if (!isVisible) { + const children = command.children; + if (!isVisible || children === undefined || children.length === 0) { return <>; } const placement = getTooltipPlacement(isHorizontal); - const label = usedInSettings ? undefined : command.getLabel(t); - const flexDirection = getFlexDirection(isHorizontal); - const children = command.children; - if (children === undefined || !command.hasChildren) { - return <>; - } + const PanelContent = ( + + ); + + return usedInSettings ? ( + + ) : ( + + ); +}; + +const FilterMenu = ({ + command, + isOpen, + setOpen, + label, + isEnabled, + placement, + iconName, + uniqueId, + PanelContent +}: { + command: BaseFilterCommand; + isOpen: boolean; + setOpen: Dispatch>; + label: string; + isEnabled: boolean; + placement: PlacementType; + iconName: string | undefined; + uniqueId: number; + PanelContent: ReactElement; +}): ReactElement => { + const { t } = useTranslation(); + return ( { - setOpen(open); - }} + onOpenChange={setOpen} appendTo={'parent'} - placement={usedInSettings ? 'auto-start' : 'bottom-end'} + placement={'right-start'} disableCloseOnClickInside renderTrigger={(props: any) => ( } - disabled={usedInSettings || label === undefined} + disabled={isOpen || label === undefined} appendTo={document.body} placement={placement}> + }} + /> )}> - { - command.toggleAllChecked(); - }}> - {BaseFilterCommand.getAllString(t)} - - - {children.map((child, _index): ReactElement => { - return ; - })} - + {PanelContent} ); +}; - function getButtonIcon(usedInSettings: boolean, isOpen: boolean): ReactElement { - return usedInSettings ? ( - isOpen ? ( - - ) : ( - - ) - ) : ( - - ); - } +const FilterDropdown = ({ + label, + selectedLabel, + isOpen, + PanelContent +}: { + label: string; + selectedLabel: string; + isOpen: boolean; + PanelContent: ReactElement; +}): ReactElement => { + return ( + + + + + + + {PanelContent} + + + ); +}; + +const FilterSelectPanelContent = ({ + command, + isAllChecked, + isSomeChecked, + label +}: { + command: BaseFilterCommand; + label: string; + isAllChecked: boolean; + isSomeChecked: boolean; +}): ReactElement => { + const { t } = useTranslation(); + + const children = command.children; + + return ( + <> + + + + { + command.toggleAllChecked(); + }} + label={BaseFilterCommand.getAllString(t)}> + {BaseFilterCommand.getAllString(t)} + + + + {children?.map((child, _index): ReactElement => { + return ; + })} + + + + ); }; -const StyledMenuItems = styled.div` - max-height: 300px; - overflow-y: auto; - overflow-x: hidden; +const StyledDropdownRow = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 8px; + padding: ${DEFAULT_PADDING}; +`; + +const StyledSelectPanel = styled(SelectPanel)` + display: flex; + display-direction: row; + justify-content: space-between; + align-items: center; + gap: 8px; +`; + +const StyledDropdownSelectionLabel = styled.label` + font-weight: 400; `; diff --git a/react-components/src/components/Architecture/FilterItem.tsx b/react-components/src/components/Architecture/FilterItem.tsx index 4f633a16c3e..729aa3b6141 100644 --- a/react-components/src/components/Architecture/FilterItem.tsx +++ b/react-components/src/components/Architecture/FilterItem.tsx @@ -3,7 +3,7 @@ */ import { useCallback, useEffect, useState, type ReactElement } from 'react'; -import { Menu } from '@cognite/cogs-lab'; +import { SelectPanel } from '@cognite/cogs-lab'; import { useTranslation } from '../i18n/I18n'; import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; import styled from 'styled-components'; @@ -37,10 +37,11 @@ export const FilterItem = ({ command }: { command: BaseFilterItemCommand }): Rea return <>; } return ( - {command.color !== undefined && } @@ -49,15 +50,16 @@ export const FilterItem = ({ command }: { command: BaseFilterItemCommand }): Rea onClick={() => { command.invoke(); }} - label={command.getLabel(t)}> + label={command.getLabel(t)} + /> ); }; const ColorBox = styled.div<{ backgroundColor: Color }>` width: 16px; height: 16px; - border: 1px solid black; display: inline-block; + border-radius: 4px; background-color: ${(props) => props.backgroundColor.getStyle()}; `; diff --git a/react-components/src/components/Architecture/SettingsButton.tsx b/react-components/src/components/Architecture/SettingsButton.tsx index b3cced8a5df..45d9d143d52 100644 --- a/react-components/src/components/Architecture/SettingsButton.tsx +++ b/react-components/src/components/Architecture/SettingsButton.tsx @@ -2,15 +2,8 @@ * Copyright 2024 Cognite AS */ -import { - type MouseEvent, - useCallback, - useEffect, - useMemo, - useState, - type ReactElement -} from 'react'; -import { Button, Tooltip as CogsTooltip, Slider } from '@cognite/cogs.js'; +import { useCallback, useEffect, useMemo, useState, type ReactElement } from 'react'; +import { Button, Tooltip as CogsTooltip, Slider, Switch } from '@cognite/cogs.js'; import { Menu } from '@cognite/cogs-lab'; import { useTranslation } from '../i18n/I18n'; import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; @@ -38,6 +31,7 @@ import { IconComponent } from './IconComponentMapper'; import { TOOLBAR_HORIZONTAL_PANEL_OFFSET } from '../constants'; import { offset } from '@floating-ui/dom'; +import { DividerCommand } from '../../architecture/base/commands/DividerCommand'; export const SettingsButton = ({ inputCommand, @@ -99,7 +93,7 @@ export const SettingsButton = ({ renderTrigger={(props: any) => ( } - disabled={label === undefined} + disabled={isOpen || label === undefined} appendTo={document.body} placement={placement}>