From a0484f686ac1a7b31cc4c3f36b92f72735bb8498 Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Wed, 8 Apr 2026 06:57:08 +0530 Subject: [PATCH] Add color to object icon picker in data model (#19368) Closes [#2291](https://github.com/twentyhq/core-team-issues/issues/2291) --- .../components/SidePanelEditColorOption.tsx | 44 +--- ...SettingsUpdateDataModelObjectAboutForm.tsx | 15 +- .../SettingsDataModelObjectAboutForm.tsx | 26 ++- ...ataModelObjectAboutFormSchema.test.ts.snap | 39 ++++ ...ingsDataModelObjectAboutFormSchema.test.ts | 11 + .../settingsDataModelObjectAboutFormSchema.ts | 3 + .../ui/input/components/IconPicker.tsx | 204 +++++++++++++++--- .../input/components/ThemeColorPickerMenu.tsx | 55 +++++ .../settings/data-model/SettingsNewObject.tsx | 1 + .../display/color/components/ColorSample.tsx | 12 +- .../__stories__/ColorSample.stories.tsx | 4 + .../MenuItemSelectColor.stories.tsx | 2 +- 12 files changed, 337 insertions(+), 79 deletions(-) create mode 100644 packages/twenty-front/src/modules/ui/input/components/ThemeColorPickerMenu.tsx diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx index 2389abdecd..b4f93ef891 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx @@ -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={{colorLabel}} dropdownComponents={ - setSearchValue(event.target.value)} + - - - {filteredColorNames.map((colorName) => ( - handleSelectColor(colorName)} - color={colorName} - selected={colorName === themeColor} - colorLabels={DEFAULT_COLOR_LABELS} - /> - ))} - } /> diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx index 154453b9c5..a794bcd54c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx index 233525938e..72643819f2 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx @@ -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 = ({ { + setValue('color', nextColor, { + shouldDirty: true, + shouldValidate: true, + }); + onNewDirtyField?.(); + }, + } + : undefined + } onChange={({ iconKey }) => { if (disableEdition) { return; diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap index 346d195422..568598a3aa 100644 --- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap @@ -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: [ { diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts index a7e282cbfe..89d55c9197 100644 --- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts @@ -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])( diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts index ec60508a31..44bf1d8e46 100644 --- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts @@ -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, diff --git a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx index 37850c18ff..e2bac03182 100644 --- a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx @@ -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 = ( + { + onSearchChange(event.target.value); + }} + /> + ); + + if (!isDefined(iconColorPicker)) { + return searchInput; + } + + return ( + + {searchInput} + + ( + + )} + size="small" + /> + } + dropdownComponents={ + + { + iconColorPicker.onColorChange(nextColor); + closeDropdown(iconColorPickerDropdownId); + }} + /> + + } + /> + + + ); }; 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) => ( + + )} onClick={onSelect} /> @@ -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) => ( + + ); + + const iconColorPickerDropdownId = `${dropdownId}-icon-color-picker`; const selectableListInstanceId = 'icon-list'; @@ -307,12 +437,15 @@ export const IconPicker = ({ @@ -330,12 +463,12 @@ export const IconPicker = ({ selectableItemIdMatrix={iconKeys2d} focusId={dropdownId} > - { - setSearchString(event.target.value); - }} +
))} diff --git a/packages/twenty-front/src/modules/ui/input/components/ThemeColorPickerMenu.tsx b/packages/twenty-front/src/modules/ui/input/components/ThemeColorPickerMenu.tsx new file mode 100644 index 0000000000..c8e9410bf9 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/ThemeColorPickerMenu.tsx @@ -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 ( + <> + setSearchValue(event.target.value)} + /> + + + {filteredColorNames.map((colorName) => ( + onSelectColor(colorName)} + color={colorName} + selected={colorName === selectedColor} + colorLabels={DEFAULT_COLOR_LABELS} + /> + ))} + + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx index a27133042b..49c61f2961 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx @@ -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, }, diff --git a/packages/twenty-ui/src/display/color/components/ColorSample.tsx b/packages/twenty-ui/src/display/color/components/ColorSample.tsx index 9c03fdce81..0453640a4e 100644 --- a/packages/twenty-ui/src/display/color/components/ColorSample.tsx +++ b/packages/twenty-ui/src/display/color/components/ColorSample.tsx @@ -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` 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')}; diff --git a/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx b/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx index deab220b24..22f966796b 100644 --- a/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx +++ b/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx @@ -19,3 +19,7 @@ export const Default: Story = {}; export const Pipeline: Story = { args: { variant: 'pipeline' }, }; + +export const Circle: Story = { + args: { variant: 'circle' }, +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx index df16924626..0c12adf42f 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx @@ -67,7 +67,7 @@ export const Catalog: CatalogStory = { }, { name: 'variant', - values: ['default', 'pipeline'], + values: ['circle', 'default', 'pipeline'], props: (variant: ColorSampleVariant) => ({ variant }), labels: (variant: ColorSampleVariant) => variant, },