mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-07-23 17:30:35 +00:00
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:
parent
60c9477d18
commit
e32b763e15
@ -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');
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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"
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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": "客户端电子邮箱",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user