chore(client): new UI for the tier-card (#12536)

* chore(client): tier-card ui update

* minor change

* test(client): for the new UI of tier-card

* address comments

* fix UI

* replace some css with utility classes

* add language locales for word Clear

* some css and test fix

* fix(client): cypress test for add and remove tier
This commit is contained in:
Abhishek Porwal 2023-07-25 11:34:49 +05:30 committed by GitHub
parent 60c9477d18
commit e32b763e15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 213 additions and 237 deletions

View File

@ -62,16 +62,13 @@ const addRemoveOwner = () => {
const addRemoveTier = () => {
cy.get('[data-testid="edit-tier"]').click();
cy.get('[data-testid="card-list"]').first().should('be.visible').as('tier1');
cy.get('@tier1')
.find('[data-testid="icon"] > [data-testid="select-tier-button"]')
.click();
cy.get('@tier1').find('[data-testid="radio-btn"]').click();
verifyResponseStatusCode('@patchOwner', 200);
cy.clickOutside();
cy.get('[data-testid="Tier"]').should('contain', TIER);
cy.get('[data-testid="edit-tier"]').click();
cy.get('[data-testid="card-list"]').first().should('be.visible').as('tier1');
cy.get('@tier1').find('[data-testid="remove-tier"]').click();
cy.get('[data-testid="clear-tier"]').should('be.visible').click();
verifyResponseStatusCode('@patchOwner', 200);
cy.get('[data-testid="Tier"]').should('contain', 'No Tier');

View File

@ -11,7 +11,15 @@
* limitations under the License.
*/
import { findByTestId, render } from '@testing-library/react';
import {
findByTestId,
getAllByTestId,
getByTestId,
getByText,
render,
waitForElementToBeRemoved,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import TierCard from './TierCard';
@ -31,10 +39,19 @@ const mockTierData = [
},
];
const mockGetTags = jest
.fn()
.mockImplementation(() => Promise.resolve({ data: mockTierData }));
const mockOnUpdate = jest.fn();
const mockShowErrorToast = jest.fn();
const mockProps = {
currentTier: 'currentTier',
updateTier: mockOnUpdate,
children: <div>Child</div>,
};
jest.mock('rest/tagAPI', () => ({
getTags: jest
.fn()
.mockImplementation(() => Promise.resolve({ data: mockTierData })),
getTags: jest.fn().mockImplementation(() => mockGetTags()),
}));
jest.mock('../../Loader/Loader', () => {
@ -42,7 +59,7 @@ jest.mock('../../Loader/Loader', () => {
});
jest.mock('../../../utils/ToastUtils', () => {
return jest.fn().mockReturnValue(<div>showErrorToast</div>);
return jest.fn().mockImplementation(() => mockShowErrorToast());
});
// Mock Antd components
@ -51,19 +68,58 @@ jest.mock('antd', () => ({
Popover: jest
.fn()
.mockImplementation(({ content }) => (
<div data-testid="tier-card-container">{content}</div>
)),
.mockImplementation(({ content, onOpenChange, children }) => {
onOpenChange(true);
return (
<>
{content}
{children}
</>
);
}),
}));
const MockOnUpdate = jest.fn();
jest.mock('../rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<div>RichTextEditorPreviewer</div>);
});
describe('Test TierCard Component', () => {
it('Component should have card', async () => {
const { container } = render(
<TierCard currentTier="" updateTier={MockOnUpdate} />
);
const { container } = render(<TierCard {...mockProps} />);
await expect(getByText(container, 'Loader')).toBeInTheDocument();
expect(mockGetTags).toHaveBeenCalled();
expect(await findByTestId(container, 'cards')).toBeInTheDocument();
});
it('should call the mockOnUpdate when click on radio button', async () => {
const { container } = render(<TierCard {...mockProps} />);
await waitForElementToBeRemoved(() => getByText(container, 'Loader'));
const radioBtns = getAllByTestId(container, 'radio-btn');
expect(radioBtns).toHaveLength(1);
userEvent.click(radioBtns[0]);
expect(mockOnUpdate).toHaveBeenCalled();
});
it('should call the mockOnUpdate when click on Clear button', async () => {
const { container } = render(<TierCard {...mockProps} />);
await waitForElementToBeRemoved(() => getByText(container, 'Loader'));
const clearTier = getByTestId(container, 'clear-tier');
expect(clearTier).toBeInTheDocument();
userEvent.click(clearTier);
expect(mockOnUpdate).toHaveBeenCalled();
});
});

View File

@ -11,24 +11,18 @@
* limitations under the License.
*/
import Icon from '@ant-design/icons/lib/components/Icon';
import {
Button,
Card,
Col,
Collapse,
Popover,
Row,
Radio,
RadioChangeEvent,
Space,
Typography,
} from 'antd';
import { ReactComponent as IconRemove } from 'assets/svg/ic-remove.svg';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import Loader from 'components/Loader/Loader';
import { t } from 'i18next';
import { LoadingState } from 'Models';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useState } from 'react';
import { getTags } from 'rest/tagAPI';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { showErrorToast } from '../../../utils/ToastUtils';
@ -39,198 +33,111 @@ import { CardWithListItems, TierCardProps } from './TierCard.interface';
const { Panel } = Collapse;
const TierCard = ({ currentTier, updateTier, children }: TierCardProps) => {
const [tierData, setTierData] = useState<Array<CardWithListItems>>([]);
const [activeTier, setActiveTier] = useState(currentTier);
const [statusTier, setStatusTier] = useState<LoadingState>('initial');
const [isLoadingTierData, setIsLoadingTierData] = useState<boolean>(false);
const handleCardSelection = (cardId: string) => {
setActiveTier(cardId);
};
const setInitialTierLoadingState = () => {
setStatusTier('initial');
};
const getTierData = () => {
const getTierData = async () => {
setIsLoadingTierData(true);
getTags({
parent: 'Tier',
})
.then(({ data }) => {
if (data) {
const tierData: CardWithListItems[] =
data.map((tier: { name: string; description: string }) => ({
id: `Tier${FQN_SEPARATOR_CHAR}${tier.name}`,
title: tier.name,
description: tier.description.substring(
0,
tier.description.indexOf('\n\n')
),
data: tier.description.substring(
tier.description.indexOf('\n\n') + 1
),
})) ?? [];
setTierData(tierData);
} else {
setTierData([]);
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
t('server.entity-fetch-error', {
entity: t('label.tier-plural-lowercase'),
})
);
})
.finally(() => {
setIsLoadingTierData(false);
try {
const { data } = await getTags({
parent: 'Tier',
});
};
const prepareTier = (updatedTier: string) => {
return updatedTier !== currentTier ? updatedTier : undefined;
};
const handleTierSave = (updatedTier: string) => {
setStatusTier('waiting');
const newTier = prepareTier(updatedTier);
updateTier?.(newTier as string);
};
const getTierSelectButton = (tier: string) => {
switch (statusTier) {
case 'waiting':
return (
<Loader
className="d-inline-block"
size="small"
style={{ marginBottom: '-4px' }}
type="default"
/>
);
case 'success':
return (
<Icon
className="text-xl"
component={IconRemove}
data-testid="remove-tier"
onClick={() => updateTier?.()}
/>
);
default:
return (
<Button
data-testid="select-tier-button"
size="small"
type="primary"
onClick={() => handleTierSave(tier)}>
{t('label.select')}
</Button>
);
}
};
const getCardIcon = useCallback(
(cardId: string) => {
const isSelected = currentTier === cardId;
const isActive = activeTier === cardId;
if ((isSelected && isActive) || isSelected) {
return (
<Icon
className="text-xl"
component={IconRemove}
data-testid="remove-tier"
onClick={() => updateTier?.()}
/>
);
} else if (isActive) {
return getTierSelectButton(cardId);
if (data) {
const tierData: CardWithListItems[] =
data.map((tier: { name: string; description: string }) => ({
id: `Tier${FQN_SEPARATOR_CHAR}${tier.name}`,
title: tier.name,
description: tier.description.substring(
0,
tier.description.indexOf('\n\n')
),
data: tier.description.substring(
tier.description.indexOf('\n\n') + 1
),
})) ?? [];
setTierData(tierData);
} else {
return (
<Button
ghost
data-testid="select-tier-button"
size="small"
type="primary"
onClick={() => handleTierSave(cardId)}>
{t('label.select')}
</Button>
);
setTierData([]);
}
},
[currentTier, activeTier]
);
useEffect(() => {
setActiveTier(currentTier);
if (statusTier === 'waiting') {
setStatusTier('success');
setTimeout(() => {
setInitialTierLoadingState();
}, 300);
} catch (err) {
showErrorToast(
err,
t('server.entity-fetch-error', {
entity: t('label.tier-plural-lowercase'),
})
);
} finally {
setIsLoadingTierData(false);
}
}, [currentTier]);
};
const handleTierSelection = ({ target: { value } }: RadioChangeEvent) => {
updateTier?.(value as string);
};
const clearTierSelection = () => {
updateTier?.();
};
return (
<Popover
className="p-0 tier-card-popover"
content={
<Card
className="tier-card"
data-testid="cards"
headStyle={{
borderBottom: 'none',
paddingLeft: '16px',
paddingTop: '12px',
}}
title={
<Row>
<Col span={21}>
<Typography.Title className="m-b-0" level={5}>
{t('label.edit-entity', { entity: t('label.tier') })}
</Typography.Title>
</Col>
</Row>
<Space className="w-full p-xs justify-between">
<Typography.Text className="m-b-0 font-medium text-md">
{t('label.edit-entity', { entity: t('label.tier') })}
</Typography.Text>
<Typography.Text
className="m-b-0 font-normal text-primary cursor-pointer"
data-testid="clear-tier"
onClick={clearTierSelection}>
{t('label.clear')}
</Typography.Text>
</Space>
}>
<Collapse
accordion
className="collapse-container"
defaultActiveKey={currentTier}
onChange={(key) => handleCardSelection(key as string)}>
{tierData.map((card) => (
<Panel
className={classNames('collapse-tier-panel', {
selected: currentTier === card.id,
})}
data-testid="card-list"
extra={<div data-testid="icon">{getCardIcon(card.id)}</div>}
header={
<Space direction="vertical" size={0}>
<Typography.Paragraph className="m-b-0 text-color-inherit text-base font-semibold">
{card.title}
</Typography.Paragraph>
<Typography.Paragraph className="m-b-0 text-color-inherit font-medium">
{card.description.replace(/\*/g, '')}
</Typography.Paragraph>
</Space>
}
key={card.id}>
<RichTextEditorPreviewer
enableSeeMoreVariant={false}
markdown={card.data}
/>
</Panel>
))}
</Collapse>
<Radio.Group value={currentTier} onChange={handleTierSelection}>
<Collapse
accordion
className="bg-white border-none"
defaultActiveKey={currentTier}
expandIconPosition="end">
{tierData.map((card) => (
<Panel
data-testid="card-list"
header={
<div className="flex self-start">
<Radio
className="radio-input"
data-testid="radio-btn"
value={card.id}
/>
<Space direction="vertical" size={0}>
<Typography.Paragraph className="m-b-0 font-regular text-grey-body">
{card.title}
</Typography.Paragraph>
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-muted">
{card.description.replace(/\*/g, '')}
</Typography.Paragraph>
</Space>
</div>
}
key={card.id}>
<RichTextEditorPreviewer
className="tier-card-description"
enableSeeMoreVariant={false}
markdown={card.data}
/>
</Panel>
))}
</Collapse>
</Radio.Group>
{isLoadingTierData && <Loader />}
</Card>
}
data-testid="tier-card-container"
overlayClassName="tier-card-container"
placement="bottomRight"
showArrow={false}
trigger="click"

View File

@ -13,51 +13,61 @@
@import url('../../../styles/variables.less');
.tier-card {
width: 760px;
border: 1px solid #dde3ea;
box-shadow: @box-shadow-base;
border-radius: 4px !important;
}
.tier-card-container {
padding: 0;
.ant-popover-inner {
box-shadow: none;
.ant-popover-inner-content {
padding: 0;
}
}
.collapse-container {
.collapse-tier-panel {
border: @global-border;
.ant-collapse-content {
border-color: @primary-color;
}
&.ant-collapse-item-active {
border: 1px solid @primary-color;
background: @primary-color-hover;
}
&.selected {
border: 1px solid @primary-color;
background: @primary-color;
.ant-collapse-header {
color: @white;
.tier-card {
width: 655px;
.ant-card-body {
.ant-collapse-item {
.ant-collapse-header {
.ant-collapse-arrow {
position: absolute;
top: 20px;
}
}
.ant-collapse-content {
&.ant-collapse-content-active {
border: none;
}
.ant-collapse-content-box {
padding: 0 12px;
}
}
}
}
}
&:first-child,
&:first-child.ant-collapse-item-active {
border-radius: 4px 4px 0 0;
}
&:last-child {
border-radius: 0 0 4px 4px;
}
.tier-card-description {
.toastui-editor-contents ul {
padding-left: 18px;
font-size: 12px;
font-weight: 400;
color: @text-color-secondary;
margin: 4px;
li {
margin: 0;
}
}
}
&.ant-collapse {
background-color: @white;
border: none;
.radio-input .ant-radio {
.ant-radio-inner {
width: 14px;
height: 14px;
border: 1.5px solid @primary-color;
box-shadow: none !important;
}
&:hover .ant-radio-inner {
background-color: @primary-color-hover;
}
&.ant-radio-checked .ant-radio-inner::after {
box-shadow: none !important;
transform: scale(0.35);
}
}
}

View File

@ -118,6 +118,7 @@
"classification-lowercase-plural": "classifications",
"classification-plural": "Classifications",
"clean-up-policy-plural-lowercase": "clean-up policies",
"clear": "Clear",
"clear-entity": "Clear {{entity}}",
"click-here": "Click here",
"client-email": "Client Email",

View File

@ -118,6 +118,7 @@
"classification-lowercase-plural": "classifications",
"classification-plural": "Clasificaciones",
"clean-up-policy-plural-lowercase": "políticas de limpieza",
"clear": "Clear",
"clear-entity": "Limpiar {{entity}}",
"click-here": "Haga clic aquí",
"client-email": "Correo electrónico del cliente",

View File

@ -118,6 +118,7 @@
"classification-lowercase-plural": "classifications",
"classification-plural": "Classifications",
"clean-up-policy-plural-lowercase": "Nettoyer les stratégies",
"clear": "Clear",
"clear-entity": "Effacer {{entity}}",
"click-here": "Cliquer ici",
"client-email": "Client Email",

View File

@ -118,6 +118,7 @@
"classification-lowercase-plural": "classifications",
"classification-plural": "分類",
"clean-up-policy-plural-lowercase": "ポリシーの削除",
"clear": "Clear",
"clear-entity": "{{entity}}を削除",
"click-here": "ここをクリック",
"client-email": "Client Email",

View File

@ -118,6 +118,7 @@
"classification-lowercase-plural": "classifications",
"classification-plural": "Classificações",
"clean-up-policy-plural-lowercase": "limpar políticas",
"clear": "Clear",
"clear-entity": "Limpar {{entity}}",
"click-here": "Clique aqui",
"client-email": "E-mail do cliente",

View File

@ -118,6 +118,7 @@
"classification-lowercase-plural": "classifications",
"classification-plural": "分类",
"clean-up-policy-plural-lowercase": "清理策略",
"clear": "Clear",
"clear-entity": "清除{{entity}}",
"click-here": "点击这里",
"client-email": "客户端电子邮箱",