From 3617abb0e64559dd1d0d11802895f58515638eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Wed, 29 Nov 2023 12:49:41 +0100 Subject: [PATCH] feat: pick select field option colors (#2748) Closes #2433 Co-authored-by: Charles Bochet --- .../hooks/useUpdateCompanyBoardColumns.ts | 12 ++-- .../SettingsObjectFieldSelectForm.tsx | 10 ++-- ...SettingsObjectFieldSelectFormOptionRow.tsx | 56 ++++++++++++++++--- .../data-model/hooks/useFieldMetadataForm.ts | 6 +- .../display/color/components/ColorSample.tsx | 39 +++++++++++++ .../__stories__/ColorSample.stories.tsx | 25 +++++++++ .../components/MenuItemSelectColor.tsx | 37 +++++++----- .../MenuItemSelectColor.stories.tsx | 31 +++++----- .../field/types/guards/isFieldSelectValue.ts | 5 +- .../RecordBoardColumnEditTitleMenu.tsx | 34 +++-------- ...RecordBoardColumnEditTitleMenu.stories.tsx | 11 +--- .../src/modules/ui/theme/constants/colors.ts | 4 +- .../ui/theme/utils/castStringAsThemeColor.ts | 6 -- .../ui/theme/utils/themeColorSchema.ts | 7 +++ 14 files changed, 186 insertions(+), 97 deletions(-) create mode 100644 front/src/modules/ui/display/color/components/ColorSample.tsx create mode 100644 front/src/modules/ui/display/color/components/__stories__/ColorSample.stories.tsx delete mode 100644 front/src/modules/ui/theme/utils/castStringAsThemeColor.ts create mode 100644 front/src/modules/ui/theme/utils/themeColorSchema.ts diff --git a/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts b/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts index 21cf23c4a..523ff946c 100644 --- a/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts +++ b/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts @@ -8,7 +8,7 @@ import { boardCardIdsByColumnIdFamilyState } from '@/ui/object/record-board/stat import { boardColumnsState } from '@/ui/object/record-board/states/boardColumnsState'; import { savedBoardColumnsState } from '@/ui/object/record-board/states/savedBoardColumnsState'; import { BoardColumnDefinition } from '@/ui/object/record-board/types/BoardColumnDefinition'; -import { isThemeColor } from '@/ui/theme/utils/castStringAsThemeColor'; +import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { logError } from '~/utils/logError'; @@ -90,7 +90,11 @@ export const useUpdateCompanyBoard = () => const newBoardColumns: BoardColumnDefinition[] = orderedPipelineSteps?.map((pipelineStep) => { - if (!isThemeColor(pipelineStep.color)) { + const colorValidationResult = themeColorSchema.safeParse( + pipelineStep.color, + ); + + if (!colorValidationResult.success) { logError( `Color ${pipelineStep.color} is not recognized in useUpdateCompanyBoard.`, ); @@ -99,8 +103,8 @@ export const useUpdateCompanyBoard = () => return { id: pipelineStep.id, title: pipelineStep.name, - colorCode: isThemeColor(pipelineStep.color) - ? pipelineStep.color + colorCode: colorValidationResult.success + ? colorValidationResult.data : undefined, position: pipelineStep.position ?? 0, }; diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx index d5288845f..1f25ee326 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { IconPlus } from '@/ui/display/icon'; import { Button } from '@/ui/input/button/components/Button'; -import { mainColors, ThemeColor } from '@/ui/theme/constants/colors'; +import { mainColorNames, ThemeColor } from '@/ui/theme/constants/colors'; import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption'; @@ -46,9 +46,11 @@ const StyledButton = styled(Button)` `; const getNextColor = (currentColor: ThemeColor) => { - const colors = Object.keys(mainColors) as ThemeColor[]; - const currentColorIndex = colors.findIndex((color) => color === currentColor); - return colors[(currentColorIndex + 1) % colors.length]; + const currentColorIndex = mainColorNames.findIndex( + (color) => color === currentColor, + ); + const nextColorIndex = (currentColorIndex + 1) % mainColorNames.length; + return mainColorNames[nextColorIndex]; }; export const SettingsObjectFieldSelectForm = ({ diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectFormOptionRow.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectFormOptionRow.tsx index d6cff6640..9efc73786 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectFormOptionRow.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectFormOptionRow.tsx @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import styled from '@emotion/styled'; import { v4 } from 'uuid'; +import { ColorSample } from '@/ui/display/color/components/ColorSample'; import { IconCheck, IconDotsVertical, @@ -16,6 +17,8 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { MenuItemSelectColor } from '@/ui/navigation/menu-item/components/MenuItemSelectColor'; +import { mainColorNames } from '@/ui/theme/constants/colors'; import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption'; @@ -33,6 +36,11 @@ const StyledRow = styled.div` padding: ${({ theme }) => theme.spacing(1)} 0; `; +const StyledColorSample = styled(ColorSample)` + cursor: pointer; + margin-right: 14px; +`; + const StyledOptionInput = styled(TextInput)` flex: 1 0 auto; margin-right: ${({ theme }) => theme.spacing(2)}; @@ -48,22 +56,56 @@ export const SettingsObjectFieldSelectFormOptionRow = ({ onRemove, option, }: SettingsObjectFieldSelectFormOptionRowProps) => { - const dropdownScopeId = useMemo(() => `select-field-option-row-${v4()}`, []); + const dropdownScopeIds = useMemo(() => { + const baseScopeId = `select-field-option-row-${v4()}`; + return { color: `${baseScopeId}-color`, actions: `${baseScopeId}-actions` }; + }, []); - const { closeDropdown } = useDropdown({ dropdownScopeId }); + const { closeDropdown: closeColorDropdown } = useDropdown({ + dropdownScopeId: dropdownScopeIds.color, + }); + const { closeDropdown: closeActionsDropdown } = useDropdown({ + dropdownScopeId: dropdownScopeIds.actions, + }); return ( + + } + dropdownComponents={ + + + {mainColorNames.map((colorName) => ( + { + onChange({ ...option, color: colorName }); + closeColorDropdown(); + }} + color={colorName} + selected={colorName === option.color} + /> + ))} + + + } + /> + onChange({ ...option, label })} RightIcon={isDefault ? IconCheck : undefined} /> - + } dropdownComponents={ @@ -75,7 +117,7 @@ export const SettingsObjectFieldSelectFormOptionRow = ({ text="Remove as default" onClick={() => { onChange({ ...option, isDefault: false }); - closeDropdown(); + closeActionsDropdown(); }} /> ) : ( @@ -84,7 +126,7 @@ export const SettingsObjectFieldSelectFormOptionRow = ({ text="Set as default" onClick={() => { onChange({ ...option, isDefault: true }); - closeDropdown(); + closeActionsDropdown(); }} /> )} @@ -95,7 +137,7 @@ export const SettingsObjectFieldSelectFormOptionRow = ({ text="Remove option" onClick={() => { onRemove(); - closeDropdown(); + closeActionsDropdown(); }} /> )} diff --git a/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts b/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts index 85b630cec..cad06a3f7 100644 --- a/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts +++ b/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts @@ -2,7 +2,7 @@ import { useState } from 'react'; import { DeepPartial } from 'react-hook-form'; import { z } from 'zod'; -import { mainColors, ThemeColor } from '@/ui/theme/constants/colors'; +import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema'; import { FieldMetadataType, RelationMetadataType, @@ -59,9 +59,7 @@ const selectSchema = fieldSchema.merge( select: z .array( z.object({ - color: z.enum( - Object.keys(mainColors) as [ThemeColor, ...ThemeColor[]], - ), + color: themeColorSchema, isDefault: z.boolean().optional(), label: z.string().min(1), }), diff --git a/front/src/modules/ui/display/color/components/ColorSample.tsx b/front/src/modules/ui/display/color/components/ColorSample.tsx new file mode 100644 index 000000000..a3be98ab9 --- /dev/null +++ b/front/src/modules/ui/display/color/components/ColorSample.tsx @@ -0,0 +1,39 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { ThemeColor } from '@/ui/theme/constants/colors'; + +export type ColorSampleVariant = 'default' | 'pipeline'; + +const StyledColorSample = styled.div<{ + colorName: ThemeColor; + variant?: ColorSampleVariant; +}>` + background-color: ${({ theme, colorName }) => + theme.tag.background[colorName]}; + border: 1px solid ${({ theme, colorName }) => theme.tag.text[colorName]}; + border-radius: 60px; + height: ${({ theme }) => theme.spacing(4)}; + width: ${({ theme }) => theme.spacing(3)}; + + ${({ colorName, theme, variant }) => { + if (variant === 'pipeline') + return css` + align-items: center; + border: 0; + display: flex; + justify-content: center; + + &:after { + background-color: ${theme.tag.text[colorName]}; + border-radius: ${theme.border.radius.rounded}; + content: ''; + display: block; + height: ${theme.spacing(1)}; + width: ${theme.spacing(1)}; + } + `; + }} +`; + +export { StyledColorSample as ColorSample }; diff --git a/front/src/modules/ui/display/color/components/__stories__/ColorSample.stories.tsx b/front/src/modules/ui/display/color/components/__stories__/ColorSample.stories.tsx new file mode 100644 index 000000000..2514e0f78 --- /dev/null +++ b/front/src/modules/ui/display/color/components/__stories__/ColorSample.stories.tsx @@ -0,0 +1,25 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; + +import { ColorSample } from '../ColorSample'; + +const meta: Meta = { + title: 'UI/Display/Color/ColorSample', + component: ColorSample, + decorators: [ComponentDecorator], + args: { colorName: 'green' }, + argTypes: { + as: { control: false }, + theme: { control: false }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Pipeline: Story = { + args: { variant: 'pipeline' }, +}; diff --git a/front/src/modules/ui/navigation/menu-item/components/MenuItemSelectColor.tsx b/front/src/modules/ui/navigation/menu-item/components/MenuItemSelectColor.tsx index 10b603e84..0f0a4b0fc 100644 --- a/front/src/modules/ui/navigation/menu-item/components/MenuItemSelectColor.tsx +++ b/front/src/modules/ui/navigation/menu-item/components/MenuItemSelectColor.tsx @@ -1,6 +1,9 @@ import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; +import { + ColorSample, + ColorSampleVariant, +} from '@/ui/display/color/components/ColorSample'; import { IconCheck } from '@/ui/display/icon'; import { ThemeColor } from '@/ui/theme/constants/colors'; @@ -11,33 +14,37 @@ import { import { StyledMenuItemSelect } from './MenuItemSelect'; -const StyledColorSample = styled.div<{ colorName: ThemeColor }>` - background-color: ${({ theme, colorName }) => - theme.tag.background[colorName]}; - border: 1px solid ${({ theme, colorName }) => theme.color[colorName]}; - border-radius: ${({ theme }) => theme.border.radius.sm}; - height: 12px; - width: 12px; -`; - type MenuItemSelectColorProps = { selected: boolean; - text: string; className?: string; onClick?: () => void; disabled?: boolean; hovered?: boolean; color: ThemeColor; + variant?: ColorSampleVariant; +}; + +export const colorLabels: Record = { + green: 'Green', + turquoise: 'Turquoise', + sky: 'Sky', + blue: 'Blue', + purple: 'Purple', + pink: 'Pink', + red: 'Red', + orange: 'Orange', + yellow: 'Yellow', + gray: 'Gray', }; export const MenuItemSelectColor = ({ color, - text, selected, className, onClick, disabled, hovered, + variant = 'default', }: MenuItemSelectColorProps) => { const theme = useTheme(); @@ -50,8 +57,10 @@ export const MenuItemSelectColor = ({ hovered={hovered} > - - {text} + + + {colorLabels[color]} + {selected && } diff --git a/front/src/modules/ui/navigation/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx b/front/src/modules/ui/navigation/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx index db602821d..5678a4345 100644 --- a/front/src/modules/ui/navigation/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx +++ b/front/src/modules/ui/navigation/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx @@ -1,6 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; -import { tagLight } from '@/ui/theme/constants/tag'; +import { ColorSampleVariant } from '@/ui/display/color/components/ColorSample'; +import { mainColorNames, ThemeColor } from '@/ui/theme/constants/colors'; import { CatalogDecorator, CatalogDimension, @@ -21,32 +22,22 @@ export default meta; type Story = StoryObj; export const Default: Story = { - args: { - text: 'First option', - color: 'green', - }, - argTypes: { - className: { control: false }, - }, + args: { color: 'green' }, + argTypes: { className: { control: false } }, decorators: [ComponentDecorator], }; export const Catalog: CatalogStory = { - args: { text: 'Menu item' }, - argTypes: { - className: { control: false }, - }, + argTypes: { className: { control: false } }, parameters: { pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, catalog: { dimensions: [ { name: 'color', - values: Object.keys(tagLight.background), - props: (color: string) => ({ - color: color, - }), - labels: (color: string) => color, + values: mainColorNames, + props: (color: ThemeColor) => ({ color }), + labels: (color: ThemeColor) => color, }, { name: 'states', @@ -75,6 +66,12 @@ export const Catalog: CatalogStory = { } }, }, + { + name: 'variant', + values: ['default', 'pipeline'], + props: (variant: ColorSampleVariant) => ({ variant }), + labels: (variant: ColorSampleVariant) => variant, + }, ] as CatalogDimension[], options: { elementContainer: { diff --git a/front/src/modules/ui/object/field/types/guards/isFieldSelectValue.ts b/front/src/modules/ui/object/field/types/guards/isFieldSelectValue.ts index 5abc60965..6ff57f649 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldSelectValue.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldSelectValue.ts @@ -1,10 +1,9 @@ import { z } from 'zod'; -import { mainColors, ThemeColor } from '@/ui/theme/constants/colors'; +import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema'; -const selectColors = Object.keys(mainColors) as [ThemeColor, ...ThemeColor[]]; const selectValueSchema = z.object({ - color: z.enum(selectColors), + color: themeColorSchema, label: z.string(), }); diff --git a/front/src/modules/ui/object/record-board/components/RecordBoardColumnEditTitleMenu.tsx b/front/src/modules/ui/object/record-board/components/RecordBoardColumnEditTitleMenu.tsx index 4f5ea98bb..7ba9aa065 100644 --- a/front/src/modules/ui/object/record-board/components/RecordBoardColumnEditTitleMenu.tsx +++ b/front/src/modules/ui/object/record-board/components/RecordBoardColumnEditTitleMenu.tsx @@ -7,7 +7,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItemSelectColor } from '@/ui/navigation/menu-item/components/MenuItemSelectColor'; -import { ThemeColor } from '@/ui/theme/constants/colors'; +import { mainColorNames, ThemeColor } from '@/ui/theme/constants/colors'; import { textInputStyle } from '@/ui/theme/constants/effects'; import { debounce } from '~/utils/debounce'; @@ -49,24 +49,6 @@ type RecordBoardColumnEditTitleMenuProps = { stageId: string; }; -type ColumnColorOption = { - name: string; - id: ThemeColor; -}; - -export const COLUMN_COLOR_OPTIONS: ColumnColorOption[] = [ - { name: 'Green', id: 'green' }, - { name: 'Turquoise', id: 'turquoise' }, - { name: 'Sky', id: 'sky' }, - { name: 'Blue', id: 'blue' }, - { name: 'Purple', id: 'purple' }, - { name: 'Pink', id: 'pink' }, - { name: 'Red', id: 'red' }, - { name: 'Orange', id: 'orange' }, - { name: 'Yellow', id: 'yellow' }, - { name: 'Gray', id: 'gray' }, -]; - export const RecordBoardColumnEditTitleMenu = ({ onClose, onDelete, @@ -124,15 +106,13 @@ export const RecordBoardColumnEditTitleMenu = ({ /> - {COLUMN_COLOR_OPTIONS.map((colorOption) => ( + {mainColorNames.map((colorName) => ( { - handleColorChange(colorOption.id); - }} - color={colorOption.id} - selected={colorOption.id === color} - text={colorOption.name} + key={colorName} + onClick={() => handleColorChange(colorName)} + color={colorName} + selected={colorName === color} + variant="pipeline" /> ))} diff --git a/front/src/modules/ui/object/record-board/components/__stories__/RecordBoardColumnEditTitleMenu.stories.tsx b/front/src/modules/ui/object/record-board/components/__stories__/RecordBoardColumnEditTitleMenu.stories.tsx index bb818e778..35f9526cf 100644 --- a/front/src/modules/ui/object/record-board/components/__stories__/RecordBoardColumnEditTitleMenu.stories.tsx +++ b/front/src/modules/ui/object/record-board/components/__stories__/RecordBoardColumnEditTitleMenu.stories.tsx @@ -2,21 +2,12 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; -import { - COLUMN_COLOR_OPTIONS, - RecordBoardColumnEditTitleMenu, -} from '../RecordBoardColumnEditTitleMenu'; +import { RecordBoardColumnEditTitleMenu } from '../RecordBoardColumnEditTitleMenu'; const meta: Meta = { title: 'UI/Layout/Board/BoardColumnMenu', component: RecordBoardColumnEditTitleMenu, decorators: [ComponentDecorator], - argTypes: { - color: { - control: 'select', - options: COLUMN_COLOR_OPTIONS.map(({ id }) => id), - }, - }, args: { color: 'green', title: 'Column title' }, }; diff --git a/front/src/modules/ui/theme/constants/colors.ts b/front/src/modules/ui/theme/constants/colors.ts index 1425b06f6..86dbfed88 100644 --- a/front/src/modules/ui/theme/constants/colors.ts +++ b/front/src/modules/ui/theme/constants/colors.ts @@ -24,7 +24,6 @@ export const grayScale = { }; export const mainColors = { - yellow: '#ffd338', green: '#55ef3c', turquoise: '#15de8f', sky: '#00e0ff', @@ -33,11 +32,14 @@ export const mainColors = { pink: '#f54bd0', red: '#f83e3e', orange: '#ff7222', + yellow: '#ffd338', gray: grayScale.gray30, }; export type ThemeColor = keyof typeof mainColors; +export const mainColorNames = Object.keys(mainColors) as ThemeColor[]; + export const secondaryColors = { yellow80: '#2e2a1a', yellow70: '#453d1e', diff --git a/front/src/modules/ui/theme/utils/castStringAsThemeColor.ts b/front/src/modules/ui/theme/utils/castStringAsThemeColor.ts deleted file mode 100644 index e4ed73779..000000000 --- a/front/src/modules/ui/theme/utils/castStringAsThemeColor.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mainColors, ThemeColor } from '../constants/colors'; - -export const COLORS = Object.keys(mainColors); - -export const isThemeColor = (color: string): color is ThemeColor => - COLORS.includes(color); diff --git a/front/src/modules/ui/theme/utils/themeColorSchema.ts b/front/src/modules/ui/theme/utils/themeColorSchema.ts new file mode 100644 index 000000000..ec8bafba2 --- /dev/null +++ b/front/src/modules/ui/theme/utils/themeColorSchema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +import { mainColorNames, ThemeColor } from '../constants/colors'; + +export const themeColorSchema = z.enum( + mainColorNames as [ThemeColor, ...ThemeColor[]], +);