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", "RunOnRadio",
"NodeTypeLetter", "NodeTypeLetter",
"SelectableItem", "SelectableItem",
"Dash" "Dash",
"Plural"
], ],
"ignoreCallee": ["describe"] "ignoreCallee": ["describe"]
} }

View File

@@ -1,6 +1,17 @@
{ {"catalogs":[{
"localeDir": "src/locales/", "path": "<rootDir>/locales/{locale}/messages",
"srcPathDirs": ["src/"], "include": ["<rootDir>"],
"format": "po", "exclude": ["**/node_modules/**"]
"sourceLocale": "en" }],
"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" "node": "14.x"
}, },
"dependencies": { "dependencies": {
"@lingui/react": "^2.9.1", "@lingui/react": "^3.7.1",
"@patternfly/patternfly": "^4.80.3", "@patternfly/patternfly": "^4.80.3",
"@patternfly/react-core": "^4.90.2", "@patternfly/react-core": "^4.90.2",
"@patternfly/react-icons": "4.7.22", "@patternfly/react-icons": "4.7.22",
@@ -14,6 +14,8 @@
"ace-builds": "^1.4.12", "ace-builds": "^1.4.12",
"ansi-to-html": "^0.6.11", "ansi-to-html": "^0.6.11",
"axios": "^0.21.1", "axios": "^0.21.1",
"babel-plugin-macros": "^3.0.1",
"codemirror": "^5.47.0",
"d3": "^5.12.0", "d3": "^5.12.0",
"dagre": "^0.8.4", "dagre": "^0.8.4",
"formik": "^2.1.2", "formik": "^2.1.2",
@@ -33,8 +35,8 @@
"devDependencies": { "devDependencies": {
"@babel/polyfill": "^7.8.7", "@babel/polyfill": "^7.8.7",
"@cypress/instrument-cra": "^1.4.0", "@cypress/instrument-cra": "^1.4.0",
"@lingui/cli": "^2.9.2", "@lingui/cli": "^3.7.1",
"@lingui/macro": "^2.9.1", "@lingui/macro": "^3.7.1",
"@nteract/mockument": "^1.0.4", "@nteract/mockument": "^1.0.4",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"enzyme": "^3.10.0", "enzyme": "^3.10.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,15 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import {
mountWithContexts,
shallowWithContexts,
} from '../../../testUtils/enzymeHelpers';
import Sparkline from './Sparkline'; import Sparkline from './Sparkline';
describe('Sparkline', () => { describe('Sparkline', () => {
test('renders the expected content', () => { test('renders the expected content', () => {
const wrapper = mount(<Sparkline />); const wrapper = shallowWithContexts(<Sparkline />);
expect(wrapper).toHaveLength(1); expect(wrapper).toHaveLength(1);
}); });
test('renders an icon with tooltips and links for each job', () => { 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( const deleteDetailsRequests = relatedResourceDeleteRequests.template(
selected[0], selected[0]
i18n
); );
return ( return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { useLocation, useRouteMatch, Link } from 'react-router-dom'; import { useLocation, useRouteMatch, Link } from 'react-router-dom';
import { withI18n } from '@lingui/react'; 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 { Card, PageSection, DropdownItem } from '@patternfly/react-core';
import { InventoriesAPI } from '../../../api'; import { InventoriesAPI } from '../../../api';
import useRequest, { useDeleteItems } from '../../../util/useRequest'; import useRequest, { useDeleteItems } from '../../../util/useRequest';
@@ -129,8 +129,7 @@ function InventoryList({ i18n }) {
}; };
const deleteDetailsRequests = relatedResourceDeleteRequests.inventory( const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(
selected[0], selected[0]
i18n
); );
const addInventory = i18n._(t`Add inventory`); const addInventory = i18n._(t`Add inventory`);
@@ -219,15 +218,14 @@ function InventoryList({ i18n }) {
onDelete={handleInventoryDelete} onDelete={handleInventoryDelete}
itemsToDelete={selected} itemsToDelete={selected}
pluralizedItemName={i18n._(t`Inventories`)} 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} 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( const deleteDetailsRequests = relatedResourceDeleteRequests.inventorySource(
inventorySource.inventory, inventorySource.inventory,
i18n,
inventorySource inventorySource
); );

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useHistory, useLocation, withRouter } from 'react-router-dom'; import { useHistory, useLocation, withRouter } from 'react-router-dom';
import { I18n } from '@lingui/react'; import { i18n } from '@lingui/core';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
@@ -625,7 +625,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
}; };
const renderSearchComponent = i18n => ( const renderSearchComponent = () => (
<Search <Search
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
columns={[ columns={[
@@ -688,176 +688,157 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
} }
return ( return (
<I18n> <>
{({ i18n }) => ( <CardBody>
<> {isHostModalOpen && (
<CardBody> <HostEventModal
{isHostModalOpen && ( onClose={handleHostModalClose}
<HostEventModal isOpen={isHostModalOpen}
onClose={handleHostModalClose} hostEvent={hostEvent}
isOpen={isHostModalOpen} />
hostEvent={hostEvent} )}
/> <OutputHeader>
)} <HeaderTitle>
<OutputHeader> <StatusIcon status={job.status} />
<HeaderTitle> <h1>{job.name}</h1>
<StatusIcon status={job.status} /> </HeaderTitle>
<h1>{job.name}</h1> <OutputToolbar
</HeaderTitle> job={job}
<OutputToolbar jobStatus={jobStatus}
job={job} onCancel={() => setShowCancelModal(true)}
jobStatus={jobStatus} onDelete={deleteJob}
onCancel={() => setShowCancelModal(true)} isDeleteDisabled={isDeleting}
onDelete={deleteJob} />
isDeleteDisabled={isDeleting} </OutputHeader>
/> <HostStatusBar counts={job.host_status_counts} />
</OutputHeader> <SearchToolbar
<HostStatusBar counts={job.host_status_counts} /> id="job_output-toolbar"
<SearchToolbar clearAllFilters={handleRemoveAllSearchTerms}
id="job_output-toolbar" collapseListedFiltersBreakpoint="lg"
clearAllFilters={handleRemoveAllSearchTerms} clearFiltersButtonText={i18n._(t`Clear all filters`)}
collapseListedFiltersBreakpoint="lg" >
clearFiltersButtonText={i18n._(t`Clear all filters`)} <SearchToolbarContent>
> <ToolbarToggleGroup toggleIcon={<SearchIcon />} breakpoint="lg">
<SearchToolbarContent> <ToolbarItem variant="search-filter">
<ToolbarToggleGroup toggleIcon={<SearchIcon />} breakpoint="lg"> {isJobRunning(job.status) ? (
<ToolbarItem variant="search-filter"> <Tooltip
{isJobRunning(job.status) ? ( content={i18n._(
<Tooltip t`Search is disabled while the job is running`
content={i18n._(
t`Search is disabled while the job is running`
)}
>
{renderSearchComponent(i18n)}
</Tooltip>
) : (
renderSearchComponent(i18n)
)} )}
<Tooltip >
content={i18n._(t`Job output documentation`)} {renderSearchComponent(i18n)}
position="bottom" </Tooltip>
> ) : (
<Button renderSearchComponent(i18n)
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>
)} )}
</InfiniteLoader> </ToolbarItem>
<OutputFooter /> </ToolbarToggleGroup>
</OutputWrapper> </SearchToolbarContent>
</CardBody> </SearchToolbar>
{showCancelModal && isJobRunning(job.status) && ( <PageControls
<AlertModal onScrollFirst={handleScrollFirst}
isOpen={showCancelModal} 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" variant="danger"
onClose={() => setShowCancelModal(false)} isDisabled={isCancelling}
title={i18n._(t`Cancel Job`)} aria-label={i18n._(t`Cancel job`)}
label={i18n._(t`Cancel Job`)} onClick={cancelJob}
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>,
]}
> >
{i18n._( {i18n._(t`Cancel job`)}
t`Are you sure you want to submit the request to cancel this job?` </Button>,
)} <Button
</AlertModal> id="cancel-job-return-button"
)} key="cancel"
{dismissableDeleteError && ( variant="secondary"
<AlertModal aria-label={i18n._(t`Return`)}
isOpen={dismissableDeleteError} onClick={() => setShowCancelModal(false)}
variant="danger"
onClose={dismissDeleteError}
title={i18n._(t`Job Delete Error`)}
label={i18n._(t`Job Delete Error`)}
> >
<ErrorDetail error={dismissableDeleteError} /> {i18n._(t`Return`)}
</AlertModal> </Button>,
]}
>
{i18n._(
t`Are you sure you want to submit the request to cancel this job?`
)} )}
{dismissableCancelError && ( </AlertModal>
<AlertModal
isOpen={dismissableCancelError}
variant="danger"
onClose={dismissCancelError}
title={i18n._(t`Job Cancel Error`)}
label={i18n._(t`Job Cancel Error`)}
>
<ErrorDetail error={dismissableCancelError} />
</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 { error, dismissError } = useDismissableError(deleteError);
const deleteDetailsRequests = relatedResourceDeleteRequests.organization( const deleteDetailsRequests = relatedResourceDeleteRequests.organization(
organization, organization
i18n
); );
if (hasContentLoading) { if (hasContentLoading) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,8 @@ import {
WorkflowJobTemplatesAPI, WorkflowJobTemplatesAPI,
WorkflowJobTemplateNodesAPI, WorkflowJobTemplateNodesAPI,
CredentialsAPI, CredentialsAPI,
ExecutionEnvironmentsAPI,
CredentialInputSourcesAPI,
} from '../api'; } from '../api';
jest.mock('../api/models/Credentials'); jest.mock('../api/models/Credentials');
@@ -19,17 +21,8 @@ jest.mock('../api/models/JobTemplates');
jest.mock('../api/models/Projects'); jest.mock('../api/models/Projects');
jest.mock('../api/models/WorkflowJobTemplates'); jest.mock('../api/models/WorkflowJobTemplates');
jest.mock('../api/models/WorkflowJobTemplateNodes'); jest.mock('../api/models/WorkflowJobTemplateNodes');
jest.mock('../api/models/CredentialInputSources');
const i18n = { jest.mock('../api/models/ExecutionEnvironments');
_: key => {
if (key.values) {
Object.entries(key.values).forEach(([k, v]) => {
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
});
}
return key.id;
},
};
describe('delete details', () => { describe('delete details', () => {
afterEach(() => { afterEach(() => {
@@ -38,7 +31,7 @@ describe('delete details', () => {
test('should call api for credentials list', () => { test('should call api for credentials list', () => {
getRelatedResourceDeleteCounts( getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.credential({ id: 1 }, i18n) relatedResourceDeleteRequests.credential({ id: 1 })
); );
expect(InventoriesAPI.read).toBeCalledWith({ expect(InventoriesAPI.read).toBeCalledWith({
insights_credential: 1, insights_credential: 1,
@@ -52,7 +45,7 @@ describe('delete details', () => {
test('should call api for projects list', () => { test('should call api for projects list', () => {
getRelatedResourceDeleteCounts( getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.project({ id: 1 }, i18n) relatedResourceDeleteRequests.project({ id: 1 })
); );
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({ expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
unified_job_template: 1, unified_job_template: 1,
@@ -65,7 +58,7 @@ describe('delete details', () => {
test('should call api for templates list', () => { test('should call api for templates list', () => {
getRelatedResourceDeleteCounts( getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.template({ id: 1 }, i18n) relatedResourceDeleteRequests.template({ id: 1 })
); );
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({ expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
unified_job_template: 1, unified_job_template: 1,
@@ -74,7 +67,7 @@ describe('delete details', () => {
test('should call api for credential type list', () => { test('should call api for credential type list', () => {
getRelatedResourceDeleteCounts( getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.credentialType({ id: 1 }, i18n) relatedResourceDeleteRequests.credentialType({ id: 1 })
); );
expect(CredentialsAPI.read).toBeCalledWith({ expect(CredentialsAPI.read).toBeCalledWith({
credential_type__id: 1, credential_type__id: 1,
@@ -83,7 +76,7 @@ describe('delete details', () => {
test('should call api for inventory list', () => { test('should call api for inventory list', () => {
getRelatedResourceDeleteCounts( getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.inventory({ id: 1 }, i18n) relatedResourceDeleteRequests.inventory({ id: 1 })
); );
expect(JobTemplatesAPI.read).toBeCalledWith({ inventory: 1 }); expect(JobTemplatesAPI.read).toBeCalledWith({ inventory: 1 });
expect(WorkflowJobTemplatesAPI.read).toBeCalledWith({ expect(WorkflowJobTemplatesAPI.read).toBeCalledWith({
@@ -96,7 +89,7 @@ describe('delete details', () => {
data: [{ inventory_source: 2 }], data: [{ inventory_source: 2 }],
}); });
await getRelatedResourceDeleteCounts( await getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.inventorySource(1, i18n) relatedResourceDeleteRequests.inventorySource(1)
); );
expect(InventoriesAPI.updateSources).toBeCalledWith(1); expect(InventoriesAPI.updateSources).toBeCalledWith(1);
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({ expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
@@ -106,7 +99,7 @@ describe('delete details', () => {
test('should call api for organization list', async () => { test('should call api for organization list', async () => {
getRelatedResourceDeleteCounts( getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.organization({ id: 1 }, i18n) relatedResourceDeleteRequests.organization({ id: 1 })
); );
expect(CredentialsAPI.read).toBeCalledWith({ organization: 1 }); expect(CredentialsAPI.read).toBeCalledWith({ organization: 1 });
}); });
@@ -123,7 +116,7 @@ describe('delete details', () => {
}, },
}); });
const { error } = await getRelatedResourceDeleteCounts( const { error } = await getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.inventorySource(1, i18n) relatedResourceDeleteRequests.inventorySource(1)
); );
expect(InventoriesAPI.updateSources).toBeCalledWith(1); expect(InventoriesAPI.updateSources).toBeCalledWith(1);
@@ -131,14 +124,24 @@ describe('delete details', () => {
}); });
test('should return proper results', async () => { 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 } }); ProjectsAPI.read.mockResolvedValue({ data: { count: 2 } });
InventoriesAPI.read.mockResolvedValue({ data: { count: 3 } }); InventoriesAPI.read.mockResolvedValue({ data: { count: 3 } });
InventorySourcesAPI.read.mockResolvedValue({ data: { count: 0 } });
const { results } = await getRelatedResourceDeleteCounts( 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 { t } from '@lingui/macro';
import { i18n } from '@lingui/core';
import { import {
UnifiedJobTemplatesAPI, UnifiedJobTemplatesAPI,
@@ -46,7 +47,7 @@ export async function getRelatedResourceDeleteCounts(requests) {
} }
export const relatedResourceDeleteRequests = { export const relatedResourceDeleteRequests = {
credential: (selected, i18n) => [ credential: selected => [
{ {
request: () => request: () =>
JobTemplatesAPI.read({ JobTemplatesAPI.read({
@@ -77,7 +78,7 @@ export const relatedResourceDeleteRequests = {
CredentialInputSourcesAPI.read({ CredentialInputSourcesAPI.read({
source_credential: selected.id, source_credential: selected.id,
}), }),
label: i18n._(t`Credential`), label: i18n._(t`Credential Input Sources`),
}, },
{ {
request: () => request: () =>
@@ -88,7 +89,7 @@ export const relatedResourceDeleteRequests = {
}, },
], ],
credentialType: (selected, i18n) => [ credentialType: selected => [
{ {
request: async () => request: async () =>
CredentialsAPI.read({ CredentialsAPI.read({
@@ -98,7 +99,7 @@ export const relatedResourceDeleteRequests = {
}, },
], ],
inventory: (selected, i18n) => [ inventory: selected => [
{ {
request: async () => request: async () =>
JobTemplatesAPI.read({ JobTemplatesAPI.read({
@@ -112,7 +113,7 @@ export const relatedResourceDeleteRequests = {
}, },
], ],
inventorySource: (inventoryId, i18n, inventorySource) => [ inventorySource: (inventoryId, inventorySource) => [
{ {
request: async () => { request: async () => {
try { try {
@@ -147,7 +148,7 @@ export const relatedResourceDeleteRequests = {
}, },
], ],
project: (selected, i18n) => [ project: selected => [
{ {
request: () => request: () =>
JobTemplatesAPI.read({ JobTemplatesAPI.read({
@@ -171,7 +172,7 @@ export const relatedResourceDeleteRequests = {
}, },
], ],
template: (selected, i18n) => [ template: selected => [
{ {
request: async () => request: async () =>
WorkflowJobTemplateNodesAPI.read({ WorkflowJobTemplateNodesAPI.read({
@@ -181,7 +182,7 @@ export const relatedResourceDeleteRequests = {
}, },
], ],
organization: (selected, i18n) => [ organization: selected => [
{ {
request: async () => request: async () =>
CredentialsAPI.read({ CredentialsAPI.read({
@@ -232,7 +233,7 @@ export const relatedResourceDeleteRequests = {
label: i18n._(t`Applications`), label: i18n._(t`Applications`),
}, },
], ],
executionEnvironment: (selected, i18n) => [ executionEnvironment: selected => [
{ {
request: async () => request: async () =>
UnifiedJobTemplatesAPI.read({ UnifiedJobTemplatesAPI.read({
@@ -283,7 +284,7 @@ export const relatedResourceDeleteRequests = {
label: [i18n._(t`Workflow Job Template Nodes`)], label: [i18n._(t`Workflow Job Template Nodes`)],
}, },
], ],
instanceGroup: (selected, i18n) => [ instanceGroup: selected => [
{ {
request: () => OrganizationsAPI.read({ instance_groups: selected.id }), request: () => OrganizationsAPI.read({ instance_groups: selected.id }),
label: i18n._(t`Organizations`), label: i18n._(t`Organizations`),

View File

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

View File

@@ -3,41 +3,20 @@
* derived from https://lingui.js.org/guides/testing.html * derived from https://lingui.js.org/guides/testing.html
*/ */
import React from 'react'; 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 { mount, shallow } from 'enzyme';
import { MemoryRouter, Router } from 'react-router-dom'; import { MemoryRouter, Router } from 'react-router-dom';
import { I18nProvider } from '@lingui/react'; 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'; i18n.loadLocaleData({ en: { plurals: en } });
const intlProvider = new I18nProvider( i18n.load({ en: english });
{ i18n.activate('en');
language,
catalogs: {
[language]: {},
},
},
{}
);
const {
linguiPublisher: { i18n: originalI18n },
} = intlProvider.getChildContext();
const defaultContexts = { 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: { config: {
ansible_version: null, ansible_version: null,
custom_virtualenvs: [], custom_virtualenvs: [],
@@ -45,8 +24,8 @@ const defaultContexts = {
me: { is_superuser: true }, me: { is_superuser: true },
toJSON: () => '/config/', toJSON: () => '/config/',
license_info: { license_info: {
valid_key: true valid_key: true,
} },
}, },
router: { router: {
history_: { history_: {
@@ -89,15 +68,19 @@ function wrapContexts(node, context) {
const component = React.cloneElement(children, props); const component = React.cloneElement(children, props);
if (router.history) { if (router.history) {
return ( return (
<ConfigProvider value={config}> <I18nProvider i18n={i18n}>
<Router history={router.history}>{component}</Router> <ConfigProvider value={config}>
</ConfigProvider> <Router history={router.history}>{component}</Router>
</ConfigProvider>
</I18nProvider>
); );
} }
return ( return (
<ConfigProvider value={config}> <I18nProvider i18n={i18n}>
<MemoryRouter>{component}</MemoryRouter> <ConfigProvider value={config}>
</ConfigProvider> <MemoryRouter>{component}</MemoryRouter>
</ConfigProvider>
</I18nProvider>
); );
} }
} }
@@ -127,9 +110,6 @@ export function shallowWithContexts(node, options = {}) {
export function mountWithContexts(node, options = {}) { export function mountWithContexts(node, options = {}) {
const context = applyDefaultContexts(options.context); const context = applyDefaultContexts(options.context);
const childContextTypes = { const childContextTypes = {
linguiPublisher: shape({
i18n: object.isRequired, // eslint-disable-line react/forbid-prop-types
}).isRequired,
config: shape({ config: shape({
ansible_version: string, ansible_version: string,
custom_virtualenvs: arrayOf(string), custom_virtualenvs: arrayOf(string),