mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Removed injectI18n from dashboard listing * Added helpers for I18nProvider * Remove intl from DashboardCloneModal * Remove intl from options.tsx * Remove intl from DashboardSaveModal
This commit is contained in:
parent
b4d200c172
commit
65850c7c6e
8 changed files with 159 additions and 162 deletions
|
@ -19,14 +19,10 @@
|
|||
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
EuiLink,
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { TableListView } from './../../table_list_view';
|
||||
|
||||
|
@ -37,8 +33,7 @@ export const EMPTY_FILTER = '';
|
|||
// and not supporting server-side paging.
|
||||
// This component does not try to tackle these problems (yet) and is just feature matching the legacy component
|
||||
// TODO support server side sorting/paging once title and description are sortable on the server.
|
||||
class DashboardListingUi extends React.Component {
|
||||
|
||||
export class DashboardListing extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
@ -54,21 +49,15 @@ class DashboardListingUi extends React.Component {
|
|||
listingLimit={this.props.listingLimit}
|
||||
initialFilter={this.props.initialFilter}
|
||||
noItemsFragment={this.getNoItemsMessage()}
|
||||
entityName={
|
||||
i18n.translate('kbn.dashboard.listing.table.entityName', {
|
||||
defaultMessage: 'dashboard'
|
||||
})
|
||||
}
|
||||
entityNamePlural={
|
||||
i18n.translate('kbn.dashboard.listing.table.entityNamePlural', {
|
||||
defaultMessage: 'dashboards'
|
||||
})
|
||||
}
|
||||
tableListTitle={
|
||||
i18n.translate('kbn.dashboard.listing.dashboardsTitle', {
|
||||
defaultMessage: 'Dashboards'
|
||||
})
|
||||
}
|
||||
entityName={i18n.translate('kbn.dashboard.listing.table.entityName', {
|
||||
defaultMessage: 'dashboard',
|
||||
})}
|
||||
entityNamePlural={i18n.translate('kbn.dashboard.listing.table.entityNamePlural', {
|
||||
defaultMessage: 'dashboards',
|
||||
})}
|
||||
tableListTitle={i18n.translate('kbn.dashboard.listing.dashboardsTitle', {
|
||||
defaultMessage: 'Dashboards',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -146,7 +135,6 @@ class DashboardListingUi extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
getTableColumns() {
|
||||
|
@ -154,7 +142,7 @@ class DashboardListingUi extends React.Component {
|
|||
{
|
||||
field: 'title',
|
||||
name: i18n.translate('kbn.dashboard.listing.table.titleColumnName', {
|
||||
defaultMessage: 'Title'
|
||||
defaultMessage: 'Title',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (field, record) => (
|
||||
|
@ -164,22 +152,22 @@ class DashboardListingUi extends React.Component {
|
|||
>
|
||||
{field}
|
||||
</EuiLink>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
name: i18n.translate('kbn.dashboard.listing.table.descriptionColumnName', {
|
||||
defaultMessage: 'Description'
|
||||
defaultMessage: 'Description',
|
||||
}),
|
||||
dataType: 'string',
|
||||
sortable: true,
|
||||
}
|
||||
},
|
||||
];
|
||||
return tableColumns;
|
||||
}
|
||||
}
|
||||
|
||||
DashboardListingUi.propTypes = {
|
||||
DashboardListing.propTypes = {
|
||||
createItem: PropTypes.func.isRequired,
|
||||
findItems: PropTypes.func.isRequired,
|
||||
deleteItems: PropTypes.func.isRequired,
|
||||
|
@ -190,8 +178,6 @@ DashboardListingUi.propTypes = {
|
|||
initialFilter: PropTypes.string,
|
||||
};
|
||||
|
||||
DashboardListingUi.defaultProps = {
|
||||
DashboardListing.defaultProps = {
|
||||
initialFilter: EMPTY_FILTER,
|
||||
};
|
||||
|
||||
export const DashboardListing = injectI18n(DashboardListingUi);
|
||||
|
|
|
@ -17,63 +17,54 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
jest.mock('ui/notify',
|
||||
jest.mock(
|
||||
'ui/notify',
|
||||
() => ({
|
||||
toastNotifications: {
|
||||
addWarning: () => {},
|
||||
}
|
||||
}), { virtual: true });
|
||||
},
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
jest.mock('lodash',
|
||||
jest.mock(
|
||||
'lodash',
|
||||
() => ({
|
||||
...require.requireActual('lodash'),
|
||||
// mock debounce to fire immediately with no internal timer
|
||||
debounce: function (func) {
|
||||
debounce: func => {
|
||||
function debounced(...args) {
|
||||
return func.apply(this, args);
|
||||
}
|
||||
return debounced;
|
||||
}
|
||||
}), { virtual: true });
|
||||
},
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
import React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import {
|
||||
DashboardListing,
|
||||
} from './dashboard_listing';
|
||||
import { DashboardListing } from './dashboard_listing';
|
||||
|
||||
const find = (num) => {
|
||||
const find = num => {
|
||||
const hits = [];
|
||||
for (let i = 0; i < num; i++) {
|
||||
hits.push({
|
||||
id: `dashboard${i}`,
|
||||
title: `dashboard${i} title`,
|
||||
description: `dashboard${i} desc`
|
||||
description: `dashboard${i} desc`,
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
total: num,
|
||||
hits: hits
|
||||
hits: hits,
|
||||
});
|
||||
};
|
||||
|
||||
test('renders empty page in before initial fetch to avoid flickering', () => {
|
||||
const component = shallowWithIntl(<DashboardListing.WrappedComponent
|
||||
findItems={find.bind(null, 2)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
editItem={() => {}}
|
||||
getViewUrl={() => {}}
|
||||
listingLimit={1000}
|
||||
hideWriteControls={false}
|
||||
/>);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('after fetch', () => {
|
||||
test('initialFilter', async () => {
|
||||
const component = shallowWithIntl(<DashboardListing.WrappedComponent
|
||||
const component = shallow(
|
||||
<DashboardListing
|
||||
findItems={find.bind(null, 2)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
|
@ -81,8 +72,25 @@ describe('after fetch', () => {
|
|||
getViewUrl={() => {}}
|
||||
listingLimit={1000}
|
||||
hideWriteControls={false}
|
||||
initialFilter="my dashboard"
|
||||
/>);
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('after fetch', () => {
|
||||
test('initialFilter', async () => {
|
||||
const component = shallow(
|
||||
<DashboardListing
|
||||
findItems={find.bind(null, 2)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
editItem={() => {}}
|
||||
getViewUrl={() => {}}
|
||||
listingLimit={1000}
|
||||
hideWriteControls={false}
|
||||
initialFilter="my dashboard"
|
||||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
@ -93,15 +101,17 @@ describe('after fetch', () => {
|
|||
});
|
||||
|
||||
test('renders table rows', async () => {
|
||||
const component = shallowWithIntl(<DashboardListing.WrappedComponent
|
||||
findItems={find.bind(null, 2)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
editItem={() => {}}
|
||||
getViewUrl={() => {}}
|
||||
listingLimit={1000}
|
||||
hideWriteControls={false}
|
||||
/>);
|
||||
const component = shallow(
|
||||
<DashboardListing
|
||||
findItems={find.bind(null, 2)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
editItem={() => {}}
|
||||
getViewUrl={() => {}}
|
||||
listingLimit={1000}
|
||||
hideWriteControls={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
@ -112,15 +122,17 @@ describe('after fetch', () => {
|
|||
});
|
||||
|
||||
test('renders call to action when no dashboards exist', async () => {
|
||||
const component = shallowWithIntl(<DashboardListing.WrappedComponent
|
||||
findItems={find.bind(null, 0)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
editItem={() => {}}
|
||||
getViewUrl={() => {}}
|
||||
listingLimit={1}
|
||||
hideWriteControls={false}
|
||||
/>);
|
||||
const component = shallow(
|
||||
<DashboardListing
|
||||
findItems={find.bind(null, 0)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
editItem={() => {}}
|
||||
getViewUrl={() => {}}
|
||||
listingLimit={1}
|
||||
hideWriteControls={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
@ -131,15 +143,17 @@ describe('after fetch', () => {
|
|||
});
|
||||
|
||||
test('hideWriteControls', async () => {
|
||||
const component = shallowWithIntl(<DashboardListing.WrappedComponent
|
||||
findItems={find.bind(null, 0)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
editItem={() => {}}
|
||||
getViewUrl={() => {}}
|
||||
listingLimit={1}
|
||||
hideWriteControls={true}
|
||||
/>);
|
||||
const component = shallow(
|
||||
<DashboardListing
|
||||
findItems={find.bind(null, 0)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
editItem={() => {}}
|
||||
getViewUrl={() => {}}
|
||||
listingLimit={1}
|
||||
hideWriteControls={true}
|
||||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
@ -150,15 +164,17 @@ describe('after fetch', () => {
|
|||
});
|
||||
|
||||
test('renders warning when listingLimit is exceeded', async () => {
|
||||
const component = shallowWithIntl(<DashboardListing.WrappedComponent
|
||||
findItems={find.bind(null, 2)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
editItem={() => {}}
|
||||
getViewUrl={() => {}}
|
||||
listingLimit={1}
|
||||
hideWriteControls={false}
|
||||
/>);
|
||||
const component = shallow(
|
||||
<DashboardListing
|
||||
findItems={find.bind(null, 2)}
|
||||
deleteItems={() => {}}
|
||||
createItem={() => {}}
|
||||
editItem={() => {}}
|
||||
getViewUrl={() => {}}
|
||||
listingLimit={1}
|
||||
hideWriteControls={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
|
|
@ -19,55 +19,48 @@
|
|||
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import {
|
||||
findTestSubject,
|
||||
} from '@elastic/eui/lib/test';
|
||||
import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
||||
import {
|
||||
DashboardCloneModal,
|
||||
} from './clone_modal';
|
||||
import { DashboardCloneModal } from './clone_modal';
|
||||
|
||||
let onClone;
|
||||
let onClose;
|
||||
let component;
|
||||
|
||||
beforeEach(() => {
|
||||
onClone = sinon.spy();
|
||||
onClose = sinon.spy();
|
||||
});
|
||||
|
||||
function createComponent(creationMethod = mountWithIntl) {
|
||||
component = creationMethod(
|
||||
<DashboardCloneModal.WrappedComponent
|
||||
title="dash title"
|
||||
onClose={onClose}
|
||||
onClone={onClone}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
test('renders DashboardCloneModal', () => {
|
||||
createComponent(shallowWithIntl);
|
||||
const component = shallowWithI18nProvider(
|
||||
<DashboardCloneModal title="dash title" onClose={onClose} onClone={onClone} />
|
||||
);
|
||||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
||||
});
|
||||
|
||||
test('onClone', () => {
|
||||
createComponent();
|
||||
const component = mountWithI18nProvider(
|
||||
<DashboardCloneModal title="dash title" onClose={onClose} onClone={onClone} />
|
||||
);
|
||||
findTestSubject(component, 'cloneConfirmButton').simulate('click');
|
||||
sinon.assert.calledWith(onClone, 'dash title');
|
||||
sinon.assert.notCalled(onClose);
|
||||
});
|
||||
|
||||
test('onClose', () => {
|
||||
createComponent();
|
||||
const component = mountWithI18nProvider(
|
||||
<DashboardCloneModal title="dash title" onClose={onClose} onClone={onClone} />
|
||||
);
|
||||
findTestSubject(component, 'cloneCancelButton').simulate('click');
|
||||
sinon.assert.calledOnce(onClose);
|
||||
sinon.assert.notCalled(onClone);
|
||||
});
|
||||
|
||||
test('title', () => {
|
||||
createComponent();
|
||||
const component = mountWithI18nProvider(
|
||||
<DashboardCloneModal title="dash title" onClose={onClose} onClone={onClone} />
|
||||
);
|
||||
const event = { target: { value: 'a' } };
|
||||
component.find('input').simulate('change', event);
|
||||
findTestSubject(component, 'cloneConfirmButton').simulate('click');
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
|
@ -43,7 +44,6 @@ interface Props {
|
|||
) => Promise<void>;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -53,7 +53,7 @@ interface State {
|
|||
isLoading: boolean;
|
||||
}
|
||||
|
||||
class DashboardCloneModalUi extends React.Component<Props, State> {
|
||||
export class DashboardCloneModal extends React.Component<Props, State> {
|
||||
private isMounted = false;
|
||||
|
||||
constructor(props: Props) {
|
||||
|
@ -117,15 +117,12 @@ class DashboardCloneModalUi extends React.Component<Props, State> {
|
|||
<EuiSpacer />
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={this.props.intl.formatMessage(
|
||||
{
|
||||
id: 'kbn.dashboard.topNav.cloneModal.dashboardExistsTitle',
|
||||
defaultMessage: 'A dashboard with the title {newDashboardName} already exists.',
|
||||
},
|
||||
{
|
||||
title={i18n.translate('kbn.dashboard.topNav.cloneModal.dashboardExistsTitle', {
|
||||
defaultMessage: 'A dashboard with the title {newDashboardName} already exists.',
|
||||
values: {
|
||||
newDashboardName: `'${this.state.newDashboardName}'`,
|
||||
}
|
||||
)}
|
||||
},
|
||||
})}
|
||||
color="warning"
|
||||
data-test-subj="titleDupicateWarnMsg"
|
||||
>
|
||||
|
@ -215,5 +212,3 @@ class DashboardCloneModalUi extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DashboardCloneModal = injectI18n(DashboardCloneModalUi);
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { injectI18n, InjectedIntl } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||
|
||||
|
@ -27,7 +27,6 @@ interface Props {
|
|||
onUseMarginsChange: (useMargins: boolean) => void;
|
||||
hidePanelTitles: boolean;
|
||||
onHidePanelTitlesChange: (hideTitles: boolean) => void;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -35,7 +34,7 @@ interface State {
|
|||
hidePanelTitles: boolean;
|
||||
}
|
||||
|
||||
class OptionsMenuUi extends Component<Props, State> {
|
||||
export class OptionsMenu extends Component<Props, State> {
|
||||
state = {
|
||||
useMargins: this.props.useMargins,
|
||||
hidePanelTitles: this.props.hidePanelTitles,
|
||||
|
@ -62,10 +61,12 @@ class OptionsMenuUi extends Component<Props, State> {
|
|||
<EuiForm data-test-subj="dashboardOptionsMenu">
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'kbn.dashboard.topNav.options.useMarginsBetweenPanelsSwitchLabel',
|
||||
defaultMessage: 'Use margins between panels',
|
||||
})}
|
||||
label={i18n.translate(
|
||||
'kbn.dashboard.topNav.options.useMarginsBetweenPanelsSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Use margins between panels',
|
||||
}
|
||||
)}
|
||||
checked={this.state.useMargins}
|
||||
onChange={this.handleUseMarginsChange}
|
||||
data-test-subj="dashboardMarginsCheckbox"
|
||||
|
@ -74,8 +75,7 @@ class OptionsMenuUi extends Component<Props, State> {
|
|||
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'kbn.dashboard.topNav.options.hideAllPanelTitlesSwitchLabel',
|
||||
label={i18n.translate('kbn.dashboard.topNav.options.hideAllPanelTitlesSwitchLabel', {
|
||||
defaultMessage: 'Show panel titles',
|
||||
})}
|
||||
checked={!this.state.hidePanelTitles}
|
||||
|
@ -87,5 +87,3 @@ class OptionsMenuUi extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const OptionsMenu = injectI18n(OptionsMenuUi);
|
||||
|
|
|
@ -18,20 +18,20 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import {
|
||||
DashboardSaveModal,
|
||||
} from './save_modal';
|
||||
import { DashboardSaveModal } from './save_modal';
|
||||
|
||||
test('renders DashboardSaveModal', () => {
|
||||
const component = shallowWithIntl(<DashboardSaveModal.WrappedComponent
|
||||
onSave={() => {}}
|
||||
onClose={() => {}}
|
||||
title="dash title"
|
||||
description="dash description"
|
||||
timeRestore={true}
|
||||
showCopyOnSave={true}
|
||||
/>);
|
||||
const component = shallowWithI18nProvider(
|
||||
<DashboardSaveModal
|
||||
onSave={() => {}}
|
||||
onClose={() => {}}
|
||||
title="dash title"
|
||||
description="dash description"
|
||||
timeRestore={true}
|
||||
showCopyOnSave={true}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
|
||||
import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui';
|
||||
|
@ -46,7 +46,6 @@ interface Props {
|
|||
description: string;
|
||||
timeRestore: boolean;
|
||||
showCopyOnSave: boolean;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -54,7 +53,7 @@ interface State {
|
|||
timeRestore: boolean;
|
||||
}
|
||||
|
||||
class DashboardSaveModalUi extends React.Component<Props, State> {
|
||||
export class DashboardSaveModal extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
description: this.props.description,
|
||||
timeRestore: this.props.timeRestore,
|
||||
|
@ -152,5 +151,3 @@ class DashboardSaveModalUi extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DashboardSaveModal = injectI18n(DashboardSaveModalUi);
|
||||
|
|
|
@ -128,3 +128,15 @@ export function renderWithIntl<T>(
|
|||
}
|
||||
|
||||
export const nextTick = () => new Promise(res => process.nextTick(res));
|
||||
|
||||
export function shallowWithI18nProvider<T>(child: ReactElement<T>) {
|
||||
const wrapped = shallow(<I18nProvider>{child}</I18nProvider>);
|
||||
const name = typeof child.type === 'string' ? child.type : child.type.name;
|
||||
return wrapped.find(name).dive();
|
||||
}
|
||||
|
||||
export function mountWithI18nProvider<T>(child: ReactElement<T>) {
|
||||
const wrapped = mount(<I18nProvider>{child}</I18nProvider>);
|
||||
const name = typeof child.type === 'string' ? child.type : child.type.name;
|
||||
return wrapped.find(name);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue