Success
diff --git a/front-packages/akeneo-design-system/jest-puppeteer.config.js b/front-packages/akeneo-design-system/jest-puppeteer.config.js
index a8453deb5356..2424d597b124 100644
--- a/front-packages/akeneo-design-system/jest-puppeteer.config.js
+++ b/front-packages/akeneo-design-system/jest-puppeteer.config.js
@@ -2,6 +2,7 @@ module.exports = {
launch: {
dumpio: true,
headless: true,
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
},
server: {
command: 'yarn http-server storybook-static -p 6006',
diff --git a/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-avatar-list-correctly-1-snap.png b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-avatar-list-correctly-1-snap.png
new file mode 100644
index 000000000000..9b3c79354d0a
Binary files /dev/null and b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-avatar-list-correctly-1-snap.png differ
diff --git a/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-avatar-size-correctly-1-snap.png b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-avatar-size-correctly-1-snap.png
new file mode 100644
index 000000000000..1e69520884d1
Binary files /dev/null and b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-avatar-size-correctly-1-snap.png differ
diff --git a/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-background-colors-correctly-1-snap.png b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-background-colors-correctly-1-snap.png
new file mode 100644
index 000000000000..4f63adac7037
Binary files /dev/null and b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-background-colors-correctly-1-snap.png differ
diff --git a/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-standard-correctly-1-snap.png b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-standard-correctly-1-snap.png
new file mode 100644
index 000000000000..84053f15b813
Binary files /dev/null and b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-standard-correctly-1-snap.png differ
diff --git a/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-with-image-correctly-1-snap.png b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-with-image-correctly-1-snap.png
new file mode 100644
index 000000000000..d5ac6fa368fc
Binary files /dev/null and b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-with-image-correctly-1-snap.png differ
diff --git a/front-packages/akeneo-design-system/src/components/Avatar/Avatar.stories.mdx b/front-packages/akeneo-design-system/src/components/Avatar/Avatar.stories.mdx
new file mode 100644
index 000000000000..0ec0bfd22941
--- /dev/null
+++ b/front-packages/akeneo-design-system/src/components/Avatar/Avatar.stories.mdx
@@ -0,0 +1,147 @@
+import {Meta, Story, ArgsTable, Canvas} from '@storybook/addon-docs';
+import {Avatar} from './Avatar.tsx';
+import {Avatars} from './Avatars.tsx';
+
+
+
+# Avatar
+
+## Usage
+
+This component is used to display users avatars. If no avatar is available, first letters of the first and last name are
+displayed with a dedicated color.
+
+## Playground
+
+
+
+
+
+## Variation on background colors
+
+The background color is based from the username.
+
+
+
+## Variation with image
+
+
+
+## Variation on List
+
+You can use a dedicated component to display avatar list. After a defined maximum, other avatars are not displayed.
+
+
+
+## Variation on Size
+
+
diff --git a/front-packages/akeneo-design-system/src/components/Avatar/Avatar.tsx b/front-packages/akeneo-design-system/src/components/Avatar/Avatar.tsx
new file mode 100644
index 000000000000..7d79ef3399ac
--- /dev/null
+++ b/front-packages/akeneo-design-system/src/components/Avatar/Avatar.tsx
@@ -0,0 +1,102 @@
+import React, {useMemo} from 'react';
+import styled, {css} from 'styled-components';
+import {useTheme} from '../../hooks';
+import {Override} from '../../shared';
+import {AkeneoThemedProps, getColor} from '../../theme';
+
+const AvatarContainer = styled.span
`
+ ${({size}) =>
+ size === 'default'
+ ? css`
+ height: 32px;
+ width: 32px;
+ line-height: 32px;
+ font-size: 15px;
+ border-radius: 32px;
+ `
+ : css`
+ height: 140px;
+ width: 140px;
+ line-height: 140px;
+ font-size: 66px;
+ border-radius: 140px;
+ `}
+ display: inline-block;
+ color: ${getColor('white')};
+ text-align: center;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ text-transform: uppercase;
+ cursor: ${({onClick}) => (onClick ? 'pointer' : 'default')};
+`;
+
+type AvatarProps = Override<
+ React.HTMLAttributes,
+ {
+ /**
+ * Username to use as fallback if the avatar is not provided and the Firstname and Lastname are empty.
+ */
+ username: string;
+
+ /**
+ * Firstname to use as fallback with the Lastname if the avatar is not provided.
+ */
+ firstName: string;
+
+ /**
+ * Lastname to use as fallback with the Firstname if the avatar is not provided.
+ */
+ lastName: string;
+
+ /**
+ * Url of the avatar image.
+ */
+ avatarUrl?: string;
+
+ /**
+ * Size of the avatar.
+ */
+ size?: 'default' | 'big';
+ }
+>;
+
+const Avatar = ({username, firstName, lastName, avatarUrl, size = 'default', ...rest}: AvatarProps) => {
+ const theme = useTheme();
+
+ const fallback = (
+ firstName.trim().charAt(0) + lastName.trim().charAt(0) || username.substring(0, 2)
+ ).toLocaleUpperCase();
+ const title = `${firstName} ${lastName}`.trim() || username;
+
+ const backgroundColor = useMemo(() => {
+ const colorId = username.split('').reduce((s, l) => s + l.charCodeAt(0), 0);
+ const colors = [
+ theme.colorAlternative.green120,
+ theme.colorAlternative.darkCyan120,
+ theme.colorAlternative.forestGreen120,
+ theme.colorAlternative.oliveGreen120,
+ theme.colorAlternative.blue120,
+ theme.colorAlternative.darkBlue120,
+ theme.colorAlternative.hotPink120,
+ theme.colorAlternative.red120,
+ theme.colorAlternative.coralRed120,
+ theme.colorAlternative.yellow120,
+ theme.colorAlternative.orange120,
+ theme.colorAlternative.chocolate120,
+ ];
+
+ return colors[colorId % colors.length];
+ }, [theme, username]);
+
+ const style = avatarUrl ? {backgroundImage: `url(${avatarUrl})`} : {backgroundColor};
+
+ return (
+
+ {avatarUrl ? '' : fallback}
+
+ );
+};
+
+export {Avatar};
+export type {AvatarProps};
diff --git a/front-packages/akeneo-design-system/src/components/Avatar/Avatar.unit.tsx b/front-packages/akeneo-design-system/src/components/Avatar/Avatar.unit.tsx
new file mode 100644
index 000000000000..816f0397f650
--- /dev/null
+++ b/front-packages/akeneo-design-system/src/components/Avatar/Avatar.unit.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import {render, screen} from '../../storybook/test-util';
+import {Avatar} from './Avatar';
+
+test('renders', () => {
+ render();
+
+ const avatar = screen.getByTitle('John Doe');
+ expect(avatar).toBeInTheDocument();
+});
+
+test('avatar image', () => {
+ render();
+
+ const avatar = screen.getByTitle('John Doe');
+ expect(avatar).toHaveStyle('background-image: url(path/to/image)');
+});
+
+test('deterministic fallback color', () => {
+ render();
+
+ const avatar = screen.getByTitle('John Doe');
+ expect(avatar).toHaveStyle('background-color: rgb(68, 31, 0)');
+});
+
+test('fallback to firstname + lastname', () => {
+ render();
+
+ const avatar = screen.getByTitle('John Doe');
+ expect(avatar).toHaveTextContent('JD');
+});
+
+test('fallback to firstname only', () => {
+ render();
+
+ const avatar = screen.getByTitle('John');
+ expect(avatar).toHaveTextContent('J');
+});
+
+test('fallback to lastname only', () => {
+ render();
+
+ const avatar = screen.getByTitle('Doe');
+ expect(avatar).toHaveTextContent('D');
+});
+
+test('fallback to username', () => {
+ render();
+
+ const avatar = screen.getByTitle('john');
+ expect(avatar).toHaveTextContent('JO');
+});
+
+test('initial are converted to uppercase', () => {
+ render();
+
+ const avatar = screen.getByTitle('john doe');
+ expect(avatar).toHaveTextContent('JD');
+});
+
+test('size default', () => {
+ render();
+
+ const avatar = screen.getByTitle('John Doe');
+ expect(avatar).toHaveStyle('width: 32px');
+});
+
+test('size big', () => {
+ render();
+
+ const avatar = screen.getByTitle('John Doe');
+ expect(avatar).toHaveStyle('width: 140px');
+});
+
+test('supports ...rest props', () => {
+ render();
+
+ expect(screen.getByTestId('my_value')).toBeInTheDocument();
+});
diff --git a/front-packages/akeneo-design-system/src/components/Avatar/Avatars.tsx b/front-packages/akeneo-design-system/src/components/Avatar/Avatars.tsx
new file mode 100644
index 000000000000..dbd5a1178d82
--- /dev/null
+++ b/front-packages/akeneo-design-system/src/components/Avatar/Avatars.tsx
@@ -0,0 +1,50 @@
+import React, {Children} from 'react';
+import styled from 'styled-components';
+import {Override} from '../../shared';
+import {AkeneoThemedProps, getColor} from '../../theme';
+
+const AvatarListContainer = styled.div`
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ & > * {
+ margin-right: -4px;
+ position: relative;
+ }
+`;
+
+const RemainingAvatar = styled.span`
+ height: 32px;
+ width: 32px;
+ display: inline-block;
+ border: 1px solid ${getColor('grey', 10)};
+ line-height: 32px;
+ text-align: center;
+ font-size: 15px;
+ border-radius: 32px;
+ background-color: ${getColor('white')};
+`;
+
+type AvatarsProps = Override<
+ React.HTMLAttributes,
+ {
+ max: number;
+ }
+>;
+
+const Avatars = ({max, children, ...rest}: AvatarsProps) => {
+ const childrenArray = Children.toArray(children);
+ const displayedChildren = childrenArray.slice(0, max);
+ const remainingChildrenCount = childrenArray.length - max;
+ const reverseChildren = displayedChildren.reverse();
+
+ return (
+
+ {remainingChildrenCount > 0 && +{remainingChildrenCount}}
+ {reverseChildren}
+
+ );
+};
+
+export {Avatars};
+export type {AvatarsProps};
diff --git a/front-packages/akeneo-design-system/src/components/Avatar/Avatars.unit.tsx b/front-packages/akeneo-design-system/src/components/Avatar/Avatars.unit.tsx
new file mode 100644
index 000000000000..2f8ac9ea39b4
--- /dev/null
+++ b/front-packages/akeneo-design-system/src/components/Avatar/Avatars.unit.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import {render, screen} from '../../storybook/test-util';
+import {Avatar} from './Avatar';
+import {Avatars} from './Avatars';
+
+test('renders multiple avatars', () => {
+ render(
+
+
+
+
+ );
+
+ expect(screen.getByTitle('John Doe')).toBeInTheDocument();
+ expect(screen.getByTitle('Leonard Doe')).toBeInTheDocument();
+});
+
+test('renders a maximum number of avatars', () => {
+ render(
+
+
+
+
+ );
+
+ expect(screen.getByTitle('John Doe')).toBeInTheDocument();
+ expect(screen.queryByTitle('Leonard Doe')).not.toBeInTheDocument();
+ expect(screen.getByText('+1')).toBeInTheDocument();
+});
+
+test('renders no avatar', () => {
+ render();
+
+ expect(screen.queryByTitle('+1')).not.toBeInTheDocument();
+});
+
+test('supports ...rest props', () => {
+ render();
+
+ expect(screen.getByTestId('my_value')).toBeInTheDocument();
+});
diff --git a/front-packages/akeneo-design-system/src/components/Input/BooleanInput/BooleanInput.tsx b/front-packages/akeneo-design-system/src/components/Input/BooleanInput/BooleanInput.tsx
index 214cd7e077e1..f5b78c4172aa 100644
--- a/front-packages/akeneo-design-system/src/components/Input/BooleanInput/BooleanInput.tsx
+++ b/front-packages/akeneo-design-system/src/components/Input/BooleanInput/BooleanInput.tsx
@@ -149,7 +149,7 @@ type BooleanInputProps = Override<
clearLabel?: string;
}
) & {
- readOnly: boolean;
+ readOnly?: boolean;
yesLabel: string;
noLabel: string;
invalid?: boolean;
@@ -165,7 +165,7 @@ const BooleanInput = React.forwardRef(
(
{
value,
- readOnly,
+ readOnly = false,
onChange,
clearable = false,
yesLabel,
diff --git a/front-packages/akeneo-design-system/src/components/Input/MultiSelectInput/MultiSelectInput.tsx b/front-packages/akeneo-design-system/src/components/Input/MultiSelectInput/MultiSelectInput.tsx
index 05d0b6b42538..7b196793ecd3 100644
--- a/front-packages/akeneo-design-system/src/components/Input/MultiSelectInput/MultiSelectInput.tsx
+++ b/front-packages/akeneo-design-system/src/components/Input/MultiSelectInput/MultiSelectInput.tsx
@@ -150,17 +150,24 @@ type MultiMultiSelectInputProps = Override<
* Callback called when the user hit enter on the field.
*/
onSubmit?: () => void;
-
- /**
- * Handler called when the next page is almost reached.
- */
- onNextPage?: () => void;
-
- /**
- * Handler called when the search value changed
- */
- onSearchChange?: (searchValue: string) => void;
- }
+ } & (
+ | {
+ /**
+ * Handler called when the next page is almost reached.
+ */
+ onNextPage?: () => void;
+ /**
+ * Handler called when the search value changed
+ */
+ onSearchChange?: (searchValue: string) => void;
+ disableInternalSearch?: false;
+ }
+ | {
+ onNextPage: () => void;
+ onSearchChange: (searchValue: string) => void;
+ disableInternalSearch: true;
+ }
+ )
>;
/**
@@ -183,6 +190,7 @@ const MultiSelectInput = ({
verticalPosition,
onNextPage,
onSearchChange,
+ disableInternalSearch = false,
'aria-labelledby': ariaLabelledby,
...rest
}: MultiMultiSelectInputProps) => {
@@ -211,12 +219,14 @@ const MultiSelectInput = ({
return indexedChips;
}, {});
- const filteredChildren = validChildren.filter(({props}) => {
- const childValue = props.value;
- const optionValue = childValue + props.children;
+ const filteredChildren = disableInternalSearch
+ ? validChildren
+ : validChildren.filter(({props}) => {
+ const childValue = props.value;
+ const optionValue = childValue + props.children;
- return !value.includes(childValue) && optionValue.toLowerCase().includes(searchValue.toLowerCase());
- });
+ return !value.includes(childValue) && optionValue.toLowerCase().includes(searchValue.toLowerCase());
+ });
const handleEnter = () => {
if (filteredChildren.length > 0 && dropdownIsOpen) {
diff --git a/front-packages/akeneo-design-system/src/components/Input/MultiSelectInput/MultiSelectInput.unit.tsx b/front-packages/akeneo-design-system/src/components/Input/MultiSelectInput/MultiSelectInput.unit.tsx
index c0e9ff2438ed..b7ae72c0aa30 100644
--- a/front-packages/akeneo-design-system/src/components/Input/MultiSelectInput/MultiSelectInput.unit.tsx
+++ b/front-packages/akeneo-design-system/src/components/Input/MultiSelectInput/MultiSelectInput.unit.tsx
@@ -77,6 +77,51 @@ test('it handles search', () => {
expect(onChange).toHaveBeenCalledTimes(2);
});
+test('it handles external search', () => {
+ const onChange = jest.fn();
+ const onNextPage = jest.fn();
+ const onSearchChange = jest.fn();
+
+ const observe = jest.fn();
+ const unobserve = jest.fn();
+ window.IntersectionObserver = jest.fn(() => ({
+ observe,
+ unobserve,
+ })) as unknown as jest.Mock;
+
+ render(
+
+ English
+ French
+ German
+ Spanish
+
+ );
+
+ const input = screen.getByRole('textbox');
+ fireEvent.click(input);
+ fireEvent.change(input, {target: {value: 'Fr'}});
+
+ const germanOption = screen.queryByText('German');
+ expect(germanOption).toBeInTheDocument();
+ const usOption = screen.queryByText('English');
+ expect(usOption).toBeInTheDocument();
+ const spanishOption = screen.queryByText('Spanish');
+ expect(spanishOption).toBeInTheDocument();
+ const frenchOption = screen.getByText('French');
+ expect(frenchOption).toBeInTheDocument();
+});
+
test('it handles empty cases', () => {
const onChange = jest.fn();
render(
diff --git a/front-packages/akeneo-design-system/src/components/Input/SelectInput/SelectInput.tsx b/front-packages/akeneo-design-system/src/components/Input/SelectInput/SelectInput.tsx
index f83c1aa897c3..cc675ffddfe1 100644
--- a/front-packages/akeneo-design-system/src/components/Input/SelectInput/SelectInput.tsx
+++ b/front-packages/akeneo-design-system/src/components/Input/SelectInput/SelectInput.tsx
@@ -178,17 +178,24 @@ type SelectInputProps = Override<
* Force the vertical position of the overlay.
*/
verticalPosition?: VerticalPosition;
-
- /**
- * Handler called when the next page is almost reached.
- */
- onNextPage?: () => void;
-
- /**
- * Handler called when the search value changed
- */
- onSearchChange?: (searchValue: string) => void;
- }
+ } & (
+ | {
+ /**
+ * Handler called when the next page is almost reached.
+ */
+ onNextPage?: () => void;
+ /**
+ * Handler called when the search value changed
+ */
+ onSearchChange?: (searchValue: string) => void;
+ disableInternalSearch?: false;
+ }
+ | {
+ onNextPage: () => void;
+ onSearchChange: (searchValue: string) => void;
+ disableInternalSearch: true;
+ }
+ )
>;
/**
@@ -209,6 +216,7 @@ const SelectInput = ({
verticalPosition,
onNextPage,
onSearchChange,
+ disableInternalSearch = false,
'aria-labelledby': ariaLabelledby,
...rest
}: SelectInputProps) => {
@@ -235,14 +243,16 @@ const SelectInput = ({
return optionCodes;
}, []);
- const filteredChildren = validChildren.filter(child => {
- const content = typeof child.props.children === 'string' ? child.props.children : '';
- const title = child.props.title ?? '';
- const value = child.props.value;
- const optionValue = value + content + title;
+ const filteredChildren = disableInternalSearch
+ ? validChildren
+ : validChildren.filter(child => {
+ const content = typeof child.props.children === 'string' ? child.props.children : '';
+ const title = child.props.title ?? '';
+ const value = child.props.value;
+ const optionValue = value + content + title;
- return optionValue.toLowerCase().includes(searchValue.toLowerCase());
- });
+ return optionValue.toLowerCase().includes(searchValue.toLowerCase());
+ });
const currentValueElement =
validChildren.find(child => {
diff --git a/front-packages/akeneo-design-system/src/components/Input/SelectInput/SelectInput.unit.tsx b/front-packages/akeneo-design-system/src/components/Input/SelectInput/SelectInput.unit.tsx
index b27153f57588..da1abe9ae486 100644
--- a/front-packages/akeneo-design-system/src/components/Input/SelectInput/SelectInput.unit.tsx
+++ b/front-packages/akeneo-design-system/src/components/Input/SelectInput/SelectInput.unit.tsx
@@ -95,6 +95,58 @@ test('it handles search', () => {
expect(onChange).toHaveBeenCalledWith('es_ES');
});
+test('it handles external search', () => {
+ const onChange = jest.fn();
+ const onNextPage = jest.fn();
+ const onSearchChange = jest.fn();
+
+ const observe = jest.fn();
+ const unobserve = jest.fn();
+ window.IntersectionObserver = jest.fn(() => ({
+ observe,
+ unobserve,
+ })) as unknown as jest.Mock;
+
+ render(
+
+
+
+
+
+ Français
+
+
+
+
+
+
+
+
+ );
+
+ const input = screen.getByRole('textbox');
+ fireEvent.click(input);
+ fireEvent.change(input, {target: {value: 'Français'}});
+
+ const germanOption = screen.queryByText('German');
+ expect(germanOption).toBeInTheDocument();
+ const usOption = screen.queryByText('English');
+ expect(usOption).toBeInTheDocument();
+ const spanishOption = screen.queryByText('Spanish');
+ expect(spanishOption).toBeInTheDocument();
+ const frenchOption = screen.getByText('Français');
+ expect(frenchOption).toBeInTheDocument();
+});
+
test('it handles empty cases', () => {
const onChange = jest.fn();
render(
diff --git a/front-packages/akeneo-design-system/src/components/Input/TableInput/TableInputRow/TableInputRow.tsx b/front-packages/akeneo-design-system/src/components/Input/TableInput/TableInputRow/TableInputRow.tsx
index f694f095f46d..a01608ff78a0 100644
--- a/front-packages/akeneo-design-system/src/components/Input/TableInput/TableInputRow/TableInputRow.tsx
+++ b/front-packages/akeneo-design-system/src/components/Input/TableInput/TableInputRow/TableInputRow.tsx
@@ -96,15 +96,10 @@ const TableInputTr = styled.tr<
height: 0;
margin-top: -1px;
}
- &:has(div) {
- background: red !important;
- }
border-bottom-color: ${getColor('blue', 100)};
- &:first-child {
- border-left: 1px solid ${getColor('blue', 100)};
- }
+ border-left-color: ${getColor('blue', 100)};
&:last-child {
- border-right: 1px solid ${getColor('blue', 100)};
+ border-right-color: ${getColor('blue', 100)};
}
}
`}
diff --git a/front-packages/akeneo-design-system/src/components/Input/TagInput/TagInput.tsx b/front-packages/akeneo-design-system/src/components/Input/TagInput/TagInput.tsx
index 1a6a21392b3b..8d1994ae5d3b 100644
--- a/front-packages/akeneo-design-system/src/components/Input/TagInput/TagInput.tsx
+++ b/front-packages/akeneo-design-system/src/components/Input/TagInput/TagInput.tsx
@@ -55,12 +55,12 @@ const TagText = styled.span`
white-space: nowrap;
`;
-const InputContainer = styled.li`
+const InputContainer = styled.li`
list-style-type: none;
color: ${getColor('grey', 120)};
border: 0;
flex: 1;
- padding: 0;
+ padding: ${({hasTags}) => (hasTags ? '0' : '0 11px')};
align-items: center;
display: flex;
@@ -255,7 +255,7 @@ const TagInput: FC = ({
);
})}
-
+ 0}>