Add warehouse choice
This commit is contained in:
parent
e57c081cce
commit
0b5c9e35eb
5 changed files with 308 additions and 79 deletions
|
@ -3,6 +3,7 @@ import {
|
|||
ExportErrorCode,
|
||||
ExportProductsInput
|
||||
} from "@saleor/types/globalTypes";
|
||||
import { warehouseList } from "@saleor/warehouses/fixtures";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
|
@ -30,7 +31,8 @@ const props: ProductExportDialogProps = {
|
|||
all: 100,
|
||||
filter: 32
|
||||
},
|
||||
selectedProducts: 18
|
||||
selectedProducts: 18,
|
||||
warehouses: warehouseList
|
||||
};
|
||||
|
||||
storiesOf("Views / Products / Export / Export settings", module)
|
||||
|
|
|
@ -25,11 +25,13 @@ import {
|
|||
import getExportErrorMessage from "@saleor/utils/errors/export";
|
||||
import { toggle } from "@saleor/utils/lists";
|
||||
import { mapNodeToChoice } from "@saleor/utils/maps";
|
||||
import { WarehouseList_warehouses_edges_node } from "@saleor/warehouses/types/WarehouseList";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import ProductExportDialogInfo, {
|
||||
attributeNamePrefix
|
||||
attributeNamePrefix,
|
||||
warehouseNamePrefix
|
||||
} from "./ProductExportDialogInfo";
|
||||
import ProductExportDialogSettings, {
|
||||
ProductQuantity
|
||||
|
@ -64,7 +66,8 @@ function useSteps(): Array<Step<ProductExportStep>> {
|
|||
const initialForm: ExportProductsInput = {
|
||||
exportInfo: {
|
||||
attributes: [],
|
||||
fields: []
|
||||
fields: [],
|
||||
warehouses: []
|
||||
},
|
||||
fileType: FileTypesEnum.CSV,
|
||||
scope: ExportScope.ALL
|
||||
|
@ -78,6 +81,7 @@ export interface ProductExportDialogProps extends DialogProps, FetchMoreProps {
|
|||
errors: ExportErrorFragment[];
|
||||
productQuantity: ProductQuantity;
|
||||
selectedProducts: number;
|
||||
warehouses: WarehouseList_warehouses_edges_node[];
|
||||
onFetch: (query: string) => void;
|
||||
onSubmit: (data: ExportProductsInput) => void;
|
||||
}
|
||||
|
@ -91,6 +95,7 @@ const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
|
|||
onSubmit,
|
||||
open,
|
||||
selectedProducts,
|
||||
warehouses,
|
||||
...fetchMoreProps
|
||||
}) => {
|
||||
const [step, { next, prev, set: setStep }] = useWizard(
|
||||
|
@ -114,6 +119,7 @@ const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
|
|||
});
|
||||
|
||||
const attributeChoices = mapNodeToChoice(attributes);
|
||||
const warehouseChoices = mapNodeToChoice(warehouses);
|
||||
|
||||
const handleAttributeSelect: FormChange = event => {
|
||||
const id = event.target.name.substr(attributeNamePrefix.length);
|
||||
|
@ -135,6 +141,35 @@ const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const handleWarehouseSelect: FormChange = event =>
|
||||
change({
|
||||
target: {
|
||||
name: "exportInfo",
|
||||
value: {
|
||||
...data.exportInfo,
|
||||
warehouses: toggle(
|
||||
event.target.name.substr(warehouseNamePrefix.length),
|
||||
data.exportInfo.warehouses,
|
||||
(a, b) => a === b
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleToggleAllWarehouses: FormChange = () =>
|
||||
change({
|
||||
target: {
|
||||
name: "exportInfo",
|
||||
value: {
|
||||
...data.exportInfo,
|
||||
warehouses:
|
||||
data.exportInfo.warehouses.length === warehouses.length
|
||||
? []
|
||||
: warehouses.map(warehouse => warehouse.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} open={open} maxWidth="sm" fullWidth>
|
||||
<>
|
||||
|
@ -156,7 +191,10 @@ const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
|
|||
data={data}
|
||||
selectedAttributes={selectedAttributes}
|
||||
onAttrtibuteSelect={handleAttributeSelect}
|
||||
onWarehouseSelect={handleWarehouseSelect}
|
||||
onChange={change}
|
||||
warehouses={warehouseChoices}
|
||||
onSelectAllWarehouses={handleToggleAllWarehouses}
|
||||
{...fetchMoreProps}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -9,7 +9,7 @@ import Checkbox from "@saleor/components/Checkbox";
|
|||
import Chip from "@saleor/components/Chip";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
import { ChangeEvent } from "@saleor/hooks/useForm";
|
||||
import { ChangeEvent, FormChange } from "@saleor/hooks/useForm";
|
||||
import useSearchQuery from "@saleor/hooks/useSearchQuery";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import { FetchMoreProps } from "@saleor/types";
|
||||
|
@ -22,9 +22,18 @@ import React from "react";
|
|||
import { useIntl } from "react-intl";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import useProductExportFieldMessages from "./messages";
|
||||
|
||||
export const attributeNamePrefix = "attribute-";
|
||||
export const warehouseNamePrefix = "warehouse-";
|
||||
const maxChips = 4;
|
||||
|
||||
const inventoryFields = [
|
||||
ProductFieldEnum.PRODUCT_WEIGHT,
|
||||
ProductFieldEnum.VARIANT_SKU,
|
||||
ProductFieldEnum.VARIANT_WEIGHT
|
||||
];
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
accordion: {
|
||||
|
@ -45,12 +54,23 @@ const useStyles = makeStyles(
|
|||
marginBottom: theme.spacing(3),
|
||||
marginTop: theme.spacing(3)
|
||||
},
|
||||
hrWarehouses: {
|
||||
marginBottom: theme.spacing(3),
|
||||
marginTop: theme.spacing(1)
|
||||
},
|
||||
label: {
|
||||
"&&": {
|
||||
overflow: "visible"
|
||||
},
|
||||
"&:first-of-type": {
|
||||
paddingTop: 0
|
||||
},
|
||||
"&:not(:last-of-type)": {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`
|
||||
},
|
||||
justifyContent: "space-between",
|
||||
margin: theme.spacing(0),
|
||||
padding: theme.spacing(1, 0),
|
||||
width: "100%"
|
||||
},
|
||||
loadMoreContainer: {
|
||||
|
@ -62,8 +82,14 @@ const useStyles = makeStyles(
|
|||
display: "inline-block",
|
||||
marginBottom: theme.spacing()
|
||||
},
|
||||
optionLabel: {
|
||||
marginLeft: 0
|
||||
},
|
||||
quickPeekContainer: {
|
||||
marginBottom: theme.spacing(-1)
|
||||
},
|
||||
warehousesLabel: {
|
||||
marginBottom: theme.spacing(2)
|
||||
}
|
||||
}),
|
||||
{
|
||||
|
@ -80,6 +106,9 @@ const Option: React.FC<{
|
|||
|
||||
return (
|
||||
<FormControlLabel
|
||||
classes={{
|
||||
label: classes.optionLabel
|
||||
}}
|
||||
color="primary"
|
||||
control={
|
||||
<Checkbox
|
||||
|
@ -103,66 +132,7 @@ const FieldAccordion: React.FC<AccordionProps & {
|
|||
onToggleAll: (field: ProductFieldEnum[], setTo: boolean) => void;
|
||||
}> = ({ data, fields, onChange, onToggleAll, ...props }) => {
|
||||
const classes = useStyles({});
|
||||
const intl = useIntl();
|
||||
|
||||
const fieldNames: Record<ProductFieldEnum, string> = {
|
||||
[ProductFieldEnum.CATEGORY]: intl.formatMessage({
|
||||
defaultMessage: "Category",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.CHARGE_TAXES]: intl.formatMessage({
|
||||
defaultMessage: "Charge Taxes",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.COLLECTIONS]: intl.formatMessage({
|
||||
defaultMessage: "Collections",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.COST_PRICE]: intl.formatMessage({
|
||||
defaultMessage: "Cost Price",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.DESCRIPTION]: intl.formatMessage({
|
||||
defaultMessage: "Description",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.NAME]: intl.formatMessage({
|
||||
defaultMessage: "Name",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.PRODUCT_IMAGES]: intl.formatMessage({
|
||||
defaultMessage: "Product Images",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.PRODUCT_TYPE]: intl.formatMessage({
|
||||
defaultMessage: "Type",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.PRODUCT_WEIGHT]: intl.formatMessage({
|
||||
defaultMessage: "Weight",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.VARIANT_IMAGES]: intl.formatMessage({
|
||||
defaultMessage: "Variant Images",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.VARIANT_PRICE]: intl.formatMessage({
|
||||
defaultMessage: "Variant Price",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.VARIANT_SKU]: intl.formatMessage({
|
||||
defaultMessage: "SKU",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.VARIANT_WEIGHT]: intl.formatMessage({
|
||||
defaultMessage: "Variant Weight",
|
||||
description: "product field"
|
||||
}),
|
||||
[ProductFieldEnum.VISIBLE]: intl.formatMessage({
|
||||
defaultMessage: "Visibility",
|
||||
description: "product field"
|
||||
})
|
||||
};
|
||||
const getFieldLabel = useProductExportFieldMessages();
|
||||
|
||||
const selectedAll = fields.every(field =>
|
||||
data.exportInfo.fields.includes(field)
|
||||
|
@ -180,7 +150,7 @@ const FieldAccordion: React.FC<AccordionProps & {
|
|||
{selectedFields.slice(0, maxChips).map(field => (
|
||||
<Chip
|
||||
className={classes.chip}
|
||||
label={fieldNames[field]}
|
||||
label={getFieldLabel(field)}
|
||||
onClose={() =>
|
||||
onChange({
|
||||
target: {
|
||||
|
@ -225,7 +195,7 @@ const FieldAccordion: React.FC<AccordionProps & {
|
|||
onChange={onChange}
|
||||
key={field}
|
||||
>
|
||||
{fieldNames[field]}
|
||||
{getFieldLabel(field)}
|
||||
</Option>
|
||||
))}
|
||||
</Accordion>
|
||||
|
@ -234,11 +204,14 @@ const FieldAccordion: React.FC<AccordionProps & {
|
|||
|
||||
export interface ProductExportDialogInfoProps extends FetchMoreProps {
|
||||
attributes: MultiAutocompleteChoiceType[];
|
||||
warehouses: MultiAutocompleteChoiceType[];
|
||||
data: ExportProductsInput;
|
||||
selectedAttributes: MultiAutocompleteChoiceType[];
|
||||
onAttrtibuteSelect: (event: ChangeEvent) => void;
|
||||
onChange: (event: ChangeEvent) => void;
|
||||
onAttrtibuteSelect: FormChange;
|
||||
onWarehouseSelect: FormChange;
|
||||
onChange: FormChange;
|
||||
onFetch: (query: string) => void;
|
||||
onSelectAllWarehouses: FormChange;
|
||||
}
|
||||
|
||||
const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
|
||||
|
@ -247,14 +220,18 @@ const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
|
|||
hasMore,
|
||||
selectedAttributes,
|
||||
loading,
|
||||
warehouses,
|
||||
onAttrtibuteSelect,
|
||||
onWarehouseSelect,
|
||||
onChange,
|
||||
onFetch,
|
||||
onFetchMore
|
||||
onFetchMore,
|
||||
onSelectAllWarehouses
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
const intl = useIntl();
|
||||
const [query, onQueryChange] = useSearchQuery(onFetch);
|
||||
const getFieldLabel = useProductExportFieldMessages();
|
||||
|
||||
const handleFieldChange = (event: ChangeEvent) =>
|
||||
onChange({
|
||||
|
@ -289,6 +266,12 @@ const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
|
|||
}
|
||||
});
|
||||
|
||||
const selectedInventoryFields = data.exportInfo.fields.filter(field =>
|
||||
inventoryFields.includes(field)
|
||||
);
|
||||
const selectedAllInventoryFields =
|
||||
selectedInventoryFields.length === inventoryFields.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography className={classes.dialogLabel}>
|
||||
|
@ -409,22 +392,134 @@ const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
|
|||
onToggleAll={handleToggleAllFields}
|
||||
data-test="financial"
|
||||
/>
|
||||
<FieldAccordion
|
||||
<Accordion
|
||||
className={classes.accordion}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Inventory Information",
|
||||
description: "informations about product stock, header"
|
||||
})}
|
||||
data={data}
|
||||
fields={[
|
||||
ProductFieldEnum.PRODUCT_WEIGHT,
|
||||
ProductFieldEnum.VARIANT_SKU,
|
||||
ProductFieldEnum.VARIANT_WEIGHT
|
||||
]}
|
||||
onChange={handleFieldChange}
|
||||
onToggleAll={handleToggleAllFields}
|
||||
quickPeek={
|
||||
(data.exportInfo.warehouses.length > 0 ||
|
||||
selectedInventoryFields.length > 0) && (
|
||||
<div className={classes.quickPeekContainer}>
|
||||
{selectedInventoryFields.slice(0, maxChips).map(field => (
|
||||
<Chip
|
||||
className={classes.chip}
|
||||
label={getFieldLabel(field)}
|
||||
onClose={() =>
|
||||
onChange({
|
||||
target: {
|
||||
name: field,
|
||||
value: false
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{data.exportInfo.warehouses
|
||||
.slice(0, maxChips - selectedInventoryFields.length)
|
||||
.map(warehouseId => (
|
||||
<Chip
|
||||
className={classes.chip}
|
||||
label={
|
||||
warehouses.find(
|
||||
warehouse => warehouse.value === warehouseId
|
||||
).label
|
||||
}
|
||||
onClose={() =>
|
||||
onWarehouseSelect({
|
||||
target: {
|
||||
name: warehouseNamePrefix + warehouseId,
|
||||
value: undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{data.exportInfo.warehouses.length +
|
||||
selectedInventoryFields.length >
|
||||
maxChips && (
|
||||
<Typography className={classes.moreLabel} variant="caption">
|
||||
<FormattedMessage
|
||||
defaultMessage="and {number} more"
|
||||
description="there are more elements of list that are hidden"
|
||||
values={{
|
||||
number:
|
||||
data.exportInfo.warehouses.length +
|
||||
selectedInventoryFields.length -
|
||||
maxChips
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
data-test="inventory"
|
||||
/>
|
||||
>
|
||||
<div>
|
||||
<Option
|
||||
checked={selectedAllInventoryFields}
|
||||
name="all"
|
||||
onChange={() =>
|
||||
handleToggleAllFields(
|
||||
inventoryFields,
|
||||
!selectedAllInventoryFields
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Select All"
|
||||
description="selectt all options"
|
||||
/>
|
||||
</Option>
|
||||
{inventoryFields.map(field => (
|
||||
<Option
|
||||
checked={data.exportInfo.fields.includes(field)}
|
||||
name={field}
|
||||
onChange={handleFieldChange}
|
||||
key={field}
|
||||
>
|
||||
{getFieldLabel(field)}
|
||||
</Option>
|
||||
))}
|
||||
</div>
|
||||
<Hr className={classes.hrWarehouses} />
|
||||
<Typography>
|
||||
<FormattedMessage defaultMessage="Export Product Stock Quantity to CSV" />
|
||||
</Typography>
|
||||
<div>
|
||||
<Option
|
||||
checked={warehouses.every(warehouse =>
|
||||
data.exportInfo.warehouses.includes(warehouse.value)
|
||||
)}
|
||||
name="all-warehouses"
|
||||
onChange={onSelectAllWarehouses}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Export stock for all warehouses"
|
||||
description="option"
|
||||
/>
|
||||
</Option>
|
||||
</div>
|
||||
<Hr className={classes.hrWarehouses} />
|
||||
<Typography className={classes.warehousesLabel} variant="subtitle1">
|
||||
<FormattedMessage
|
||||
defaultMessage="Warehouses A to Z"
|
||||
description="list of warehouses"
|
||||
/>
|
||||
</Typography>
|
||||
{warehouses.map(warehouse => (
|
||||
<Option
|
||||
checked={data.exportInfo.warehouses.includes(warehouse.value)}
|
||||
name={warehouseNamePrefix + warehouse.value}
|
||||
onChange={onWarehouseSelect}
|
||||
key={warehouse.value}
|
||||
>
|
||||
{warehouse.label}
|
||||
</Option>
|
||||
))}
|
||||
</Accordion>
|
||||
<FieldAccordion
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "SEO Information",
|
||||
|
|
83
src/products/components/ProductExportDialog/messages.ts
Normal file
83
src/products/components/ProductExportDialog/messages.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { ProductFieldEnum } from "@saleor/types/globalTypes";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
function useProductExportFieldMessages() {
|
||||
const intl = useIntl();
|
||||
|
||||
const messages = {
|
||||
[ProductFieldEnum.CATEGORY]: {
|
||||
defaultMessage: "Category",
|
||||
description: "product field",
|
||||
id: "productExportFieldCategory"
|
||||
},
|
||||
[ProductFieldEnum.CHARGE_TAXES]: {
|
||||
defaultMessage: "Charge Taxes",
|
||||
description: "product field",
|
||||
id: "productExportFieldTaxes"
|
||||
},
|
||||
[ProductFieldEnum.COLLECTIONS]: {
|
||||
defaultMessage: "Collections",
|
||||
description: "product field",
|
||||
id: "productExportFieldCollections"
|
||||
},
|
||||
[ProductFieldEnum.COST_PRICE]: {
|
||||
defaultMessage: "Cost Price",
|
||||
description: "product field",
|
||||
id: "productExportFieldPrice"
|
||||
},
|
||||
[ProductFieldEnum.DESCRIPTION]: {
|
||||
defaultMessage: "Description",
|
||||
description: "product field",
|
||||
id: "productExportFieldDescription"
|
||||
},
|
||||
[ProductFieldEnum.NAME]: {
|
||||
defaultMessage: "Name",
|
||||
description: "product field",
|
||||
id: "productExportFieldName"
|
||||
},
|
||||
[ProductFieldEnum.PRODUCT_IMAGES]: {
|
||||
defaultMessage: "Product Images",
|
||||
description: "product field",
|
||||
id: "productExportFieldProductImages"
|
||||
},
|
||||
[ProductFieldEnum.PRODUCT_TYPE]: {
|
||||
defaultMessage: "Type",
|
||||
description: "product field",
|
||||
id: "productExportFieldType"
|
||||
},
|
||||
[ProductFieldEnum.PRODUCT_WEIGHT]: {
|
||||
defaultMessage: "Export Product Weight",
|
||||
description: "product field",
|
||||
id: "productExportFieldProductWeight"
|
||||
},
|
||||
[ProductFieldEnum.VARIANT_IMAGES]: {
|
||||
defaultMessage: "Variant Images",
|
||||
description: "product field",
|
||||
id: "productExportFieldVariantImages"
|
||||
},
|
||||
[ProductFieldEnum.VARIANT_PRICE]: {
|
||||
defaultMessage: "Variant Price",
|
||||
description: "product field",
|
||||
id: "productExportFieldVariantPrice"
|
||||
},
|
||||
[ProductFieldEnum.VARIANT_SKU]: {
|
||||
defaultMessage: "Export Variant SKU",
|
||||
description: "product field",
|
||||
id: "productExportFieldVariantSku"
|
||||
},
|
||||
[ProductFieldEnum.VARIANT_WEIGHT]: {
|
||||
defaultMessage: "Export Variant Weight",
|
||||
description: "product field",
|
||||
id: "productExportFieldVariantWeight"
|
||||
},
|
||||
[ProductFieldEnum.VISIBLE]: {
|
||||
defaultMessage: "Visibility",
|
||||
description: "product field",
|
||||
id: "productExportFieldVisibility"
|
||||
}
|
||||
};
|
||||
|
||||
return (field: ProductFieldEnum) => intl.formatMessage(messages[field]);
|
||||
}
|
||||
|
||||
export default useProductExportFieldMessages;
|
|
@ -39,6 +39,7 @@ import { ListViews } from "@saleor/types";
|
|||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||
import { getSortUrlVariables } from "@saleor/utils/sort";
|
||||
import { useWarehouseList } from "@saleor/warehouses/queries";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -124,6 +125,11 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
first: 10
|
||||
}
|
||||
});
|
||||
const warehouses = useWarehouseList({
|
||||
variables: {
|
||||
first: 100
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(
|
||||
() =>
|
||||
|
@ -551,6 +557,11 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
filter: data?.products.totalCount
|
||||
}}
|
||||
selectedProducts={listElements.length}
|
||||
warehouses={
|
||||
warehouses.data?.warehouses.edges.map(
|
||||
edge => edge.node
|
||||
) || []
|
||||
}
|
||||
onClose={closeModal}
|
||||
onSubmit={data =>
|
||||
exportProducts({
|
||||
|
|
Loading…
Reference in a new issue