Fix UserDateDetail translation

Add UserDateDetail to Org detail & InventoryGroupDetail
Add VariablesDetail to InventoryGroupDetail
This commit is contained in:
Keith Grant
2019-12-18 11:44:38 -08:00
parent 3d510c5064
commit 8ff0902177
8 changed files with 67 additions and 140 deletions

View File

@@ -112,7 +112,7 @@ afterEach(() => {
... ...
``` ```
**Test Attributes** - **Test Attributes** -
It should be noted that the `dataCy` prop, as well as its equivalent attribute `data-cy`, are used as flags for any UI test that wants to avoid relying on brittle CSS selectors such as `nth-of-type()`. It should be noted that the `dataCy` prop, as well as its equivalent attribute `data-cy`, are used as flags for any UI test that wants to avoid relying on brittle CSS selectors such as `nth-of-type()`.
## Handling API Errors ## Handling API Errors
@@ -296,7 +296,7 @@ The lingui library provides various React helpers for dealing with both marking
**Note:** Variables that are put inside the t-marked template tag will not be translated. If you have a variable string with text that needs translating, you must wrap it in ```i18n._(t``)``` where it is defined. **Note:** Variables that are put inside the t-marked template tag will not be translated. If you have a variable string with text that needs translating, you must wrap it in ```i18n._(t``)``` where it is defined.
**Note:** We do not use the `I18n` consumer, `i18nMark` function, or `<Trans>` component lingui gives us access to in this repo. i18nMark does not actually replace the string in the UI (leading to the potential for untranslated bugs), and the other helpers are redundant. Settling on a consistent, single pattern helps us ease the mental overhead of the need to understand the ins and outs of the lingui API. **Note:** We try to avoid the `I18n` consumer, `i18nMark` function, or `<Trans>` component lingui gives us access to in this repo. i18nMark does not actually replace the string in the UI (leading to the potential for untranslated bugs), and the other helpers are redundant. Settling on a consistent, single pattern helps us ease the mental overhead of the need to understand the ins and outs of the lingui API.
You can learn more about the ways lingui and its React helpers at [this link](https://lingui.js.org/tutorials/react-patterns.html). You can learn more about the ways lingui and its React helpers at [this link](https://lingui.js.org/tutorials/react-patterns.html).

View File

@@ -14,6 +14,10 @@ function VariablesDetail({ value, label, rows }) {
const [currentValue, setCurrentValue] = useState(value); const [currentValue, setCurrentValue] = useState(value);
const [error, setError] = useState(null); const [error, setError] = useState(null);
if (!value) {
return null;
}
return ( return (
<> <>
<DetailName <DetailName

View File

@@ -1,20 +1,25 @@
import React from 'react'; import React from 'react';
import { node, string } from 'prop-types'; import { node, string } from 'prop-types';
import { Trans } from '@lingui/macro';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { formatDateString } from '@util/dates'; import { formatDateString } from '@util/dates';
import Detail from './Detail'; import Detail from './Detail';
import { SummaryFieldUser } from '../../types'; import { SummaryFieldUser } from '../../types';
function UserDateDetail({ label, date, user }) { function UserDateDetail({ label, date, user }) {
const dateStr = formatDateString(date);
const username = user ? user.username : '';
return ( return (
<Detail <Detail
label={label} label={label}
value={ value={
<span> user ? (
{formatDateString(date)} <Trans>
{user && ' by '} {dateStr} by <Link to={`/users/${user.id}`}>{username}</Link>
{user && <Link to={`/users/${user.id}`}>{user.username}</Link>} </Trans>
</span> ) : (
dateStr
)
} }
/> />
); );

View File

@@ -68,13 +68,11 @@ function InventoryDetail({ inventory, i18n }) {
</ChipGroup> </ChipGroup>
} }
/> />
{inventory.variables && ( <VariablesDetail
<VariablesDetail label={i18n._(t`Variables`)}
label={i18n._(t`Variables`)} value={inventory.variables}
value={inventory.variables} rows={4}
rows={4} />
/>
)}
<UserDateDetail <UserDateDetail
label={i18n._(t`Created`)} label={i18n._(t`Created`)}
date={inventory.created} date={inventory.created}

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { CardHeader } from '@patternfly/react-core';
import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom'; import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom';
import { GroupsAPI } from '@api'; import { GroupsAPI } from '@api';
@@ -9,6 +8,7 @@ import CardCloseButton from '@components/CardCloseButton';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import { TabbedCardHeader } from '@components/Card';
import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit'; import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit';
import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail'; import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
@@ -97,12 +97,12 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
!history.location.pathname.endsWith('edit') !history.location.pathname.endsWith('edit')
) { ) {
cardHeader = ( cardHeader = (
<CardHeader style={{ padding: 0 }}> <TabbedCardHeader>
<RoutedTabs history={history} tabsArray={tabsArray} /> <RoutedTabs history={history} tabsArray={tabsArray} />
<CardCloseButton <CardCloseButton
linkTo={`/inventories/inventory/${inventory.id}/groups`} linkTo={`/inventories/inventory/${inventory.id}/groups`}
/> />
</CardHeader> </TabbedCardHeader>
); );
} }
return ( return (

View File

@@ -3,23 +3,16 @@ import { t } from '@lingui/macro';
import { CardBody, Button } from '@patternfly/react-core'; import { CardBody, Button } from '@patternfly/react-core';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { withRouter, Link } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { VariablesInput as CodeMirrorInput } from '@components/CodeMirrorInput'; import { VariablesDetail } from '@components/CodeMirrorInput';
import ErrorDetail from '@components/ErrorDetail'; import ErrorDetail from '@components/ErrorDetail';
import AlertModal from '@components/AlertModal'; import AlertModal from '@components/AlertModal';
import { formatDateString } from '@util/dates';
import { GroupsAPI } from '@api'; import { GroupsAPI } from '@api';
import { DetailList, Detail } from '@components/DetailList'; import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
const VariablesInput = styled(CodeMirrorInput)` // TODO: extract this into a component for use in all relevant Detail views
.pf-c-form__label {
font-weight: 600;
font-size: 16px;
}
margin: 20px 0;
`;
const ActionButtonWrapper = styled.div` const ActionButtonWrapper = styled.div`
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@@ -28,6 +21,7 @@ const ActionButtonWrapper = styled.div`
margin-left: 20px; margin-left: 20px;
} }
`; `;
function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
const { const {
summary_fields: { created_by, modified_by }, summary_fields: { created_by, modified_by },
@@ -50,52 +44,26 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
} }
}; };
let createdBy = '';
if (created) {
if (created_by && created_by.username) {
createdBy = (
<span>
{i18n._(t`${formatDateString(inventoryGroup.created)} by`)}{' '}
<Link to={`/users/${created_by.id}`}>{created_by.username}</Link>
</span>
);
} else {
createdBy = formatDateString(inventoryGroup.created);
}
}
let modifiedBy = '';
if (modified) {
if (modified_by && modified_by.username) {
modifiedBy = (
<span>
{i18n._(t`${formatDateString(inventoryGroup.modified)} by`)}{' '}
<Link to={`/users/${modified_by.id}`}>{modified_by.username}</Link>
</span>
);
} else {
modifiedBy = formatDateString(inventoryGroup.modified);
}
}
return ( return (
<CardBody css="padding-top: 20px"> <CardBody>
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail label={i18n._(t`Name`)} value={name} /> <Detail label={i18n._(t`Name`)} value={name} />
<Detail label={i18n._(t`Description`)} value={description} /> <Detail label={i18n._(t`Description`)} value={description} />
</DetailList> <VariablesDetail
<VariablesInput label={i18n._(t`Variables`)}
id="inventoryGroup-variables" value={variables}
readOnly rows={4}
value={variables} />
rows={4} <UserDateDetail
label={i18n._(t`Variables`)} label={i18n._(t`Created`)}
/> date={created}
<DetailList> user={created_by}
{createdBy && <Detail label={i18n._(t`Created`)} value={createdBy} />} />
{modifiedBy && ( <UserDateDetail
<Detail label={i18n._(t`Modified`)} value={modifiedBy} /> label={i18n._(t`Last Modified`)}
)} date={modified}
user={modified_by}
/>
</DetailList> </DetailList>
<ActionButtonWrapper> <ActionButtonWrapper>
<Button <Button

View File

@@ -4,10 +4,9 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import styled from 'styled-components'; import styled from 'styled-components';
import { Project } from '@types'; import { Project } from '@types';
import { formatDateString } from '@util/dates';
import { Config } from '@contexts/Config'; import { Config } from '@contexts/Config';
import { Button, CardBody, List, ListItem } from '@patternfly/react-core'; import { Button, CardBody, List, ListItem } from '@patternfly/react-core';
import { DetailList, Detail } from '@components/DetailList'; import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import { CredentialChip } from '@components/Chip'; import { CredentialChip } from '@components/Chip';
import { toTitleCase } from '@util/strings'; import { toTitleCase } from '@util/strings';
@@ -64,30 +63,6 @@ function ProjectDetail({ project, i18n }) {
); );
} }
let createdBy = '';
if (created) {
if (summary_fields.created_by && summary_fields.created_by.username) {
createdBy = i18n._(
t`${formatDateString(created)} by ${summary_fields.created_by.username}`
);
} else {
createdBy = formatDateString(created);
}
}
let modifiedBy = '';
if (modified) {
if (summary_fields.modified_by && summary_fields.modified_by.username) {
modifiedBy = i18n._(
t`${formatDateString(modified)} by ${
summary_fields.modified_by.username
}`
);
} else {
modifiedBy = formatDateString(modified);
}
}
return ( return (
<CardBody> <CardBody>
<DetailList gutter="sm"> <DetailList gutter="sm">
@@ -150,10 +125,16 @@ function ProjectDetail({ project, i18n }) {
)} )}
</Config> </Config>
<Detail label={i18n._(t`Playbook Directory`)} value={local_path} /> <Detail label={i18n._(t`Playbook Directory`)} value={local_path} />
{/* TODO: Link to user in users */} <UserDateDetail
<Detail label={i18n._(t`Created`)} value={createdBy} /> label={i18n._(t`Created`)}
{/* TODO: Link to user in users */} date={created}
<Detail label={i18n._(t`Last Modified`)} value={modifiedBy} /> user={summary_fields.created_by}
/>
<UserDateDetail
label={i18n._(t`Last Modified`)}
date={modified}
user={summary_fields.modified_by}
/>
</DetailList> </DetailList>
<ActionButtonWrapper> <ActionButtonWrapper>
{summary_fields.user_capabilities && {summary_fields.user_capabilities &&

View File

@@ -16,8 +16,7 @@ import ContentError from '@components/ContentError';
import LaunchButton from '@components/LaunchButton'; import LaunchButton from '@components/LaunchButton';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import { ChipGroup, Chip, CredentialChip } from '@components/Chip'; import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
import { DetailList, Detail } from '@components/DetailList'; import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import { formatDateString } from '@util/dates';
import { JobTemplatesAPI } from '@api'; import { JobTemplatesAPI } from '@api';
const ButtonGroup = styled.div` const ButtonGroup = styled.div`
@@ -112,32 +111,6 @@ class JobTemplateDetail extends Component {
const renderOptionsField = const renderOptionsField =
become_enabled || host_config_key || allow_simultaneous || use_fact_cache; become_enabled || host_config_key || allow_simultaneous || use_fact_cache;
let createdBy = '';
if (created) {
if (summary_fields.created_by && summary_fields.created_by.username) {
createdBy = i18n._(
t`${formatDateString(created)} by ${
summary_fields.created_by.username
}`
);
} else {
createdBy = formatDateString(created);
}
}
let modifiedBy = '';
if (modified) {
if (summary_fields.modified_by && summary_fields.modified_by.username) {
modifiedBy = i18n._(
t`${formatDateString(modified)} by ${
summary_fields.modified_by.username
}`
);
} else {
modifiedBy = formatDateString(modified);
}
}
const renderOptions = ( const renderOptions = (
<TextList component={TextListVariants.ul}> <TextList component={TextListVariants.ul}>
{become_enabled && ( {become_enabled && (
@@ -239,18 +212,16 @@ class JobTemplateDetail extends Component {
value={verbosityDetails[0].details} value={verbosityDetails[0].details}
/> />
<Detail label={i18n._(t`Timeout`)} value={timeout || '0'} /> <Detail label={i18n._(t`Timeout`)} value={timeout || '0'} />
{createdBy && ( <UserDateDetail
<Detail label={i18n._(t`Created`)}
label={i18n._(t`Created`)} date={created}
value={createdBy} // TODO: link to user in users user={summary_fields.created_by}
/> />
)} <UserDateDetail
{modifiedBy && ( label={i18n._(t`Last Modified`)}
<Detail date={modified}
label={i18n._(t`Last Modified`)} user={summary_fields.modified_by}
value={modifiedBy} // TODO: link to user in users />
/>
)}
<Detail <Detail
label={i18n._(t`Show Changes`)} label={i18n._(t`Show Changes`)}
value={diff_mode ? 'On' : 'Off'} value={diff_mode ? 'On' : 'Off'}