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

View File

@ -11,7 +11,15 @@
* limitations under the License. * 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 React from 'react';
import TierCard from './TierCard'; 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', () => ({ jest.mock('rest/tagAPI', () => ({
getTags: jest getTags: jest.fn().mockImplementation(() => mockGetTags()),
.fn()
.mockImplementation(() => Promise.resolve({ data: mockTierData })),
})); }));
jest.mock('../../Loader/Loader', () => { jest.mock('../../Loader/Loader', () => {
@ -42,7 +59,7 @@ jest.mock('../../Loader/Loader', () => {
}); });
jest.mock('../../../utils/ToastUtils', () => { jest.mock('../../../utils/ToastUtils', () => {
return jest.fn().mockReturnValue(<div>showErrorToast</div>); return jest.fn().mockImplementation(() => mockShowErrorToast());
}); });
// Mock Antd components // Mock Antd components
@ -51,19 +68,58 @@ jest.mock('antd', () => ({
Popover: jest Popover: jest
.fn() .fn()
.mockImplementation(({ content }) => ( .mockImplementation(({ content, onOpenChange, children }) => {
<div data-testid="tier-card-container">{content}</div> 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', () => { describe('Test TierCard Component', () => {
it('Component should have card', async () => { it('Component should have card', async () => {
const { container } = render( const { container } = render(<TierCard {...mockProps} />);
<TierCard currentTier="" updateTier={MockOnUpdate} />
); await expect(getByText(container, 'Loader')).toBeInTheDocument();
expect(mockGetTags).toHaveBeenCalled();
expect(await findByTestId(container, 'cards')).toBeInTheDocument(); 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. * limitations under the License.
*/ */
import Icon from '@ant-design/icons/lib/components/Icon';
import { import {
Button,
Card, Card,
Col,
Collapse, Collapse,
Popover, Popover,
Row, Radio,
RadioChangeEvent,
Space, Space,
Typography, Typography,
} from 'antd'; } 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 Loader from 'components/Loader/Loader';
import { t } from 'i18next'; import { t } from 'i18next';
import { LoadingState } from 'Models'; import React, { useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { getTags } from 'rest/tagAPI'; import { getTags } from 'rest/tagAPI';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { showErrorToast } from '../../../utils/ToastUtils'; import { showErrorToast } from '../../../utils/ToastUtils';
@ -39,198 +33,111 @@ import { CardWithListItems, TierCardProps } from './TierCard.interface';
const { Panel } = Collapse; const { Panel } = Collapse;
const TierCard = ({ currentTier, updateTier, children }: TierCardProps) => { const TierCard = ({ currentTier, updateTier, children }: TierCardProps) => {
const [tierData, setTierData] = useState<Array<CardWithListItems>>([]); const [tierData, setTierData] = useState<Array<CardWithListItems>>([]);
const [activeTier, setActiveTier] = useState(currentTier);
const [statusTier, setStatusTier] = useState<LoadingState>('initial');
const [isLoadingTierData, setIsLoadingTierData] = useState<boolean>(false); const [isLoadingTierData, setIsLoadingTierData] = useState<boolean>(false);
const handleCardSelection = (cardId: string) => { const getTierData = async () => {
setActiveTier(cardId);
};
const setInitialTierLoadingState = () => {
setStatusTier('initial');
};
const getTierData = () => {
setIsLoadingTierData(true); setIsLoadingTierData(true);
getTags({ try {
parent: 'Tier', const { data } = await 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);
}); });
};
const prepareTier = (updatedTier: string) => { if (data) {
return updatedTier !== currentTier ? updatedTier : undefined; const tierData: CardWithListItems[] =
}; data.map((tier: { name: string; description: string }) => ({
id: `Tier${FQN_SEPARATOR_CHAR}${tier.name}`,
const handleTierSave = (updatedTier: string) => { title: tier.name,
setStatusTier('waiting'); description: tier.description.substring(
0,
const newTier = prepareTier(updatedTier); tier.description.indexOf('\n\n')
updateTier?.(newTier as string); ),
}; data: tier.description.substring(
tier.description.indexOf('\n\n') + 1
const getTierSelectButton = (tier: string) => { ),
switch (statusTier) { })) ?? [];
case 'waiting': setTierData(tierData);
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);
} else { } else {
return ( setTierData([]);
<Button
ghost
data-testid="select-tier-button"
size="small"
type="primary"
onClick={() => handleTierSave(cardId)}>
{t('label.select')}
</Button>
);
} }
}, } catch (err) {
[currentTier, activeTier] showErrorToast(
); err,
t('server.entity-fetch-error', {
useEffect(() => { entity: t('label.tier-plural-lowercase'),
setActiveTier(currentTier); })
if (statusTier === 'waiting') { );
setStatusTier('success'); } finally {
setTimeout(() => { setIsLoadingTierData(false);
setInitialTierLoadingState();
}, 300);
} }
}, [currentTier]); };
const handleTierSelection = ({ target: { value } }: RadioChangeEvent) => {
updateTier?.(value as string);
};
const clearTierSelection = () => {
updateTier?.();
};
return ( return (
<Popover <Popover
className="p-0 tier-card-popover"
content={ content={
<Card <Card
className="tier-card" className="tier-card"
data-testid="cards" data-testid="cards"
headStyle={{
borderBottom: 'none',
paddingLeft: '16px',
paddingTop: '12px',
}}
title={ title={
<Row> <Space className="w-full p-xs justify-between">
<Col span={21}> <Typography.Text className="m-b-0 font-medium text-md">
<Typography.Title className="m-b-0" level={5}> {t('label.edit-entity', { entity: t('label.tier') })}
{t('label.edit-entity', { entity: t('label.tier') })} </Typography.Text>
</Typography.Title> <Typography.Text
</Col> className="m-b-0 font-normal text-primary cursor-pointer"
</Row> data-testid="clear-tier"
onClick={clearTierSelection}>
{t('label.clear')}
</Typography.Text>
</Space>
}> }>
<Collapse <Radio.Group value={currentTier} onChange={handleTierSelection}>
accordion <Collapse
className="collapse-container" accordion
defaultActiveKey={currentTier} className="bg-white border-none"
onChange={(key) => handleCardSelection(key as string)}> defaultActiveKey={currentTier}
{tierData.map((card) => ( expandIconPosition="end">
<Panel {tierData.map((card) => (
className={classNames('collapse-tier-panel', { <Panel
selected: currentTier === card.id, data-testid="card-list"
})} header={
data-testid="card-list" <div className="flex self-start">
extra={<div data-testid="icon">{getCardIcon(card.id)}</div>} <Radio
header={ className="radio-input"
<Space direction="vertical" size={0}> data-testid="radio-btn"
<Typography.Paragraph className="m-b-0 text-color-inherit text-base font-semibold"> value={card.id}
{card.title} />
</Typography.Paragraph> <Space direction="vertical" size={0}>
<Typography.Paragraph className="m-b-0 text-color-inherit font-medium"> <Typography.Paragraph className="m-b-0 font-regular text-grey-body">
{card.description.replace(/\*/g, '')} {card.title}
</Typography.Paragraph> </Typography.Paragraph>
</Space> <Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-muted">
} {card.description.replace(/\*/g, '')}
key={card.id}> </Typography.Paragraph>
<RichTextEditorPreviewer </Space>
enableSeeMoreVariant={false} </div>
markdown={card.data} }
/> key={card.id}>
</Panel> <RichTextEditorPreviewer
))} className="tier-card-description"
</Collapse> enableSeeMoreVariant={false}
markdown={card.data}
/>
</Panel>
))}
</Collapse>
</Radio.Group>
{isLoadingTierData && <Loader />} {isLoadingTierData && <Loader />}
</Card> </Card>
} }
data-testid="tier-card-container"
overlayClassName="tier-card-container"
placement="bottomRight" placement="bottomRight"
showArrow={false} showArrow={false}
trigger="click" trigger="click"

View File

@ -13,51 +13,61 @@
@import url('../../../styles/variables.less'); @import url('../../../styles/variables.less');
.tier-card { .ant-popover-inner {
width: 760px; box-shadow: none;
border: 1px solid #dde3ea;
box-shadow: @box-shadow-base;
border-radius: 4px !important;
}
.tier-card-container {
padding: 0;
.ant-popover-inner-content { .ant-popover-inner-content {
padding: 0; padding: 0;
} .tier-card {
} width: 655px;
.ant-card-body {
.collapse-container { .ant-collapse-item {
.collapse-tier-panel { .ant-collapse-header {
border: @global-border; .ant-collapse-arrow {
position: absolute;
.ant-collapse-content { top: 20px;
border-color: @primary-color; }
} }
.ant-collapse-content {
&.ant-collapse-item-active { &.ant-collapse-content-active {
border: 1px solid @primary-color; border: none;
background: @primary-color-hover; }
} .ant-collapse-content-box {
padding: 0 12px;
&.selected { }
border: 1px solid @primary-color; }
background: @primary-color; }
.ant-collapse-header {
color: @white;
} }
} }
&:first-child, }
&:first-child.ant-collapse-item-active {
border-radius: 4px 4px 0 0; .tier-card-description {
} .toastui-editor-contents ul {
&:last-child { padding-left: 18px;
border-radius: 0 0 4px 4px; font-size: 12px;
font-weight: 400;
color: @text-color-secondary;
margin: 4px;
li {
margin: 0;
}
} }
} }
&.ant-collapse { .radio-input .ant-radio {
background-color: @white; .ant-radio-inner {
border: none; 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-lowercase-plural": "classifications",
"classification-plural": "Classifications", "classification-plural": "Classifications",
"clean-up-policy-plural-lowercase": "clean-up policies", "clean-up-policy-plural-lowercase": "clean-up policies",
"clear": "Clear",
"clear-entity": "Clear {{entity}}", "clear-entity": "Clear {{entity}}",
"click-here": "Click here", "click-here": "Click here",
"client-email": "Client Email", "client-email": "Client Email",

View File

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

View File

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

View File

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

View File

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

View File

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