Add color to object icon picker in data model (#19368)
Closes [#2291](https://github.com/twentyhq/core-team-issues/issues/2291)
This commit is contained in:
@@ -1,39 +1,52 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
import React, { type ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { arrayToChunks } from '~/utils/array/arrayToChunks';
|
||||
|
||||
import { ICON_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/ui/input/components/constants/IconPickerDropdownContentWidth';
|
||||
import { IconPickerScrollEffect } from '@/ui/input/effect-components/IconPickerScrollEffect';
|
||||
import {
|
||||
ICON_PICKER_DEFAULT_VISIBLE_COUNT,
|
||||
iconPickerVisibleCountState,
|
||||
} from '@/ui/input/states/iconPickerVisibleCountState';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { type DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { IconApps, type IconComponent, useIcons } from 'twenty-ui/display';
|
||||
import React, {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
ColorSample,
|
||||
IconApps,
|
||||
type IconComponent,
|
||||
useIcons,
|
||||
} from 'twenty-ui/display';
|
||||
import {
|
||||
IconButton,
|
||||
type IconButtonSize,
|
||||
type IconButtonVariant,
|
||||
LightIconButton,
|
||||
} from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type ThemeColor } from 'twenty-ui/theme';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { ICON_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/ui/input/components/constants/IconPickerDropdownContentWidth';
|
||||
import { ThemeColorPickerMenu } from '@/ui/input/components/ThemeColorPickerMenu';
|
||||
import { IconPickerScrollEffect } from '@/ui/input/effect-components/IconPickerScrollEffect';
|
||||
import {
|
||||
ICON_PICKER_DEFAULT_VISIBLE_COUNT,
|
||||
iconPickerVisibleCountState,
|
||||
} from '@/ui/input/states/iconPickerVisibleCountState';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { type DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { arrayToChunks } from '~/utils/array/arrayToChunks';
|
||||
|
||||
export type IconPickerProps = {
|
||||
disabled?: boolean;
|
||||
@@ -50,6 +63,96 @@ export type IconPickerProps = {
|
||||
dropdownWidth?: number;
|
||||
dropdownOffset?: DropdownOffset;
|
||||
maxIconsVisible?: number;
|
||||
iconColorPicker?: {
|
||||
selectedColor: ThemeColor;
|
||||
onColorChange: (color: ThemeColor) => void;
|
||||
};
|
||||
};
|
||||
|
||||
const StyledIconPickerSearchRow = styled.div`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
padding-right: ${themeCssVariables.spacing[2]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type IconPickerSearchRowProps = {
|
||||
closeDropdown: (dropdownId: string) => void;
|
||||
dropdownWidth: number | undefined;
|
||||
iconColorPicker: IconPickerProps['iconColorPicker'];
|
||||
iconColorPickerDropdownId: string;
|
||||
onSearchChange: (searchString: string) => void;
|
||||
};
|
||||
|
||||
const IconPickerSearchRow = ({
|
||||
closeDropdown,
|
||||
dropdownWidth,
|
||||
iconColorPicker,
|
||||
iconColorPickerDropdownId,
|
||||
onSearchChange,
|
||||
}: IconPickerSearchRowProps) => {
|
||||
const searchInput = (
|
||||
<DropdownMenuSearchInput
|
||||
placeholder={t`Search icon`}
|
||||
autoFocus
|
||||
onChange={(event) => {
|
||||
onSearchChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isDefined(iconColorPicker)) {
|
||||
return searchInput;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledIconPickerSearchRow>
|
||||
{searchInput}
|
||||
<ClickOutsideListenerContext.Provider
|
||||
value={{
|
||||
excludedClickOutsideId: iconColorPickerDropdownId,
|
||||
}}
|
||||
>
|
||||
<Dropdown
|
||||
dropdownId={iconColorPickerDropdownId}
|
||||
dropdownOffset={{
|
||||
x: 24,
|
||||
y: -24,
|
||||
}}
|
||||
dropdownPlacement="right-start"
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
accent="secondary"
|
||||
Icon={() => (
|
||||
<ColorSample
|
||||
colorName={iconColorPicker.selectedColor}
|
||||
variant="circle"
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent
|
||||
widthInPixels={
|
||||
dropdownWidth || ICON_PICKER_DROPDOWN_CONTENT_WIDTH
|
||||
}
|
||||
>
|
||||
<ThemeColorPickerMenu
|
||||
selectedColor={iconColorPicker.selectedColor}
|
||||
onSelectColor={(nextColor) => {
|
||||
iconColorPicker.onColorChange(nextColor);
|
||||
closeDropdown(iconColorPickerDropdownId);
|
||||
}}
|
||||
/>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
</ClickOutsideListenerContext.Provider>
|
||||
</StyledIconPickerSearchRow>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledMenuIconItemsContainer = styled.div`
|
||||
@@ -130,6 +233,7 @@ type IconPickerIconProps = {
|
||||
selectedIconKey?: string;
|
||||
Icon: IconComponent;
|
||||
focusedIconKey?: string;
|
||||
color?: ThemeColor;
|
||||
};
|
||||
|
||||
const IconPickerIcon = ({
|
||||
@@ -138,7 +242,10 @@ const IconPickerIcon = ({
|
||||
selectedIconKey,
|
||||
Icon,
|
||||
focusedIconKey,
|
||||
color,
|
||||
}: IconPickerIconProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const selectedItemId = useAtomComponentStateValue(
|
||||
selectedItemIdComponentState,
|
||||
iconKey,
|
||||
@@ -154,7 +261,13 @@ const IconPickerIcon = ({
|
||||
title={iconKey}
|
||||
isSelected={iconKey === selectedIconKey || !!selectedItemId}
|
||||
isFocused={iconKey === focusedIconKey}
|
||||
Icon={Icon}
|
||||
Icon={(iconProps) => (
|
||||
<Icon
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...iconProps}
|
||||
color={isDefined(color) ? theme.color[color] : iconProps.color}
|
||||
/>
|
||||
)}
|
||||
onClick={onSelect}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
@@ -177,6 +290,7 @@ export const IconPicker = ({
|
||||
dropdownWidth,
|
||||
dropdownOffset,
|
||||
maxIconsVisible,
|
||||
iconColorPicker,
|
||||
}: IconPickerProps) => {
|
||||
const [searchString, setSearchString] = useState('');
|
||||
|
||||
@@ -284,7 +398,23 @@ export const IconPicker = ({
|
||||
[matchingSearchIconKeys],
|
||||
);
|
||||
|
||||
const icon = selectedIconKey ? getIcon(selectedIconKey) : IconApps;
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const BaseIcon = selectedIconKey ? getIcon(selectedIconKey) : IconApps;
|
||||
|
||||
const displayIcon: IconComponent = !isDefined(iconColorPicker)
|
||||
? BaseIcon
|
||||
: (iconProps) => (
|
||||
<BaseIcon
|
||||
className={iconProps.className}
|
||||
color={theme.color[iconColorPicker.selectedColor]}
|
||||
size={iconProps.size}
|
||||
stroke={iconProps.stroke}
|
||||
style={iconProps.style}
|
||||
/>
|
||||
);
|
||||
|
||||
const iconColorPickerDropdownId = `${dropdownId}-icon-color-picker`;
|
||||
|
||||
const selectableListInstanceId = 'icon-list';
|
||||
|
||||
@@ -307,12 +437,15 @@ export const IconPicker = ({
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownOffset={dropdownOffset}
|
||||
excludedClickOutsideIds={
|
||||
isDefined(iconColorPicker) ? [iconColorPickerDropdownId] : undefined
|
||||
}
|
||||
clickableComponent={
|
||||
clickableComponent ?? (
|
||||
<IconButton
|
||||
ariaLabel={t`Click to select icon ${iconAriaLabel}`}
|
||||
disabled={disabled}
|
||||
Icon={icon}
|
||||
Icon={displayIcon}
|
||||
variant={variant}
|
||||
size={size}
|
||||
/>
|
||||
@@ -330,12 +463,12 @@ export const IconPicker = ({
|
||||
selectableItemIdMatrix={iconKeys2d}
|
||||
focusId={dropdownId}
|
||||
>
|
||||
<DropdownMenuSearchInput
|
||||
placeholder={t`Search icon`}
|
||||
autoFocus
|
||||
onChange={(event) => {
|
||||
setSearchString(event.target.value);
|
||||
}}
|
||||
<IconPickerSearchRow
|
||||
closeDropdown={closeDropdown}
|
||||
dropdownWidth={dropdownWidth}
|
||||
iconColorPicker={iconColorPicker}
|
||||
iconColorPickerDropdownId={iconColorPickerDropdownId}
|
||||
onSearchChange={setSearchString}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<div
|
||||
@@ -355,6 +488,7 @@ export const IconPicker = ({
|
||||
selectedIconKey={selectedIconKey}
|
||||
Icon={getIcon(iconKey)}
|
||||
focusedIconKey={focusedIconKey}
|
||||
color={iconColorPicker?.selectedColor}
|
||||
/>
|
||||
))}
|
||||
</StyledMenuIconItemsContainer>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
DEFAULT_COLOR_LABELS,
|
||||
MenuItemSelectColor,
|
||||
} from 'twenty-ui/navigation';
|
||||
import { type ThemeColor, MAIN_COLOR_NAMES } from 'twenty-ui/theme';
|
||||
|
||||
type ThemeColorPickerMenuProps = {
|
||||
selectedColor: ThemeColor;
|
||||
onSelectColor: (color: ThemeColor) => void;
|
||||
};
|
||||
|
||||
export const ThemeColorPickerMenu = ({
|
||||
selectedColor,
|
||||
onSelectColor,
|
||||
}: ThemeColorPickerMenuProps) => {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const query = searchValue.trim().toLowerCase();
|
||||
|
||||
const filteredColorNames = isNonEmptyString(query)
|
||||
? MAIN_COLOR_NAMES.filter(
|
||||
(colorName) =>
|
||||
colorName.toLowerCase().includes(query) ||
|
||||
(DEFAULT_COLOR_LABELS[colorName] ?? '').toLowerCase().includes(query),
|
||||
)
|
||||
: MAIN_COLOR_NAMES;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSearchInput
|
||||
placeholder={t`Search colors...`}
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{filteredColorNames.map((colorName) => (
|
||||
<MenuItemSelectColor
|
||||
key={colorName}
|
||||
onClick={() => onSelectColor(colorName)}
|
||||
color={colorName}
|
||||
selected={colorName === selectedColor}
|
||||
colorLabels={DEFAULT_COLOR_LABELS}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user