Updates Lingui

This commit is contained in:
Alex Corey 2021-04-01 11:58:23 -04:00
parent b85559fe13
commit 43c8cabaa6
49 changed files with 15066 additions and 1662 deletions

View File

@ -93,7 +93,8 @@
"RunOnRadio",
"NodeTypeLetter",
"SelectableItem",
"Dash"
"Dash",
"Plural"
],
"ignoreCallee": ["describe"]
}

View File

@ -1,6 +1,17 @@
{
"localeDir": "src/locales/",
"srcPathDirs": ["src/"],
"format": "po",
"sourceLocale": "en"
{"catalogs":[{
"path": "<rootDir>/locales/{locale}/messages",
"include": ["<rootDir>"],
"exclude": ["**/node_modules/**"]
}],
"compileNamespace": "cjs",
"extractBabelOptions": {},
"compilerBabelOptions": {},
"fallbackLocales": { default: "en"},
"format": "po",
"locales": ["en","es","fr","nl","zh","ja", "zu"],
"orderBy": "messageId",
"pseudoLocale": "zu",
"rootDir": "./src",
"runtimeConfigModule": ["@lingui/core", "i18n"],
"sourceLocale": "en",
}

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"node": "14.x"
},
"dependencies": {
"@lingui/react": "^2.9.1",
"@lingui/react": "^3.7.1",
"@patternfly/patternfly": "^4.80.3",
"@patternfly/react-core": "^4.90.2",
"@patternfly/react-icons": "4.7.22",
@ -14,6 +14,8 @@
"ace-builds": "^1.4.12",
"ansi-to-html": "^0.6.11",
"axios": "^0.21.1",
"babel-plugin-macros": "^3.0.1",
"codemirror": "^5.47.0",
"d3": "^5.12.0",
"dagre": "^0.8.4",
"formik": "^2.1.2",
@ -33,8 +35,8 @@
"devDependencies": {
"@babel/polyfill": "^7.8.7",
"@cypress/instrument-cra": "^1.4.0",
"@lingui/cli": "^2.9.2",
"@lingui/macro": "^2.9.1",
"@lingui/cli": "^3.7.1",
"@lingui/macro": "^3.7.1",
"@nteract/mockument": "^1.0.4",
"babel-core": "^7.0.0-bridge.0",
"enzyme": "^3.10.0",

View File

@ -7,18 +7,24 @@ import {
Switch,
Redirect,
} from 'react-router-dom';
import { I18n, I18nProvider } from '@lingui/react';
import { Card, PageSection } from '@patternfly/react-core';
import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core';
import { ConfigProvider, useAuthorizedPath } from './contexts/Config';
import { en, es, fr, nl, zh, ja, zu } from 'make-plural/plurals';
import AppContainer from './components/AppContainer';
import Background from './components/Background';
import NotFound from './screens/NotFound';
import Login from './screens/Login';
import ja from './locales/ja/messages';
import en from './locales/en/messages';
import japanese from './locales/ja/messages';
import english from './locales/en/messages';
import zulu from './locales/zu/messages';
import french from './locales/fr/messages';
import dutch from './locales/nl/messages';
import chinese from './locales/zh/messages';
import spanish from './locales/es/messages';
import { isAuthenticated } from './util/auth';
import { getLanguageWithoutRegionCode } from './util/language';
import getRouteConfig from './routeConfig';
@ -74,7 +80,15 @@ const ProtectedRoute = ({ children, ...rest }) =>
);
function App() {
const catalogs = { en, ja };
const catalogs = {
en: english,
ja: japanese,
zu: zulu,
fr: french,
es: spanish,
zh: chinese,
nl: dutch,
};
let language = getLanguageWithoutRegionCode(navigator);
if (!Object.keys(catalogs).includes(language)) {
// If there isn't a string catalog available for the browser's
@ -83,32 +97,56 @@ function App() {
}
const { hash, search, pathname } = useLocation();
i18n.loadLocaleData('en', { plurals: en });
i18n.loadLocaleData('es', { plurals: es });
i18n.loadLocaleData('fr', { plurals: fr });
i18n.loadLocaleData('nl', { plurals: nl });
i18n.loadLocaleData('zh', { plurals: zh });
i18n.loadLocaleData('ja', { plurals: ja });
i18n.loadLocaleData('zu', { plurals: zu });
i18n.load({
en: english.messages,
ja: japanese.messages,
zu: zulu.messages,
fr: french.messages,
nl: dutch.messages,
zh: chinese.messages,
es: spanish.messages,
});
i18n.activate(language);
return (
<I18nProvider language={language} catalogs={catalogs}>
<I18n>
{({ i18n }) => (
<Background>
<Switch>
<Route exact strict path="/*/">
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
</Route>
<Route path="/login">
<Login isAuthenticated={isAuthenticated} />
</Route>
<Route exact path="/">
<Redirect to="/home" />
</Route>
<ProtectedRoute>
<ConfigProvider>
<AppContainer navRouteConfig={getRouteConfig(i18n)}>
<AuthorizedRoutes routeConfig={getRouteConfig(i18n)} />
</AppContainer>
</ConfigProvider>
</ProtectedRoute>
</Switch>
</Background>
)}
</I18n>
<I18nProvider i18n={i18n} catalogs={catalogs}>
<Background>
<Switch>
<Route exact strict path="/*/">
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
</Route>
<Route path="/login">
<Login isAuthenticated={isAuthenticated} />
</Route>
<Route exact path="/">
<Redirect to="/home" />
</Route>
<ProtectedRoute>
<AppContainer navRouteConfig={getRouteConfig(i18n)}>
<Switch>
{getRouteConfig(i18n)
.flatMap(({ routes }) => routes)
.map(({ path, screen: Screen }) => (
<ProtectedRoute key={path} path={path}>
<Screen match={match} />
</ProtectedRoute>
))
.concat(
<ProtectedRoute key="not-found" path="*">
<NotFound />
</ProtectedRoute>
)}
</Switch>
</AppContainer>
</ProtectedRoute>
</Switch>
</Background>
</I18nProvider>
);
}

View File

@ -1,9 +1,9 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { shallow } from 'enzyme';
import {
mountWithContexts,
shallowWithContexts,
waitForElement,
} from '../../../testUtils/enzymeHelpers';
import { sleep } from '../../../testUtils/testUtils';
@ -27,17 +27,19 @@ describe('<SelectResourceStep />', () => {
afterEach(() => {
jest.restoreAllMocks();
});
test('initially renders without crashing', () => {
shallow(
<SelectResourceStep
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={() => {}}
fetchItems={() => {}}
fetchOptions={() => {}}
/>
);
test('initially renders without crashing', async () => {
act(() => {
shallowWithContexts(
<SelectResourceStep
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={() => {}}
fetchItems={() => {}}
fetchOptions={() => {}}
/>
);
});
});
test('fetches resources on mount and adds items to list', async () => {

View File

@ -1,6 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import {
mountWithContexts,
shallowWithContexts,
} from '../../../testUtils/enzymeHelpers';
import SelectRoleStep from './SelectRoleStep';
describe('<SelectRoleStep />', () => {
@ -31,7 +35,7 @@ describe('<SelectRoleStep />', () => {
},
];
test('initially renders without crashing', () => {
wrapper = shallow(
wrapper = shallowWithContexts(
<SelectRoleStep
roles={roles}
selectedResourceRows={selectedResourceRows}

View File

@ -1,6 +1,6 @@
import React, { useContext, useEffect, useState } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { t, Plural } from '@lingui/macro';
import { arrayOf, func } from 'prop-types';
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
import { KebabifiedContext } from '../../contexts/Kebabified';
@ -22,7 +22,6 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
const [isModalOpen, setIsModalOpen] = useState(false);
const numJobsToCancel = jobsToCancel.length;
const zeroOrOneJobSelected = numJobsToCancel < 2;
const handleCancelJob = () => {
onCancel();
@ -54,35 +53,32 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
<div>
{cannotCancelPermissions.length > 0 && (
<div>
{i18n._(
'{numJobsUnableToCancel, plural, one {You do not have permission to cancel the following job:} other {You do not have permission to cancel the following jobs:}}',
{
numJobsUnableToCancel: cannotCancelPermissions.length,
}
)}
{' '.concat(cannotCancelPermissions.join(', '))}
<Plural
value={cannotCancelPermissions.length}
one="You do not have permission to cancel the following job:"
other="You do not have permission to cancel the following jobs:"
/>
</div>
)}
{cannotCancelNotRunning.length > 0 && (
<div>
{i18n._(
'{numJobsUnableToCancel, plural, one {You cannot cancel the following job because it is not running:} other {You cannot cancel the following jobs because they are not running:}}',
{
numJobsUnableToCancel: cannotCancelNotRunning.length,
}
)}
{' '.concat(cannotCancelNotRunning.join(', '))}
<Plural
value={cannotCancelNotRunning.length}
one="You cannot cancel the following job because it is not running"
other="You cannot cancel the following jobs because they are not running"
/>
</div>
)}
</div>
);
}
if (numJobsToCancel > 0) {
return i18n._(
'{numJobsToCancel, plural, one {Cancel selected job} other {Cancel selected jobs}}',
{
numJobsToCancel,
}
return (
<Plural
value={numJobsToCancel}
one={i18n._(t`Cancel selected job`)}
other={i18n._(t`Cancel selected jobs`)}
/>
);
}
return i18n._(t`Select a job to cancel`);
@ -92,12 +88,8 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
jobsToCancel.length === 0 ||
jobsToCancel.some(cannotCancelBecausePermissions) ||
jobsToCancel.some(cannotCancelBecauseNotRunning);
const cancelJobText = i18n._(
'{zeroOrOneJobSelected, plural, one {Cancel job} other {Cancel jobs}}',
{
zeroOrOneJobSelected,
}
const cancelJobText = (
<Plural value={numJobsToCancel} one="Cancel job" other="Cancel jobs" />
);
return (
@ -156,12 +148,11 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
]}
>
<div>
{i18n._(
'{numJobsToCancel, plural, one {This action will cancel the following job:} other {This action will cancel the following jobs:}}',
{
numJobsToCancel,
}
)}
<Plural
value={numJobsToCancel}
one="This action will cancel the following job:"
other="This action will cancel the following jobs:"
/>
</div>
{jobsToCancel.map(job => (
<span key={job.id}>

View File

@ -4,7 +4,7 @@ import {
Pagination as PFPagination,
DropdownDirection,
} from '@patternfly/react-core';
import { I18n } from '@lingui/react';
import { i18n } from '@lingui/core';
import { t } from '@lingui/macro';
const AWXPagination = styled(PFPagination)`
@ -19,26 +19,22 @@ const AWXPagination = styled(PFPagination)`
`;
export default props => (
<I18n>
{({ i18n }) => (
<AWXPagination
titles={{
items: i18n._(t`items`),
page: i18n._(t`page`),
pages: i18n._(t`pages`),
itemsPerPage: i18n._(t`Items per page`),
perPageSuffix: i18n._(t`per page`),
toFirstPage: i18n._(t`Go to first page`),
toPreviousPage: i18n._(t`Go to previous page`),
toLastPage: i18n._(t`Go to last page`),
toNextPage: i18n._(t`Go to next page`),
optionsToggle: i18n._(t`Select`),
currPage: i18n._(t`Current page`),
paginationTitle: i18n._(t`Pagination`),
}}
dropDirection={DropdownDirection.up}
{...props}
/>
)}
</I18n>
<AWXPagination
titles={{
items: i18n._(t`items`),
page: i18n._(t`page`),
pages: i18n._(t`pages`),
itemsPerPage: i18n._(t`Items per page`),
perPageSuffix: i18n._(t`per page`),
toFirstPage: i18n._(t`Go to first page`),
toPreviousPage: i18n._(t`Go to previous page`),
toLastPage: i18n._(t`Go to last page`),
toNextPage: i18n._(t`Go to next page`),
optionsToggle: i18n._(t`Select`),
currPage: i18n._(t`Current page`),
paginationTitle: i18n._(t`Pagination`),
}}
dropDirection={DropdownDirection.up}
{...props}
/>
);

View File

@ -15,12 +15,12 @@ function DeleteRoleConfirmationModal({
i18n,
}) {
const isTeamRole = () => {
return typeof role.team_id !== 'undefined';
return typeof role.team_id !== 'undefined'
? i18n._(t`Team`)
: i18n._(t`User`);
};
const title = i18n._(
t`Remove ${isTeamRole() ? i18n._(t`Team`) : i18n._(t`User`)} Access`
);
const title = i18n._(t`Remove ${isTeamRole()} Access`);
return (
<AlertModal
variant="danger"

View File

@ -3,7 +3,7 @@ import React from 'react';
import styled from 'styled-components';
import { useField } from 'formik';
import { withI18n } from '@lingui/react';
import { t, Trans } from '@lingui/macro';
import { t, Trans, Plural } from '@lingui/macro';
import { RRule } from 'rrule';
import {
Checkbox as _Checkbox,
@ -185,29 +185,17 @@ const FrequencyDetailSubform = ({ i18n }) => {
switch (frequency.value) {
case 'minute':
return i18n._('{intervalValue, plural, one {minute} other {minutes}}', {
intervalValue,
});
return <Plural value={intervalValue} one="minute" other="minutes" />;
case 'hour':
return i18n._('{intervalValue, plural, one {hour} other {hours}}', {
intervalValue,
});
return <Plural value={intervalValue} one="hour" other="hours" />;
case 'day':
return i18n._('{intervalValue, plural, one {day} other {days}}', {
intervalValue,
});
return <Plural value={intervalValue} one="day" other="days" />;
case 'week':
return i18n._('{intervalValue, plural, one {week} other {weeks}}', {
intervalValue,
});
return <Plural value={intervalValue} one="week" other="weeks" />;
case 'month':
return i18n._('{intervalValue, plural, one {month} other {months}}', {
intervalValue,
});
return <Plural value={intervalValue} one="month" other="months" />;
case 'year':
return i18n._('{intervalValue, plural, one {year} other {years}}', {
intervalValue,
});
return <Plural value={intervalValue} one="year" other="years" />;
default:
throw new Error(i18n._(t`Frequency did not match an expected value`));
}

View File

@ -1,12 +1,15 @@
import React from 'react';
import { mount } from 'enzyme';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import {
mountWithContexts,
shallowWithContexts,
} from '../../../testUtils/enzymeHelpers';
import Sparkline from './Sparkline';
describe('Sparkline', () => {
test('renders the expected content', () => {
const wrapper = mount(<Sparkline />);
const wrapper = shallowWithContexts(<Sparkline />);
expect(wrapper).toHaveLength(1);
});
test('renders an icon with tooltips and links for each job', () => {

View File

@ -170,8 +170,7 @@ function TemplateList({ defaultParams, i18n }) {
);
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
selected[0],
i18n
selected[0]
);
return (

View File

@ -1,5 +1,10 @@
import React from 'react';
import { en } from 'make-plural/plurals';
import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import english from '../../locales/en/messages';
import { WorkflowStateContext } from '../../contexts/Workflow';
import WorkflowStartNode from './WorkflowStartNode';
@ -10,16 +15,22 @@ const nodePositions = {
},
};
i18n.loadLocaleData({ en: { plurals: en } });
i18n.load({ en: english });
i18n.activate('en');
describe('WorkflowStartNode', () => {
test('mounts successfully', () => {
const wrapper = mountWithContexts(
<svg>
<WorkflowStateContext.Provider value={{ nodePositions }}>
<WorkflowStartNode
nodePositions={nodePositions}
showActionTooltip={false}
/>
</WorkflowStateContext.Provider>
<I18nProvider i18n={i18n}>
<WorkflowStateContext.Provider value={{ nodePositions }}>
<WorkflowStartNode
nodePositions={nodePositions}
showActionTooltip={false}
/>
</WorkflowStateContext.Provider>
</I18nProvider>
</svg>
);
expect(wrapper).toHaveLength(1);
@ -27,9 +38,14 @@ describe('WorkflowStartNode', () => {
test('tooltip shown on hover', () => {
const wrapper = mountWithContexts(
<svg>
<WorkflowStateContext.Provider value={{ nodePositions }}>
<WorkflowStartNode nodePositions={nodePositions} showActionTooltip />
</WorkflowStateContext.Provider>
<I18nProvider i18n={i18n}>
<WorkflowStateContext.Provider value={{ nodePositions }}>
<WorkflowStartNode
nodePositions={nodePositions}
showActionTooltip
/>
</WorkflowStateContext.Provider>
</I18nProvider>
</svg>
);
expect(wrapper.find('WorkflowActionTooltip')).toHaveLength(0);

View File

@ -185,8 +185,7 @@ function CredentialDetail({ i18n, credential }) {
}, [fetchDetails]);
const deleteDetailsRequests = relatedResourceDeleteRequests.credential(
credential,
i18n
credential
);
if (hasContentLoading) {

View File

@ -105,8 +105,7 @@ function CredentialList({ i18n }) {
const canAdd =
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
const deleteDetailsRequests = relatedResourceDeleteRequests.credential(
selected[0],
i18n
selected[0]
);
return (
<PageSection>

View File

@ -107,8 +107,7 @@ function CredentialTypeList({ i18n }) {
const canAdd = actions && actions.POST;
const deleteDetailsRequests = relatedResourceDeleteRequests.credentialType(
selected[0],
i18n
selected[0]
);
return (

View File

@ -43,8 +43,7 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) {
const { error, dismissError } = useDismissableError(deleteError);
const deleteDetailsRequests = relatedResourceDeleteRequests.executionEnvironment(
executionEnvironment,
i18n
executionEnvironment
);
return (
<CardBody>

View File

@ -106,8 +106,7 @@ function ExecutionEnvironmentList({ i18n }) {
const canAdd = actions && actions.POST;
const deleteDetailsRequests = relatedResourceDeleteRequests.executionEnvironment(
selected[0],
i18n
selected[0]
);
return (
<>

View File

@ -36,8 +36,7 @@ function ContainerGroupDetails({ instanceGroup, i18n }) {
const { error, dismissError } = useDismissableError(deleteError);
const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
instanceGroup,
i18n
instanceGroup
);
return (
<CardBody>

View File

@ -40,8 +40,7 @@ function InstanceGroupDetails({ instanceGroup, i18n }) {
const { error, dismissError } = useDismissableError(deleteError);
const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
instanceGroup,
i18n
instanceGroup
);
const verifyInstanceGroup = item => {
if (item.is_isolated) {

View File

@ -187,8 +187,7 @@ function InstanceGroupList({ i18n }) {
: `${match.url}/${item.id}/details`;
};
const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
selected[0],
i18n
selected[0]
);
return (
<>

View File

@ -56,8 +56,7 @@ function InventoryDetail({ inventory, i18n }) {
} = inventory.summary_fields;
const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(
inventory,
i18n
inventory
);
if (isLoading) {

View File

@ -219,10 +219,12 @@ describe('<InventoryGroupsList/> error handling', () => {
await act(async () => {
wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')();
});
wrapper.update();
await waitForElement(
wrapper,
'AlertModal[title="Delete Group?"]',
el => el.props().isOpen === true
'AlertModal__Header',
el => el.text() === 'Delete Group?'
);
await act(async () => {
wrapper.find('Radio[id="radio-delete"]').invoke('onChange')();

View File

@ -1,7 +1,7 @@
import React, { useState, useCallback, useEffect } from 'react';
import { useLocation, useRouteMatch, Link } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { t, Plural } from '@lingui/macro';
import { Card, PageSection, DropdownItem } from '@patternfly/react-core';
import { InventoriesAPI } from '../../../api';
import useRequest, { useDeleteItems } from '../../../util/useRequest';
@ -129,8 +129,7 @@ function InventoryList({ i18n }) {
};
const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(
selected[0],
i18n
selected[0]
);
const addInventory = i18n._(t`Add inventory`);
@ -219,15 +218,14 @@ function InventoryList({ i18n }) {
onDelete={handleInventoryDelete}
itemsToDelete={selected}
pluralizedItemName={i18n._(t`Inventories`)}
warningMessage={i18n._(
'{numItemsToDelete, plural, one {The inventory will be in a pending status until the final delete is processed.} other {The inventories will be in a pending status until the final delete is processed.}}',
{ numItemsToDelete: selected.length }
)}
deleteMessage={i18n._(
'{numItemsToDelete, plural, one {This inventory is currently being used by other resources. Are you sure you want to delete it?} other {Deleting these inventories could impact other resources that rely on them. Are you sure you want to delete anyway?}}',
{ numItemsToDelete: selected.length }
)}
deleteDetailsRequests={deleteDetailsRequests}
warningMessage={
<Plural
value={selected.length}
one="The inventory will be in a pending status until the final delete is processed."
other="The inventories will be in a pending status until the final delete is processed."
/>
}
/>,
]}
/>

View File

@ -99,7 +99,6 @@ function InventorySourceDetail({ inventorySource, i18n }) {
const deleteDetailsRequests = relatedResourceDeleteRequests.inventorySource(
inventorySource.inventory,
i18n,
inventorySource
);

View File

@ -146,7 +146,6 @@ function InventorySourceList({ i18n }) {
const deleteDetailsRequests = relatedResourceDeleteRequests.inventorySource(
id,
i18n,
selected[0]
);
return (

View File

@ -2,8 +2,8 @@ import 'styled-components/macro';
import React, { useState, useContext, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { func, bool, arrayOf } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { t, Plural } from '@lingui/macro';
import { Button, Radio, DropdownItem } from '@patternfly/react-core';
import styled from 'styled-components';
import { KebabifiedContext } from '../../../contexts/Kebabified';
@ -18,12 +18,8 @@ const ListItem = styled.li`
color: var(--pf-global--danger-color--100);
`;
const InventoryGroupsDeleteModal = ({
onAfterDelete,
isDisabled,
groups,
i18n,
}) => {
const InventoryGroupsDeleteModal = ({ onAfterDelete, isDisabled, groups }) => {
const { i18n } = useLingui();
const [radioOption, setRadioOption] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
@ -87,9 +83,11 @@ const InventoryGroupsDeleteModal = ({
isOpen={isModalOpen}
variant="danger"
title={
groups.length > 1
? i18n._(t`Delete Groups?`)
: i18n._(t`Delete Group?`)
<Plural
value={groups.length}
one="Delete Group?"
other="Delete Groups?"
/>
}
onClose={() => setIsModalOpen(false)}
actions={[
@ -112,11 +110,12 @@ const InventoryGroupsDeleteModal = ({
</Button>,
]}
>
{i18n._(
t`Are you sure you want to delete the ${
groups.length > 1 ? i18n._(t`groups`) : i18n._(t`group`)
} below?`
)}
<Plural
value={groups.length}
one="Are you sure you want delete the group below?"
other="Are you sure you want delete the groups below?"
/>
<div css="padding: 24px 0;">
{groups.map(group => {
return <ListItem key={group.id}>{group.name}</ListItem>;
@ -167,4 +166,4 @@ InventoryGroupsDeleteModal.defaultProps = {
groups: [],
};
export default withI18n()(InventoryGroupsDeleteModal);
export default InventoryGroupsDeleteModal;

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useHistory, useLocation, withRouter } from 'react-router-dom';
import { I18n } from '@lingui/react';
import { i18n } from '@lingui/core';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import {
@ -625,7 +625,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
};
const renderSearchComponent = i18n => (
const renderSearchComponent = () => (
<Search
qsConfig={QS_CONFIG}
columns={[
@ -688,176 +688,157 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
}
return (
<I18n>
{({ i18n }) => (
<>
<CardBody>
{isHostModalOpen && (
<HostEventModal
onClose={handleHostModalClose}
isOpen={isHostModalOpen}
hostEvent={hostEvent}
/>
)}
<OutputHeader>
<HeaderTitle>
<StatusIcon status={job.status} />
<h1>{job.name}</h1>
</HeaderTitle>
<OutputToolbar
job={job}
jobStatus={jobStatus}
onCancel={() => setShowCancelModal(true)}
onDelete={deleteJob}
isDeleteDisabled={isDeleting}
/>
</OutputHeader>
<HostStatusBar counts={job.host_status_counts} />
<SearchToolbar
id="job_output-toolbar"
clearAllFilters={handleRemoveAllSearchTerms}
collapseListedFiltersBreakpoint="lg"
clearFiltersButtonText={i18n._(t`Clear all filters`)}
>
<SearchToolbarContent>
<ToolbarToggleGroup toggleIcon={<SearchIcon />} breakpoint="lg">
<ToolbarItem variant="search-filter">
{isJobRunning(job.status) ? (
<Tooltip
content={i18n._(
t`Search is disabled while the job is running`
)}
>
{renderSearchComponent(i18n)}
</Tooltip>
) : (
renderSearchComponent(i18n)
<>
<CardBody>
{isHostModalOpen && (
<HostEventModal
onClose={handleHostModalClose}
isOpen={isHostModalOpen}
hostEvent={hostEvent}
/>
)}
<OutputHeader>
<HeaderTitle>
<StatusIcon status={job.status} />
<h1>{job.name}</h1>
</HeaderTitle>
<OutputToolbar
job={job}
jobStatus={jobStatus}
onCancel={() => setShowCancelModal(true)}
onDelete={deleteJob}
isDeleteDisabled={isDeleting}
/>
</OutputHeader>
<HostStatusBar counts={job.host_status_counts} />
<SearchToolbar
id="job_output-toolbar"
clearAllFilters={handleRemoveAllSearchTerms}
collapseListedFiltersBreakpoint="lg"
clearFiltersButtonText={i18n._(t`Clear all filters`)}
>
<SearchToolbarContent>
<ToolbarToggleGroup toggleIcon={<SearchIcon />} breakpoint="lg">
<ToolbarItem variant="search-filter">
{isJobRunning(job.status) ? (
<Tooltip
content={i18n._(
t`Search is disabled while the job is running`
)}
<Tooltip
content={i18n._(t`Job output documentation`)}
position="bottom"
>
<Button
component="a"
variant="plain"
target="_blank"
href={`${getDocsBaseUrl(
config
)}/html/userguide/jobs.html#standard-out-pane`}
>
<QuestionCircleIcon />
</Button>
</Tooltip>
</ToolbarItem>
</ToolbarToggleGroup>
</SearchToolbarContent>
</SearchToolbar>
<PageControls
onScrollFirst={handleScrollFirst}
onScrollLast={handleScrollLast}
onScrollNext={handleScrollNext}
onScrollPrevious={handleScrollPrevious}
/>
<OutputWrapper cssMap={cssMap}>
<InfiniteLoader
isRowLoaded={isRowLoaded}
loadMoreRows={loadMoreRows}
rowCount={remoteRowCount}
>
{({ onRowsRendered, registerChild }) => (
<AutoSizer nonce={window.NONCE_ID} onResize={handleResize}>
{({ width, height }) => {
return (
<>
{hasContentLoading ? (
<div style={{ width }}>
<ContentLoading />
</div>
) : (
<List
ref={ref => {
registerChild(ref);
listRef.current = ref;
}}
deferredMeasurementCache={cache}
height={height || 1}
onRowsRendered={onRowsRendered}
rowCount={remoteRowCount}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
scrollToAlignment="start"
width={width || 1}
overscanRowCount={20}
/>
)}
</>
);
}}
</AutoSizer>
>
{renderSearchComponent(i18n)}
</Tooltip>
) : (
renderSearchComponent(i18n)
)}
</InfiniteLoader>
<OutputFooter />
</OutputWrapper>
</CardBody>
{showCancelModal && isJobRunning(job.status) && (
<AlertModal
isOpen={showCancelModal}
</ToolbarItem>
</ToolbarToggleGroup>
</SearchToolbarContent>
</SearchToolbar>
<PageControls
onScrollFirst={handleScrollFirst}
onScrollLast={handleScrollLast}
onScrollNext={handleScrollNext}
onScrollPrevious={handleScrollPrevious}
/>
<OutputWrapper cssMap={cssMap}>
<InfiniteLoader
isRowLoaded={isRowLoaded}
loadMoreRows={loadMoreRows}
rowCount={remoteRowCount}
>
{({ onRowsRendered, registerChild }) => (
<AutoSizer nonce={window.NONCE_ID} onResize={handleResize}>
{({ width, height }) => {
return (
<>
{hasContentLoading ? (
<div style={{ width }}>
<ContentLoading />
</div>
) : (
<List
ref={ref => {
registerChild(ref);
listRef.current = ref;
}}
deferredMeasurementCache={cache}
height={height || 1}
onRowsRendered={onRowsRendered}
rowCount={remoteRowCount}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
scrollToAlignment="start"
width={width || 1}
overscanRowCount={20}
/>
)}
</>
);
}}
</AutoSizer>
)}
</InfiniteLoader>
<OutputFooter />
</OutputWrapper>
</CardBody>
{showCancelModal && isJobRunning(job.status) && (
<AlertModal
isOpen={showCancelModal}
variant="danger"
onClose={() => setShowCancelModal(false)}
title={i18n._(t`Cancel Job`)}
label={i18n._(t`Cancel Job`)}
actions={[
<Button
id="cancel-job-confirm-button"
key="delete"
variant="danger"
onClose={() => setShowCancelModal(false)}
title={i18n._(t`Cancel Job`)}
label={i18n._(t`Cancel Job`)}
actions={[
<Button
id="cancel-job-confirm-button"
key="delete"
variant="danger"
isDisabled={isCancelling}
aria-label={i18n._(t`Cancel job`)}
onClick={cancelJob}
>
{i18n._(t`Cancel job`)}
</Button>,
<Button
id="cancel-job-return-button"
key="cancel"
variant="secondary"
aria-label={i18n._(t`Return`)}
onClick={() => setShowCancelModal(false)}
>
{i18n._(t`Return`)}
</Button>,
]}
isDisabled={isCancelling}
aria-label={i18n._(t`Cancel job`)}
onClick={cancelJob}
>
{i18n._(
t`Are you sure you want to submit the request to cancel this job?`
)}
</AlertModal>
)}
{dismissableDeleteError && (
<AlertModal
isOpen={dismissableDeleteError}
variant="danger"
onClose={dismissDeleteError}
title={i18n._(t`Job Delete Error`)}
label={i18n._(t`Job Delete Error`)}
{i18n._(t`Cancel job`)}
</Button>,
<Button
id="cancel-job-return-button"
key="cancel"
variant="secondary"
aria-label={i18n._(t`Return`)}
onClick={() => setShowCancelModal(false)}
>
<ErrorDetail error={dismissableDeleteError} />
</AlertModal>
{i18n._(t`Return`)}
</Button>,
]}
>
{i18n._(
t`Are you sure you want to submit the request to cancel this job?`
)}
{dismissableCancelError && (
<AlertModal
isOpen={dismissableCancelError}
variant="danger"
onClose={dismissCancelError}
title={i18n._(t`Job Cancel Error`)}
label={i18n._(t`Job Cancel Error`)}
>
<ErrorDetail error={dismissableCancelError} />
</AlertModal>
)}
</>
</AlertModal>
)}
</I18n>
{dismissableDeleteError && (
<AlertModal
isOpen={dismissableDeleteError}
variant="danger"
onClose={dismissDeleteError}
title={i18n._(t`Job Delete Error`)}
label={i18n._(t`Job Delete Error`)}
>
<ErrorDetail error={dismissableDeleteError} />
</AlertModal>
)}
{dismissableCancelError && (
<AlertModal
isOpen={dismissableCancelError}
variant="danger"
onClose={dismissCancelError}
title={i18n._(t`Job Cancel Error`)}
label={i18n._(t`Job Cancel Error`)}
>
<ErrorDetail error={dismissableCancelError} />
</AlertModal>
)}
</>
);
}

View File

@ -73,8 +73,7 @@ function OrganizationDetail({ i18n, organization }) {
const { error, dismissError } = useDismissableError(deleteError);
const deleteDetailsRequests = relatedResourceDeleteRequests.organization(
organization,
i18n
organization
);
if (hasContentLoading) {

View File

@ -118,8 +118,7 @@ function OrganizationsList({ i18n }) {
}
};
const deleteDetailsRequests = relatedResourceDeleteRequests.organization(
selected[0],
i18n
selected[0]
);
return (

View File

@ -2,14 +2,21 @@ import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import { en } from 'make-plural/plurals';
import { i18n } from '@lingui/core';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import english from '../../../locales/en/messages';
import OrganizationListItem from './OrganizationListItem';
i18n.loadLocaleData({ en: { plurals: en } });
i18n.load({ en: english });
i18n.activate('en');
describe('<OrganizationListItem />', () => {
test('initially renders successfully', () => {
mountWithContexts(
<I18nProvider>
<I18nProvider i18n={i18n}>
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<table>
<tbody>
@ -40,7 +47,7 @@ describe('<OrganizationListItem />', () => {
test('edit button shown to users with edit capabilities', () => {
const wrapper = mountWithContexts(
<I18nProvider>
<I18nProvider i18n={i18n}>
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<table>
<tbody>
@ -72,7 +79,7 @@ describe('<OrganizationListItem />', () => {
test('edit button hidden from users without edit capabilities', () => {
const wrapper = mountWithContexts(
<I18nProvider>
<I18nProvider i18n={i18n}>
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<table>
<tbody>

View File

@ -53,10 +53,7 @@ function ProjectDetail({ project, i18n }) {
);
const { error, dismissError } = useDismissableError(deleteError);
const deleteDetailsRequests = relatedResourceDeleteRequests.project(
project,
i18n
);
const deleteDetailsRequests = relatedResourceDeleteRequests.project(project);
let optionsList = '';
if (
scm_clean ||

View File

@ -118,8 +118,7 @@ function ProjectList({ i18n }) {
};
const deleteDetailsRequests = relatedResourceDeleteRequests.project(
selected[0],
i18n
selected[0]
);
return (

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { bool, oneOf, shape, string } from 'prop-types';
import { withI18n } from '@lingui/react';
import { shape, string } from 'prop-types';
import { useLingui } from '@lingui/react';
import { t } from '@lingui/macro';
import { useField } from 'formik';
import {
@ -35,20 +35,20 @@ const FormGroup = styled(PFFormGroup)`
}
`;
const SettingGroup = withI18n()(
({
i18n,
children,
defaultValue,
fieldId,
helperTextInvalid,
isDisabled,
isRequired,
label,
onRevertCallback,
popoverContent,
validated,
}) => (
const SettingGroup = ({
children,
defaultValue,
fieldId,
helperTextInvalid,
isDisabled,
isRequired,
label,
onRevertCallback,
popoverContent,
validated,
}) => {
const { i18n } = useLingui();
return (
<FormGroup
fieldId={fieldId}
helperTextInvalid={helperTextInvalid}
@ -73,43 +73,42 @@ const SettingGroup = withI18n()(
>
{children}
</FormGroup>
)
);
);
};
const BooleanField = ({ ariaLabel = '', name, config, disabled = false }) => {
const [field, meta, helpers] = useField(name);
const { i18n } = useLingui();
const BooleanField = withI18n()(
({ i18n, ariaLabel = '', name, config, disabled = false }) => {
const [field, meta, helpers] = useField(name);
return config ? (
<SettingGroup
defaultValue={config.default ?? false}
fieldId={name}
helperTextInvalid={meta.error}
return config ? (
<SettingGroup
defaultValue={config.default ?? false}
fieldId={name}
helperTextInvalid={meta.error}
isDisabled={disabled}
label={config.label}
popoverContent={config.help_text}
>
<Switch
id={name}
ouiaId={name}
isChecked={field.value}
isDisabled={disabled}
label={config.label}
popoverContent={config.help_text}
>
<Switch
id={name}
ouiaId={name}
isChecked={field.value}
isDisabled={disabled}
label={i18n._(t`On`)}
labelOff={i18n._(t`Off`)}
onChange={checked => helpers.setValue(checked)}
aria-label={ariaLabel || config.label}
/>
</SettingGroup>
) : null;
}
);
label={i18n._(t`On`)}
labelOff={i18n._(t`Off`)}
onChange={checked => helpers.setValue(checked)}
aria-label={ariaLabel || config.label}
/>
</SettingGroup>
) : null;
};
BooleanField.propTypes = {
name: string.isRequired,
config: shape({}).isRequired,
ariaLabel: string,
disabled: bool,
};
const ChoiceField = withI18n()(({ i18n, name, config, isRequired = false }) => {
const ChoiceField = ({ name, config, isRequired = false }) => {
const { i18n } = useLingui();
const validate = isRequired ? required(null, i18n) : null;
const [field, meta] = useField({ name, validate });
const isValid = !meta.error || !meta.touched;
@ -137,133 +136,130 @@ const ChoiceField = withI18n()(({ i18n, name, config, isRequired = false }) => {
/>
</SettingGroup>
) : null;
});
};
ChoiceField.propTypes = {
name: string.isRequired,
config: shape({}).isRequired,
isRequired: bool,
};
const EncryptedField = withI18n()(
({ i18n, name, config, isRequired = false }) => {
const validate = isRequired ? required(null, i18n) : null;
const [, meta] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
const EncryptedField = ({ name, config, isRequired = false }) => {
const { i18n } = useLingui();
return config ? (
<SettingGroup
defaultValue={config.default ?? ''}
fieldId={name}
helperTextInvalid={meta.error}
isRequired={isRequired}
label={config.label}
popoverContent={config.help_text}
validated={isValid ? 'default' : 'error'}
>
<InputGroup>
<PasswordInput
id={name}
name={name}
label={config.label}
validate={validate}
isRequired={isRequired}
/>
</InputGroup>
</SettingGroup>
) : null;
}
);
const validate = isRequired ? required(null, i18n) : null;
const [, meta] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
return config ? (
<SettingGroup
defaultValue={config.default ?? ''}
fieldId={name}
helperTextInvalid={meta.error}
isRequired={isRequired}
label={config.label}
popoverContent={config.help_text}
validated={isValid ? 'default' : 'error'}
>
<InputGroup>
<PasswordInput
id={name}
name={name}
label={config.label}
validate={validate}
isRequired={isRequired}
/>
</InputGroup>
</SettingGroup>
) : null;
};
EncryptedField.propTypes = {
name: string.isRequired,
config: shape({}).isRequired,
isRequired: bool,
};
const InputField = withI18n()(
({ i18n, name, config, type = 'text', isRequired = false }) => {
const min_value = config?.min_value ?? Number.MIN_SAFE_INTEGER;
const max_value = config?.max_value ?? Number.MAX_SAFE_INTEGER;
const validators = [
...(isRequired ? [required(null, i18n)] : []),
...(type === 'url' ? [url(i18n)] : []),
...(type === 'number'
? [integer(i18n), minMaxValue(min_value, max_value, i18n)]
: []),
];
const [field, meta] = useField({ name, validate: combine(validators) });
const isValid = !(meta.touched && meta.error);
const InputField = ({ name, config, type = 'text', isRequired = false }) => {
const { i18n } = useLingui();
return config ? (
<SettingGroup
defaultValue={config.default ?? ''}
fieldId={name}
helperTextInvalid={meta.error}
const min_value = config?.min_value ?? Number.MIN_SAFE_INTEGER;
const max_value = config?.max_value ?? Number.MAX_SAFE_INTEGER;
const validators = [
...(isRequired ? [required(null, i18n)] : []),
...(type === 'url' ? [url(i18n)] : []),
...(type === 'number'
? [integer(i18n), minMaxValue(min_value, max_value, i18n)]
: []),
];
const [field, meta] = useField({ name, validate: combine(validators) });
const isValid = !(meta.touched && meta.error);
return config ? (
<SettingGroup
defaultValue={config.default ?? ''}
fieldId={name}
helperTextInvalid={meta.error}
isRequired={isRequired}
label={config.label}
popoverContent={config.help_text}
validated={isValid ? 'default' : 'error'}
>
<TextInput
id={name}
isRequired={isRequired}
label={config.label}
popoverContent={config.help_text}
placeholder={config.placeholder}
validated={isValid ? 'default' : 'error'}
>
<TextInput
id={name}
isRequired={isRequired}
placeholder={config.placeholder}
validated={isValid ? 'default' : 'error'}
value={field.value}
onBlur={field.onBlur}
onChange={(value, event) => {
field.onChange(event);
}}
/>
</SettingGroup>
) : null;
}
);
value={field.value}
onBlur={field.onBlur}
onChange={(value, event) => {
field.onChange(event);
}}
/>
</SettingGroup>
) : null;
};
InputField.propTypes = {
name: string.isRequired,
config: shape({}).isRequired,
type: oneOf(['text', 'number', 'url']),
isRequired: bool,
};
const TextAreaField = withI18n()(
({ i18n, name, config, isRequired = false }) => {
const validate = isRequired ? required(null, i18n) : null;
const [field, meta] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
const TextAreaField = ({ name, config, isRequired = false }) => {
const { i18n } = useLingui();
return config ? (
<SettingGroup
defaultValue={config.default || ''}
fieldId={name}
helperTextInvalid={meta.error}
const validate = isRequired ? required(null, i18n) : null;
const [field, meta] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
return config ? (
<SettingGroup
defaultValue={config.default || ''}
fieldId={name}
helperTextInvalid={meta.error}
isRequired={isRequired}
label={config.label}
popoverContent={config.help_text}
validated={isValid ? 'default' : 'error'}
>
<TextArea
id={name}
isRequired={isRequired}
label={config.label}
popoverContent={config.help_text}
placeholder={config.placeholder}
validated={isValid ? 'default' : 'error'}
>
<TextArea
id={name}
isRequired={isRequired}
placeholder={config.placeholder}
validated={isValid ? 'default' : 'error'}
value={field.value}
onBlur={field.onBlur}
onChange={(value, event) => {
field.onChange(event);
}}
resizeOrientation="vertical"
/>
</SettingGroup>
) : null;
}
);
value={field.value}
onBlur={field.onBlur}
onChange={(value, event) => {
field.onChange(event);
}}
resizeOrientation="vertical"
/>
</SettingGroup>
) : null;
};
TextAreaField.propTypes = {
name: string.isRequired,
config: shape({}).isRequired,
isRequired: bool,
};
const ObjectField = withI18n()(({ i18n, name, config, isRequired = false }) => {
const ObjectField = ({ name, config, isRequired = false }) => {
const { i18n } = useLingui();
const validate = isRequired ? required(null, i18n) : null;
const [field, meta, helpers] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
@ -297,76 +293,79 @@ const ObjectField = withI18n()(({ i18n, name, config, isRequired = false }) => {
</SettingGroup>
</FormFullWidthLayout>
) : null;
});
};
ObjectField.propTypes = {
name: string.isRequired,
config: shape({}).isRequired,
isRequired: bool,
};
const FileUploadIconWrapper = styled.div`
margin: var(--pf-global--spacer--md);
`;
const FileUploadField = withI18n()(
({ i18n, name, config, type = 'text', isRequired = false }) => {
const validate = isRequired ? required(null, i18n) : null;
const [filename, setFilename] = useState('');
const [fileIsUploading, setFileIsUploading] = useState(false);
const [field, meta, helpers] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
const FileUploadField = ({
name,
config,
type = 'text',
isRequired = false,
}) => {
const { i18n } = useLingui();
return config ? (
<FormFullWidthLayout>
<SettingGroup
defaultValue={config.default ?? ''}
fieldId={name}
helperTextInvalid={meta.error}
isRequired={isRequired}
label={config.label}
popoverContent={config.help_text}
const validate = isRequired ? required(null, i18n) : null;
const [filename, setFilename] = useState('');
const [fileIsUploading, setFileIsUploading] = useState(false);
const [field, meta, helpers] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
return config ? (
<FormFullWidthLayout>
<SettingGroup
defaultValue={config.default ?? ''}
fieldId={name}
helperTextInvalid={meta.error}
isRequired={isRequired}
label={config.label}
popoverContent={config.help_text}
validated={isValid ? 'default' : 'error'}
onRevertCallback={() => setFilename('')}
>
<FileUpload
{...field}
id={name}
type={type}
filename={filename}
onChange={(value, title) => {
helpers.setValue(value);
setFilename(title);
}}
onReadStarted={() => setFileIsUploading(true)}
onReadFinished={() => setFileIsUploading(false)}
isLoading={fileIsUploading}
allowEditingUploadedText
validated={isValid ? 'default' : 'error'}
onRevertCallback={() => setFilename('')}
hideDefaultPreview={type === 'dataURL'}
>
<FileUpload
{...field}
id={name}
type={type}
filename={filename}
onChange={(value, title) => {
helpers.setValue(value);
setFilename(title);
}}
onReadStarted={() => setFileIsUploading(true)}
onReadFinished={() => setFileIsUploading(false)}
isLoading={fileIsUploading}
allowEditingUploadedText
validated={isValid ? 'default' : 'error'}
hideDefaultPreview={type === 'dataURL'}
>
{type === 'dataURL' && (
<FileUploadIconWrapper>
{field.value ? (
<img
src={field.value}
alt={filename}
height="200px"
width="200px"
/>
) : (
<FileUploadIcon size="lg" />
)}
</FileUploadIconWrapper>
)}
</FileUpload>
</SettingGroup>
</FormFullWidthLayout>
) : null;
}
);
{type === 'dataURL' && (
<FileUploadIconWrapper>
{field.value ? (
<img
src={field.value}
alt={filename}
height="200px"
width="200px"
/>
) : (
<FileUploadIcon size="lg" />
)}
</FileUploadIconWrapper>
)}
</FileUpload>
</SettingGroup>
</FormFullWidthLayout>
) : null;
};
FileUploadField.propTypes = {
name: string.isRequired,
config: shape({}).isRequired,
isRequired: bool,
};
export {

View File

@ -3,6 +3,7 @@ import { mount } from 'enzyme';
import { Formik } from 'formik';
import { I18nProvider } from '@lingui/react';
import { act } from 'react-dom/test-utils';
import { i18n } from '@lingui/core';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import {
BooleanField,
@ -13,13 +14,17 @@ import {
ObjectField,
TextAreaField,
} from './SharedFields';
import en from '../../../locales/en/messages';
describe('Setting form fields', () => {
test('BooleanField renders the expected content', async () => {
const outerNode = document.createElement('div');
document.body.appendChild(outerNode);
i18n.loadLocaleData({ en: { plurals: en } });
i18n.load({ en });
i18n.activate('en');
const wrapper = mount(
<I18nProvider>
<I18nProvider i18n={i18n}>
<Formik
initialValues={{
boolean: true,
@ -244,7 +249,10 @@ describe('Setting form fields', () => {
)}
</Formik>
);
expect(wrapper.find('FileUploadField')).toHaveLength(1);
expect(
wrapper.find('FileUploadField[value="mock file value"]')
).toHaveLength(1);
expect(wrapper.find('label').text()).toEqual('mock file label');
expect(wrapper.find('input#mock_file-filename').prop('value')).toEqual('');
await act(async () => {

View File

@ -2,14 +2,21 @@ import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core';
import { en } from 'make-plural/plurals';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import TeamListItem from './TeamListItem';
import english from '../../../locales/en/messages';
i18n.loadLocaleData({ en: { plurals: en } });
i18n.load({ en: english });
i18n.activate('en');
describe('<TeamListItem />', () => {
test('initially renders succesfully', () => {
mountWithContexts(
<I18nProvider>
<I18nProvider i18n={i18n}>
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
<table>
<tbody>
@ -35,7 +42,7 @@ describe('<TeamListItem />', () => {
});
test('edit button shown to users with edit capabilities', () => {
const wrapper = mountWithContexts(
<I18nProvider>
<I18nProvider i18n={i18n}>
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
<table>
<tbody>
@ -62,7 +69,7 @@ describe('<TeamListItem />', () => {
});
test('edit button hidden from users without edit capabilities', () => {
const wrapper = mountWithContexts(
<I18nProvider>
<I18nProvider i18n={i18n}>
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
<table>
<tbody>

View File

@ -98,8 +98,7 @@ function JobTemplateDetail({ i18n, template }) {
const { error, dismissError } = useDismissableError(deleteError);
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
template,
i18n
template
);
const canLaunch =
summary_fields.user_capabilities && summary_fields.user_capabilities.start;

View File

@ -104,8 +104,7 @@ function WorkflowJobTemplateDetail({ template, i18n }) {
}));
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
template,
i18n
template
);
return (

View File

@ -2,10 +2,17 @@ import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core';
import { en } from 'make-plural/plurals';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import mockDetails from '../data.user.json';
import UserListItem from './UserListItem';
import english from '../../../locales/en/messages';
i18n.loadLocaleData({ en: { plurals: en } });
i18n.load({ en: english });
i18n.activate('en');
let wrapper;
@ -16,7 +23,7 @@ afterEach(() => {
describe('UserListItem with full permissions', () => {
beforeEach(() => {
wrapper = mountWithContexts(
<I18nProvider>
<I18nProvider i18n={i18n}>
<MemoryRouter initialEntries={['/users']} initialIndex={0}>
<table>
<tbody>
@ -52,7 +59,7 @@ describe('UserListItem with full permissions', () => {
describe('UserListItem without full permissions', () => {
test('edit button hidden from users without edit capabilities', () => {
wrapper = mountWithContexts(
<I18nProvider>
<I18nProvider i18n={i18n}>
<MemoryRouter initialEntries={['/users']} initialIndex={0}>
<table>
<tbody>

View File

@ -1,13 +1,20 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core';
import { en } from 'make-plural/plurals';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import UserTeamListItem from './UserTeamListItem';
import english from '../../../locales/en/messages';
i18n.loadLocaleData({ en: { plurals: en } });
i18n.load({ en: english });
i18n.activate('en');
describe('<UserTeamListItem />', () => {
test('should render item', () => {
const wrapper = mountWithContexts(
<I18nProvider>
<I18nProvider i18n={i18n}>
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
<UserTeamListItem
team={{

View File

@ -10,6 +10,8 @@ import {
WorkflowJobTemplatesAPI,
WorkflowJobTemplateNodesAPI,
CredentialsAPI,
ExecutionEnvironmentsAPI,
CredentialInputSourcesAPI,
} from '../api';
jest.mock('../api/models/Credentials');
@ -19,17 +21,8 @@ jest.mock('../api/models/JobTemplates');
jest.mock('../api/models/Projects');
jest.mock('../api/models/WorkflowJobTemplates');
jest.mock('../api/models/WorkflowJobTemplateNodes');
const i18n = {
_: key => {
if (key.values) {
Object.entries(key.values).forEach(([k, v]) => {
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
});
}
return key.id;
},
};
jest.mock('../api/models/CredentialInputSources');
jest.mock('../api/models/ExecutionEnvironments');
describe('delete details', () => {
afterEach(() => {
@ -38,7 +31,7 @@ describe('delete details', () => {
test('should call api for credentials list', () => {
getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.credential({ id: 1 }, i18n)
relatedResourceDeleteRequests.credential({ id: 1 })
);
expect(InventoriesAPI.read).toBeCalledWith({
insights_credential: 1,
@ -52,7 +45,7 @@ describe('delete details', () => {
test('should call api for projects list', () => {
getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.project({ id: 1 }, i18n)
relatedResourceDeleteRequests.project({ id: 1 })
);
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
unified_job_template: 1,
@ -65,7 +58,7 @@ describe('delete details', () => {
test('should call api for templates list', () => {
getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.template({ id: 1 }, i18n)
relatedResourceDeleteRequests.template({ id: 1 })
);
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
unified_job_template: 1,
@ -74,7 +67,7 @@ describe('delete details', () => {
test('should call api for credential type list', () => {
getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.credentialType({ id: 1 }, i18n)
relatedResourceDeleteRequests.credentialType({ id: 1 })
);
expect(CredentialsAPI.read).toBeCalledWith({
credential_type__id: 1,
@ -83,7 +76,7 @@ describe('delete details', () => {
test('should call api for inventory list', () => {
getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.inventory({ id: 1 }, i18n)
relatedResourceDeleteRequests.inventory({ id: 1 })
);
expect(JobTemplatesAPI.read).toBeCalledWith({ inventory: 1 });
expect(WorkflowJobTemplatesAPI.read).toBeCalledWith({
@ -96,7 +89,7 @@ describe('delete details', () => {
data: [{ inventory_source: 2 }],
});
await getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.inventorySource(1, i18n)
relatedResourceDeleteRequests.inventorySource(1)
);
expect(InventoriesAPI.updateSources).toBeCalledWith(1);
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
@ -106,7 +99,7 @@ describe('delete details', () => {
test('should call api for organization list', async () => {
getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.organization({ id: 1 }, i18n)
relatedResourceDeleteRequests.organization({ id: 1 })
);
expect(CredentialsAPI.read).toBeCalledWith({ organization: 1 });
});
@ -123,7 +116,7 @@ describe('delete details', () => {
},
});
const { error } = await getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.inventorySource(1, i18n)
relatedResourceDeleteRequests.inventorySource(1)
);
expect(InventoriesAPI.updateSources).toBeCalledWith(1);
@ -131,14 +124,24 @@ describe('delete details', () => {
});
test('should return proper results', async () => {
JobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
JobTemplatesAPI.read.mockResolvedValue({ data: { count: 1 } });
InventorySourcesAPI.read.mockResolvedValue({ data: { count: 10 } });
CredentialInputSourcesAPI.read.mockResolvedValue({ data: { count: 20 } });
ExecutionEnvironmentsAPI.read.mockResolvedValue({ data: { count: 30 } });
ProjectsAPI.read.mockResolvedValue({ data: { count: 2 } });
InventoriesAPI.read.mockResolvedValue({ data: { count: 3 } });
InventorySourcesAPI.read.mockResolvedValue({ data: { count: 0 } });
const { results } = await getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.credential({ id: 1 }, i18n)
relatedResourceDeleteRequests.credential({ id: 1 })
);
expect(results).toEqual({ Projects: 2, Inventories: 3 });
expect(results).toEqual({
'Job Templates': 1,
Projects: 2,
Inventories: 3,
'Inventory Sources': 10,
'Credential Input Sources': 20,
'Execution Environments': 30,
});
});
});

View File

@ -1,4 +1,5 @@
import { t } from '@lingui/macro';
import { i18n } from '@lingui/core';
import {
UnifiedJobTemplatesAPI,
@ -46,7 +47,7 @@ export async function getRelatedResourceDeleteCounts(requests) {
}
export const relatedResourceDeleteRequests = {
credential: (selected, i18n) => [
credential: selected => [
{
request: () =>
JobTemplatesAPI.read({
@ -77,7 +78,7 @@ export const relatedResourceDeleteRequests = {
CredentialInputSourcesAPI.read({
source_credential: selected.id,
}),
label: i18n._(t`Credential`),
label: i18n._(t`Credential Input Sources`),
},
{
request: () =>
@ -88,7 +89,7 @@ export const relatedResourceDeleteRequests = {
},
],
credentialType: (selected, i18n) => [
credentialType: selected => [
{
request: async () =>
CredentialsAPI.read({
@ -98,7 +99,7 @@ export const relatedResourceDeleteRequests = {
},
],
inventory: (selected, i18n) => [
inventory: selected => [
{
request: async () =>
JobTemplatesAPI.read({
@ -112,7 +113,7 @@ export const relatedResourceDeleteRequests = {
},
],
inventorySource: (inventoryId, i18n, inventorySource) => [
inventorySource: (inventoryId, inventorySource) => [
{
request: async () => {
try {
@ -147,7 +148,7 @@ export const relatedResourceDeleteRequests = {
},
],
project: (selected, i18n) => [
project: selected => [
{
request: () =>
JobTemplatesAPI.read({
@ -171,7 +172,7 @@ export const relatedResourceDeleteRequests = {
},
],
template: (selected, i18n) => [
template: selected => [
{
request: async () =>
WorkflowJobTemplateNodesAPI.read({
@ -181,7 +182,7 @@ export const relatedResourceDeleteRequests = {
},
],
organization: (selected, i18n) => [
organization: selected => [
{
request: async () =>
CredentialsAPI.read({
@ -232,7 +233,7 @@ export const relatedResourceDeleteRequests = {
label: i18n._(t`Applications`),
},
],
executionEnvironment: (selected, i18n) => [
executionEnvironment: selected => [
{
request: async () =>
UnifiedJobTemplatesAPI.read({
@ -283,7 +284,7 @@ export const relatedResourceDeleteRequests = {
label: [i18n._(t`Workflow Job Template Nodes`)],
},
],
instanceGroup: (selected, i18n) => [
instanceGroup: selected => [
{
request: () => OrganizationsAPI.read({ instance_groups: selected.id }),
label: i18n._(t`Organizations`),

View File

@ -20,9 +20,7 @@ describe('validators', () => {
});
test('required returns default message if value missing', () => {
expect(required(null, i18n)('')).toEqual({
id: 'This field must not be blank',
});
expect(required(null, i18n)('')).toEqual('This field must not be blank');
});
test('required returns custom message if value missing', () => {
@ -30,18 +28,14 @@ describe('validators', () => {
});
test('required interprets white space as empty value', () => {
expect(required(null, i18n)(' ')).toEqual({
id: 'This field must not be blank',
});
expect(required(null, i18n)('\t')).toEqual({
id: 'This field must not be blank',
});
expect(required(null, i18n)(' ')).toEqual('This field must not be blank');
expect(required(null, i18n)('\t')).toEqual('This field must not be blank');
});
test('required interprets undefined as empty value', () => {
expect(required(null, i18n)(undefined)).toEqual({
id: 'This field must not be blank',
});
expect(required(null, i18n)(undefined)).toEqual(
'This field must not be blank'
);
});
test('required interprets 0 as non-empty value', () => {
@ -57,10 +51,9 @@ describe('validators', () => {
});
test('maxLength rejects value above max', () => {
expect(maxLength(8, i18n)('abracadbra')).toEqual({
id: 'This field must not exceed {max} characters',
values: { max: 8 },
});
expect(maxLength(8, i18n)('abracadbra')).toEqual(
'This field must not exceed {max} characters'
);
});
test('minLength accepts value above min', () => {
@ -72,22 +65,21 @@ describe('validators', () => {
});
test('minLength rejects value below min', () => {
expect(minLength(12, i18n)('abracadbra')).toEqual({
id: 'This field must be at least {min} characters',
values: { min: 12 },
});
expect(minLength(12, i18n)('abracadbra')).toEqual(
'This field must be at least {min} characters'
);
});
test('noWhiteSpace returns error', () => {
expect(noWhiteSpace(i18n)('this has spaces')).toEqual({
id: 'This field must not contain spaces',
});
expect(noWhiteSpace(i18n)('this has\twhitespace')).toEqual({
id: 'This field must not contain spaces',
});
expect(noWhiteSpace(i18n)('this\nhas\nnewlines')).toEqual({
id: 'This field must not contain spaces',
});
expect(noWhiteSpace(i18n)('this has spaces')).toEqual(
'This field must not contain spaces'
);
expect(noWhiteSpace(i18n)('this has\twhitespace')).toEqual(
'This field must not contain spaces'
);
expect(noWhiteSpace(i18n)('this\nhas\nnewlines')).toEqual(
'This field must not contain spaces'
);
});
test('noWhiteSpace should accept valid string', () => {
@ -103,15 +95,11 @@ describe('validators', () => {
});
test('integer should reject decimal/float', () => {
expect(integer(i18n)(13.1)).toEqual({
id: 'This field must be an integer',
});
expect(integer(i18n)(13.1)).toEqual('This field must be an integer');
});
test('integer should reject string containing alphanum', () => {
expect(integer(i18n)('15a')).toEqual({
id: 'This field must be an integer',
});
expect(integer(i18n)('15a')).toEqual('This field must be an integer');
});
test('number should accept number (number)', () => {
@ -136,15 +124,11 @@ describe('validators', () => {
});
test('number should reject string containing alphanum', () => {
expect(number(i18n)('15a')).toEqual({
id: 'This field must be a number',
});
expect(number(i18n)('15a')).toEqual('This field must be a number');
});
test('url should reject incomplete url', () => {
expect(url(i18n)('abcd')).toEqual({
id: 'Please enter a valid URL',
});
expect(url(i18n)('abcd')).toEqual('Please enter a valid URL');
});
test('url should accept fully qualified url', () => {
@ -156,43 +140,37 @@ describe('validators', () => {
});
test('url should reject short protocol', () => {
expect(url(i18n)('h://example.com/foo')).toEqual({
id: 'Please enter a valid URL',
});
expect(url(i18n)('h://example.com/foo')).toEqual(
'Please enter a valid URL'
);
});
test('combine should run all validators', () => {
const validators = [required(null, i18n), noWhiteSpace(i18n)];
expect(combine(validators)('')).toEqual({
id: 'This field must not be blank',
});
expect(combine(validators)('one two')).toEqual({
id: 'This field must not contain spaces',
});
expect(combine(validators)('')).toEqual('This field must not be blank');
expect(combine(validators)('one two')).toEqual(
'This field must not contain spaces'
);
expect(combine(validators)('ok')).toBeUndefined();
});
test('combine should skip null validators', () => {
const validators = [required(null, i18n), null];
expect(combine(validators)('')).toEqual({
id: 'This field must not be blank',
});
expect(combine(validators)('')).toEqual('This field must not be blank');
expect(combine(validators)('ok')).toBeUndefined();
});
test('regExp rejects invalid regular expression', () => {
expect(regExp(i18n)('[')).toEqual({
id: 'This field must be a regular expression',
});
expect(regExp(i18n)('[')).toEqual(
'This field must be a regular expression'
);
expect(regExp(i18n)('')).toBeUndefined();
expect(regExp(i18n)('ok')).toBeUndefined();
expect(regExp(i18n)('[^a-zA-Z]')).toBeUndefined();
});
test('email validator rejects obviously invalid email ', () => {
expect(requiredEmail(i18n)('foobar321')).toEqual({
id: 'Invalid email address',
});
expect(requiredEmail(i18n)('foobar321')).toEqual('Invalid email address');
});
test('bob has email', () => {

View File

@ -3,41 +3,20 @@
* derived from https://lingui.js.org/guides/testing.html
*/
import React from 'react';
import { shape, object, string, arrayOf } from 'prop-types';
import { shape, string, arrayOf } from 'prop-types';
import { mount, shallow } from 'enzyme';
import { MemoryRouter, Router } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import { ConfigProvider } from '../src/contexts/Config'
import { i18n } from '@lingui/core';
import { en } from 'make-plural/plurals';
import english from '../src/locales/en/messages';
import { ConfigProvider } from '../src/contexts/Config';
const language = 'en-US';
const intlProvider = new I18nProvider(
{
language,
catalogs: {
[language]: {},
},
},
{}
);
const {
linguiPublisher: { i18n: originalI18n },
} = intlProvider.getChildContext();
i18n.loadLocaleData({ en: { plurals: en } });
i18n.load({ en: english });
i18n.activate('en');
const defaultContexts = {
linguiPublisher: {
i18n: {
...originalI18n,
_: key => {
if (key.values) {
Object.entries(key.values).forEach(([k, v]) => {
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
});
}
return key.id;
}, // provide _ macro, for just passing down the key
toJSON: () => '/i18n/',
},
},
config: {
ansible_version: null,
custom_virtualenvs: [],
@ -45,8 +24,8 @@ const defaultContexts = {
me: { is_superuser: true },
toJSON: () => '/config/',
license_info: {
valid_key: true
}
valid_key: true,
},
},
router: {
history_: {
@ -89,15 +68,19 @@ function wrapContexts(node, context) {
const component = React.cloneElement(children, props);
if (router.history) {
return (
<ConfigProvider value={config}>
<Router history={router.history}>{component}</Router>
</ConfigProvider>
<I18nProvider i18n={i18n}>
<ConfigProvider value={config}>
<Router history={router.history}>{component}</Router>
</ConfigProvider>
</I18nProvider>
);
}
return (
<ConfigProvider value={config}>
<MemoryRouter>{component}</MemoryRouter>
</ConfigProvider>
<I18nProvider i18n={i18n}>
<ConfigProvider value={config}>
<MemoryRouter>{component}</MemoryRouter>
</ConfigProvider>
</I18nProvider>
);
}
}
@ -127,9 +110,6 @@ export function shallowWithContexts(node, options = {}) {
export function mountWithContexts(node, options = {}) {
const context = applyDefaultContexts(options.context);
const childContextTypes = {
linguiPublisher: shape({
i18n: object.isRequired, // eslint-disable-line react/forbid-prop-types
}).isRequired,
config: shape({
ansible_version: string,
custom_virtualenvs: arrayOf(string),