From 30aeea9eec09ac08771c1c526234bb3ae69c0029 Mon Sep 17 00:00:00 2001
From: bosiraphael <71827178+bosiraphael@users.noreply.github.com>
Date: Fri, 13 Oct 2023 15:29:30 +0200
Subject: [PATCH] 1909 object edit add icon section (#1995)
* wip
* wip
* wip
* wip
* wip
* remove hardcoded values and use theme values
* add styles to StyledContainer
* fix iconPicker bug
* wip
* refactor IconPicker to include IconButton
* close IconPicker on click outside
* close IconPicker on escape and enter
* refactor to use DropDownMenu
* refactor to use DropDownMenu
* modify default icon
* Refactor to use useIconPicker hook
* fix WithSearch story
* reinitialized searchString state on close
* create and update stories for the iconPicker
* remove comments
* use theme for gap
* remove align-self
* fix typo in icon
* fix type any
* fix merge conflicts
* remove experimental css properties
---
.../modules/settings/assets/ArrowRight.svg | 3 +
.../settings/components/IconWithLabel.tsx | 42 +++++++++
.../components/SettingsIconSection.tsx | 55 +++++++++++
.../ui/input/components/IconPicker.tsx | 94 +++++++++++++------
.../__stories__/IconPicker.stories.tsx | 63 ++++++++++++-
.../modules/ui/input/hooks/useIconPicker.ts | 13 +++
.../ui/input/states/iconPickerState.ts | 15 +++
.../ui/input/types/IconPickerHotkeyScope.ts | 3 +
.../src/pages/settings/SettingsNewObject.tsx | 40 +++++---
9 files changed, 287 insertions(+), 41 deletions(-)
create mode 100644 front/src/modules/settings/assets/ArrowRight.svg
create mode 100644 front/src/modules/settings/components/IconWithLabel.tsx
create mode 100644 front/src/modules/settings/components/SettingsIconSection.tsx
create mode 100644 front/src/modules/ui/input/hooks/useIconPicker.ts
create mode 100644 front/src/modules/ui/input/states/iconPickerState.ts
create mode 100644 front/src/modules/ui/input/types/IconPickerHotkeyScope.ts
diff --git a/front/src/modules/settings/assets/ArrowRight.svg b/front/src/modules/settings/assets/ArrowRight.svg
new file mode 100644
index 000000000..28249ab56
--- /dev/null
+++ b/front/src/modules/settings/assets/ArrowRight.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/front/src/modules/settings/components/IconWithLabel.tsx b/front/src/modules/settings/components/IconWithLabel.tsx
new file mode 100644
index 000000000..1470b32dc
--- /dev/null
+++ b/front/src/modules/settings/components/IconWithLabel.tsx
@@ -0,0 +1,42 @@
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+
+import { IconComponent } from '@/ui/icon/types/IconComponent';
+
+type IconWithLabelProps = {
+ Icon: IconComponent;
+ label: string;
+};
+
+const StyledContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(3)};
+ padding: ${({ theme }) => theme.spacing(1)};
+`;
+
+const StyledSubContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(1)};
+`;
+const StyledItemLabel = styled.div`
+ color: ${({ theme }) => theme.font.color.secondary};
+ font-size: ${({ theme }) => theme.font.size.sm};
+ font-style: normal;
+ font-weight: ${({ theme }) => theme.font.size.md};
+ line-height: ${({ theme }) => theme.text.lineHeight.md};
+`;
+
+export const IconWithLabel = ({ Icon, label }: IconWithLabelProps) => {
+ const theme = useTheme();
+
+ return (
+
+
+
+ {label}
+
+
+ );
+};
diff --git a/front/src/modules/settings/components/SettingsIconSection.tsx b/front/src/modules/settings/components/SettingsIconSection.tsx
new file mode 100644
index 000000000..6224032da
--- /dev/null
+++ b/front/src/modules/settings/components/SettingsIconSection.tsx
@@ -0,0 +1,55 @@
+import styled from '@emotion/styled';
+
+import { IconComponent } from '@/ui/icon/types/IconComponent';
+import { IconPicker } from '@/ui/input/components/IconPicker';
+import { H2Title } from '@/ui/typography/components/H2Title';
+
+import ArrowRight from '../assets/ArrowRight.svg';
+
+import { IconWithLabel } from './IconWithLabel';
+
+const StyledContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(4)};
+`;
+
+const StyledArrowContainer = styled.div`
+ align-items: center;
+ display: flex;
+ height: 32px;
+ justify-content: center;
+`;
+
+type SettingsIconSectionProps = {
+ Icon: IconComponent;
+ iconKey: string;
+ setIconPicker: (icon: { Icon: IconComponent; iconKey: string }) => void;
+};
+
+export const SettingsIconSection = ({
+ Icon,
+ iconKey,
+ setIconPicker,
+}: SettingsIconSectionProps) => {
+ return (
+
+
+
+ {
+ setIconPicker({ Icon: icon.Icon, iconKey: icon.iconKey });
+ }}
+ />
+
+
+
+
+
+
+ );
+};
diff --git a/front/src/modules/ui/input/components/IconPicker.tsx b/front/src/modules/ui/input/components/IconPicker.tsx
index 860739bd0..8b771356c 100644
--- a/front/src/modules/ui/input/components/IconPicker.tsx
+++ b/front/src/modules/ui/input/components/IconPicker.tsx
@@ -1,28 +1,36 @@
import { useEffect, useMemo, useState } from 'react';
import styled from '@emotion/styled';
+import { IconButton } from '@/ui/button/components/IconButton';
import { LightIconButton } from '@/ui/button/components/LightIconButton';
+import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput';
-import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
+import { useDropdown } from '@/ui/dropdown/hooks/useDropdown';
+import { DropdownScope } from '@/ui/dropdown/scopes/DropdownScope';
import { IconComponent } from '@/ui/icon/types/IconComponent';
+import { IconApps } from '../constants/icons';
import { DropdownMenuSkeletonItem } from '../relation-picker/components/skeletons/DropdownMenuSkeletonItem';
+import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
type IconPickerProps = {
onChange: (params: { iconKey: string; Icon: IconComponent }) => void;
selectedIconKey?: string;
+ onClickOutside?: () => void;
+ onClose?: () => void;
+ onOpen?: () => void;
};
-const StyledIconPickerDropdownMenu = styled(StyledDropdownMenu)`
+const StyledContainer = styled.div`
width: 176px;
`;
-const StyledMenuIconItemsContainer = styled(DropdownMenuItemsContainer)`
+const StyledMenuIconItemsContainer = styled.div`
+ display: flex;
flex-direction: row;
flex-wrap: wrap;
- height: auto;
`;
const StyledLightIconButton = styled(LightIconButton)<{ isSelected?: boolean }>`
@@ -33,11 +41,19 @@ const StyledLightIconButton = styled(LightIconButton)<{ isSelected?: boolean }>`
const convertIconKeyToLabel = (iconKey: string) =>
iconKey.replace(/[A-Z]/g, (letter) => ` ${letter}`).trim();
-export const IconPicker = ({ onChange, selectedIconKey }: IconPickerProps) => {
+export const IconPicker = ({
+ onChange,
+ selectedIconKey,
+ onClickOutside,
+ onClose,
+ onOpen,
+}: IconPickerProps) => {
const [searchString, setSearchString] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [icons, setIcons] = useState>({});
+ const { closeDropdown } = useDropdown({ dropdownScopeId: 'icon-picker' });
+
useEffect(() => {
import('../constants/icons').then((lazyLoadedIcons) => {
setIcons(lazyLoadedIcons);
@@ -63,28 +79,52 @@ export const IconPicker = ({ onChange, selectedIconKey }: IconPickerProps) => {
}, [icons, searchString, selectedIconKey]);
return (
-
- setSearchString(event.target.value)}
- />
-
-
- {isLoading ? (
-
- ) : (
- iconKeys.map((iconKey) => (
- onChange({ iconKey, Icon: icons[iconKey] })}
+
+
+ }
+ dropdownComponents={
+
+ setSearchString(event.target.value)}
/>
- ))
- )}
-
-
+
+
+ {isLoading ? (
+
+ ) : (
+
+ {iconKeys.map((iconKey) => (
+ {
+ onChange({ iconKey, Icon: icons[iconKey] });
+ closeDropdown();
+ }}
+ />
+ ))}
+
+ )}
+
+
+ }
+ onClickOutside={onClickOutside}
+ onClose={() => {
+ onClose?.();
+ setSearchString('');
+ }}
+ onOpen={onOpen}
+ >
+
);
};
diff --git a/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx b/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx
index be7d26d51..db8041c72 100644
--- a/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx
+++ b/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx
@@ -22,15 +22,40 @@ export const WithSelectedIcon: Story = {
args: { selectedIconKey: 'IconCalendarEvent' },
};
+export const WithOpen: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ const iconPickerButton = await canvas.findByRole('button');
+
+ userEvent.click(iconPickerButton);
+ },
+};
+
+export const WithOpenAndSelectedIcon: Story = {
+ args: { selectedIconKey: 'IconCalendarEvent' },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ const iconPickerButton = await canvas.findByRole('button');
+
+ userEvent.click(iconPickerButton);
+ },
+};
+
export const WithSearch: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
- const searchInput = canvas.getByRole('textbox');
+ const iconPickerButton = await canvas.findByRole('button');
+
+ userEvent.click(iconPickerButton);
+
+ const searchInput = await canvas.findByRole('textbox');
await userEvent.type(searchInput, 'Building skyscraper');
- await sleep(1000);
+ await sleep(100);
const searchedIcon = canvas.getByRole('button', {
name: 'Icon Building Skyscraper',
@@ -39,3 +64,37 @@ export const WithSearch: Story = {
expect(searchedIcon).toBeInTheDocument();
},
};
+
+export const WithSearchAndClose: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ const iconPickerButton = await canvas.findByRole('button');
+
+ userEvent.click(iconPickerButton);
+
+ let searchInput = await canvas.findByRole('textbox');
+
+ await userEvent.type(searchInput, 'Building skyscraper');
+
+ await sleep(100);
+
+ const searchedIcon = canvas.getByRole('button', {
+ name: 'Icon Building Skyscraper',
+ });
+
+ expect(searchedIcon).toBeInTheDocument();
+
+ userEvent.click(searchedIcon);
+
+ await sleep(100);
+
+ userEvent.click(iconPickerButton);
+
+ await sleep(100);
+
+ searchInput = await canvas.findByRole('textbox');
+
+ expect(searchInput).toHaveValue('');
+ },
+};
diff --git a/front/src/modules/ui/input/hooks/useIconPicker.ts b/front/src/modules/ui/input/hooks/useIconPicker.ts
new file mode 100644
index 000000000..141431c71
--- /dev/null
+++ b/front/src/modules/ui/input/hooks/useIconPicker.ts
@@ -0,0 +1,13 @@
+import { useRecoilState } from 'recoil';
+
+import { iconPickerState } from '../states/iconPickerState';
+
+export const useIconPicker = () => {
+ const [iconPicker, setIconPicker] = useRecoilState(iconPickerState);
+
+ return {
+ Icon: iconPicker.Icon,
+ iconKey: iconPicker.iconKey,
+ setIconPicker,
+ };
+};
diff --git a/front/src/modules/ui/input/states/iconPickerState.ts b/front/src/modules/ui/input/states/iconPickerState.ts
new file mode 100644
index 000000000..0aa9c3dec
--- /dev/null
+++ b/front/src/modules/ui/input/states/iconPickerState.ts
@@ -0,0 +1,15 @@
+import { atom } from 'recoil';
+
+import { IconComponent } from '@/ui/icon/types/IconComponent';
+
+import { IconApps } from '../constants/icons';
+
+type IconPickerState = {
+ Icon: IconComponent;
+ iconKey: string;
+};
+
+export const iconPickerState = atom({
+ key: 'iconPickerState',
+ default: { Icon: IconApps, iconKey: 'IconApps' },
+});
diff --git a/front/src/modules/ui/input/types/IconPickerHotkeyScope.ts b/front/src/modules/ui/input/types/IconPickerHotkeyScope.ts
new file mode 100644
index 000000000..f38981837
--- /dev/null
+++ b/front/src/modules/ui/input/types/IconPickerHotkeyScope.ts
@@ -0,0 +1,3 @@
+export enum IconPickerHotkeyScope {
+ IconPicker = 'icon-picker',
+}
diff --git a/front/src/pages/settings/SettingsNewObject.tsx b/front/src/pages/settings/SettingsNewObject.tsx
index e1513c4a0..3655e62e4 100644
--- a/front/src/pages/settings/SettingsNewObject.tsx
+++ b/front/src/pages/settings/SettingsNewObject.tsx
@@ -1,25 +1,41 @@
import styled from '@emotion/styled';
+import { SettingsIconSection } from '@/settings/components/SettingsIconSection';
import { objectSettingsWidth } from '@/settings/objects/constants/objectSettings';
import { Breadcrumb } from '@/ui/breadcrumb/components/Breadcrumb';
import { IconSettings } from '@/ui/icon';
+import { useIconPicker } from '@/ui/input/hooks/useIconPicker';
import { SubMenuTopBarContainer } from '@/ui/layout/components/SubMenuTopBarContainer';
const StyledContainer = styled.div`
+ align-items: flex-start;
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(8)};
height: fit-content;
padding: ${({ theme }) => theme.spacing(8)};
width: ${objectSettingsWidth};
`;
-export const SettingsNewObject = () => (
-
-
-
-
-
-);
+export const SettingsNewObject = () => {
+ const { Icon, iconKey, setIconPicker } = useIconPicker();
+
+ return (
+
+
+
+
+
+
+
+ );
+};