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:
+7
-37
@@ -4,23 +4,17 @@ import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMe
|
||||
import { isValidObjectNavigationMenuItem } from '@/navigation-menu-item/common/utils/isValidObjectNavigationMenuItem';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useUpdateNavigationMenuItemInDraft } from '@/navigation-menu-item/edit/hooks/useUpdateNavigationMenuItemInDraft';
|
||||
import { ThemeColorPickerMenu } from '@/ui/input/components/ThemeColorPickerMenu';
|
||||
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 { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { IconColorSwatch } from 'twenty-ui/display';
|
||||
import {
|
||||
DEFAULT_COLOR_LABELS,
|
||||
MenuItemSelectColor,
|
||||
} from 'twenty-ui/navigation';
|
||||
import { type ThemeColor, MAIN_COLOR_NAMES } from 'twenty-ui/theme';
|
||||
import { DEFAULT_COLOR_LABELS } from 'twenty-ui/navigation';
|
||||
import { type ThemeColor } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const NAVIGATION_MENU_ITEM_COLOR_DROPDOWN_ID = 'navigation-menu-item-color';
|
||||
|
||||
const StyledMenuStyleText = styled.span`
|
||||
@@ -44,20 +38,9 @@ export const SidePanelEditColorOption = ({
|
||||
const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft();
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const themeColor = color ?? 'gray';
|
||||
const colorLabel = DEFAULT_COLOR_LABELS[themeColor] ?? capitalize(themeColor);
|
||||
|
||||
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;
|
||||
|
||||
const handleSelectColor = (selectedColor: ThemeColor) => {
|
||||
updateNavigationMenuItemInDraft(navigationMenuItemId, {
|
||||
color: selectedColor,
|
||||
@@ -86,23 +69,10 @@ export const SidePanelEditColorOption = ({
|
||||
RightComponent={<StyledMenuStyleText>{colorLabel}</StyledMenuStyleText>}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuSearchInput
|
||||
placeholder={t`Search colors...`}
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
<ThemeColorPickerMenu
|
||||
selectedColor={themeColor}
|
||||
onSelectColor={handleSelectColor}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{filteredColorNames.map((colorName) => (
|
||||
<MenuItemSelectColor
|
||||
key={colorName}
|
||||
onClick={() => handleSelectColor(colorName)}
|
||||
color={colorName}
|
||||
selected={colorName === themeColor}
|
||||
colorLabels={DEFAULT_COLOR_LABELS}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
|
||||
+14
-1
@@ -1,6 +1,7 @@
|
||||
import { isDDLLockedState } from '@/client-config/states/isDDLLockedState';
|
||||
import { parseThemeColor } from '@/navigation-menu-item/common/utils/parseThemeColor';
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { isDDLLockedState } from '@/client-config/states/isDDLLockedState';
|
||||
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
|
||||
import { computeUpdatedNavigationMemorizedUrlAfterObjectNamePluralChange } from '@/settings/data-model/object-details/utils/computeUpdatedNavigationMemorizedUrlAfterObjectNamePluralChange';
|
||||
import { SettingsDataModelObjectAboutForm } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
||||
@@ -60,6 +61,9 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
|
||||
labelSingular,
|
||||
namePlural,
|
||||
nameSingular,
|
||||
...(objectMetadataItem.isCustom
|
||||
? { color: parseThemeColor(objectMetadataItem.color) }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -99,6 +103,14 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
|
||||
labelSingular: updatedObject?.data?.updateOneObject.labelSingular,
|
||||
namePlural: updatedObject?.data?.updateOneObject.namePlural,
|
||||
nameSingular: updatedObject?.data?.updateOneObject.nameSingular,
|
||||
...(objectMetadataItem.isCustom
|
||||
? {
|
||||
color: parseThemeColor(
|
||||
updatedObject?.data?.updateOneObject.color ??
|
||||
objectMetadataItem.color,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
} else {
|
||||
formConfig.reset(formValues);
|
||||
@@ -134,6 +146,7 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
|
||||
nameSingular: _nameSingular,
|
||||
namePlural: _namePlural,
|
||||
isLabelSyncedWithName: _isLabelSyncedWithName,
|
||||
color: _color,
|
||||
...payloadWithoutNames
|
||||
} = updatePayload;
|
||||
|
||||
|
||||
+25
-1
@@ -1,3 +1,4 @@
|
||||
import { parseThemeColor } from '@/navigation-menu-item/common/utils/parseThemeColor';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
@@ -7,9 +8,9 @@ import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural } from 'pluralize';
|
||||
import { useContext } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
@@ -127,8 +128,12 @@ export const SettingsDataModelObjectAboutForm = ({
|
||||
const labelPlural = watch('labelPlural');
|
||||
const isStandardObject =
|
||||
isDefined(objectMetadataItem?.isCustom) && !objectMetadataItem.isCustom;
|
||||
const showObjectColorInIconPicker =
|
||||
!isStandardObject &&
|
||||
(!isDefined(objectMetadataItem) || objectMetadataItem.isCustom);
|
||||
watch('description');
|
||||
watch('icon');
|
||||
const objectIconColor = watch('color');
|
||||
|
||||
const apiNameTooltipText =
|
||||
!isDefined(objectMetadataItem) || objectMetadataItem.isCustom
|
||||
@@ -186,6 +191,25 @@ export const SettingsDataModelObjectAboutForm = ({
|
||||
<IconPicker
|
||||
selectedIconKey={value}
|
||||
disabled={disableEdition}
|
||||
dropdownId={
|
||||
isDefined(objectMetadataItem)
|
||||
? `settings-object-about-icon-${objectMetadataItem.id}`
|
||||
: 'settings-new-object-about-icon'
|
||||
}
|
||||
iconColorPicker={
|
||||
showObjectColorInIconPicker
|
||||
? {
|
||||
selectedColor: parseThemeColor(objectIconColor),
|
||||
onColorChange: (nextColor) => {
|
||||
setValue('color', nextColor, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
onNewDirtyField?.();
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onChange={({ iconKey }) => {
|
||||
if (disableEdition) {
|
||||
return;
|
||||
|
||||
+39
@@ -1,5 +1,44 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`settingsDataModelObjectAboutFormSchema fails when color is not a valid theme color 1`] = `
|
||||
[ZodError: [
|
||||
{
|
||||
"code": "invalid_value",
|
||||
"values": [
|
||||
"red",
|
||||
"ruby",
|
||||
"crimson",
|
||||
"tomato",
|
||||
"orange",
|
||||
"amber",
|
||||
"yellow",
|
||||
"lime",
|
||||
"grass",
|
||||
"green",
|
||||
"jade",
|
||||
"mint",
|
||||
"turquoise",
|
||||
"cyan",
|
||||
"sky",
|
||||
"blue",
|
||||
"iris",
|
||||
"violet",
|
||||
"purple",
|
||||
"plum",
|
||||
"pink",
|
||||
"bronze",
|
||||
"gold",
|
||||
"brown",
|
||||
"gray"
|
||||
],
|
||||
"path": [
|
||||
"color"
|
||||
],
|
||||
"message": "Invalid option: expected one of \\"red\\"|\\"ruby\\"|\\"crimson\\"|\\"tomato\\"|\\"orange\\"|\\"amber\\"|\\"yellow\\"|\\"lime\\"|\\"grass\\"|\\"green\\"|\\"jade\\"|\\"mint\\"|\\"turquoise\\"|\\"cyan\\"|\\"sky\\"|\\"blue\\"|\\"iris\\"|\\"violet\\"|\\"purple\\"|\\"plum\\"|\\"pink\\"|\\"bronze\\"|\\"gold\\"|\\"brown\\"|\\"gray\\""
|
||||
}
|
||||
]]
|
||||
`;
|
||||
|
||||
exports[`settingsDataModelObjectAboutFormSchema fails when isLabelSyncedWithName is not a boolean 1`] = `
|
||||
[ZodError: [
|
||||
{
|
||||
|
||||
+11
@@ -6,6 +6,7 @@ import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
describe('settingsDataModelObjectAboutFormSchema', () => {
|
||||
const validInput: SettingsDataModelObjectAboutFormValues = {
|
||||
color: 'gray',
|
||||
description: 'A valid description',
|
||||
icon: 'IconName',
|
||||
labelPlural: 'Labels Plural',
|
||||
@@ -148,6 +149,16 @@ describe('settingsDataModelObjectAboutFormSchema', () => {
|
||||
expectedSuccess: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'fails when color is not a valid theme color',
|
||||
context: {
|
||||
input: {
|
||||
...validInput,
|
||||
color: 'not-a-color',
|
||||
},
|
||||
expectedSuccess: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each([...passingTestsUseCase, ...failsValidationTestsUseCase])(
|
||||
|
||||
+3
@@ -1,5 +1,6 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { themeColorSchema } from 'twenty-ui/utilities';
|
||||
import { type ZodType, z } from 'zod';
|
||||
import { type ReadonlyKeysArray } from '~/types/ReadonlyKeysArray';
|
||||
import { zodNonEmptyString } from '~/types/ZodNonEmptyString';
|
||||
@@ -8,6 +9,7 @@ import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStrin
|
||||
type ZodTypeSettingsDataModelFormFields = ZodType<
|
||||
Pick<
|
||||
EnrichedObjectMetadataItem,
|
||||
| 'color'
|
||||
| 'labelSingular'
|
||||
| 'labelPlural'
|
||||
| 'description'
|
||||
@@ -18,6 +20,7 @@ type ZodTypeSettingsDataModelFormFields = ZodType<
|
||||
> & { skipNameField?: boolean }
|
||||
>;
|
||||
const settingsDataModelFormFieldsSchema = z.object({
|
||||
color: themeColorSchema.optional(),
|
||||
description: z.string().nullish(),
|
||||
icon: z.string().optional(),
|
||||
labelSingular: zodNonEmptyString,
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -34,6 +34,7 @@ export const SettingsNewObject = () => {
|
||||
mode: 'onChange',
|
||||
resolver: zodResolver(settingsDataModelObjectAboutFormSchema),
|
||||
defaultValues: {
|
||||
color: 'gray',
|
||||
isLabelSyncedWithName:
|
||||
SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE,
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type ThemeColor } from '@ui/theme';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type ColorSampleVariant = 'default' | 'pipeline';
|
||||
export type ColorSampleVariant = 'circle' | 'default' | 'pipeline';
|
||||
|
||||
type StyledColorSampleProps = {
|
||||
colorName: ThemeColor;
|
||||
@@ -32,9 +32,13 @@ const StyledColorSample = styled.div<StyledColorSampleProps>`
|
||||
background-color: ${({ colorName, color }) => getColor(colorName, color)};
|
||||
border: ${({ variant, colorName }) =>
|
||||
variant === 'pipeline' ? '0' : `1px solid ${getBorderColor(colorName)}`};
|
||||
border-radius: 60px;
|
||||
border-radius: ${({ variant }) => (variant === 'circle' ? '50%' : '60px')};
|
||||
flex-shrink: 0;
|
||||
height: ${themeCssVariables.spacing[4]};
|
||||
width: ${themeCssVariables.spacing[3]};
|
||||
width: ${({ variant }) =>
|
||||
variant === 'circle'
|
||||
? themeCssVariables.spacing[4]
|
||||
: themeCssVariables.spacing[3]};
|
||||
align-items: ${({ variant }) =>
|
||||
variant === 'pipeline' ? 'center' : 'initial'};
|
||||
display: ${({ variant }) => (variant === 'pipeline' ? 'flex' : 'block')};
|
||||
|
||||
@@ -19,3 +19,7 @@ export const Default: Story = {};
|
||||
export const Pipeline: Story = {
|
||||
args: { variant: 'pipeline' },
|
||||
};
|
||||
|
||||
export const Circle: Story = {
|
||||
args: { variant: 'circle' },
|
||||
};
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ export const Catalog: CatalogStory<Story, typeof MenuItemSelectColor> = {
|
||||
},
|
||||
{
|
||||
name: 'variant',
|
||||
values: ['default', 'pipeline'],
|
||||
values: ['circle', 'default', 'pipeline'],
|
||||
props: (variant: ColorSampleVariant) => ({ variant }),
|
||||
labels: (variant: ColorSampleVariant) => variant,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user