Add drag-and-drop to allow variants reordering

This commit is contained in:
Dawid Tarasiuk 2020-09-17 13:31:09 +02:00
parent 45d33c3920
commit 88b8950408
13 changed files with 98 additions and 55 deletions

View file

@ -15,6 +15,7 @@ interface ImageUploadProps {
isActiveClassName?: string; isActiveClassName?: string;
iconContainerClassName?: string; iconContainerClassName?: string;
iconContainerActiveClassName?: string; iconContainerActiveClassName?: string;
hideUploadIcon?: boolean;
onImageUpload: (file: FileList) => void; onImageUpload: (file: FileList) => void;
} }
@ -66,6 +67,7 @@ export const ImageUpload: React.FC<ImageUploadProps> = props => {
iconContainerActiveClassName, iconContainerActiveClassName,
iconContainerClassName, iconContainerClassName,
isActiveClassName, isActiveClassName,
hideUploadIcon,
onImageUpload onImageUpload
} = props; } = props;
@ -82,24 +84,26 @@ export const ImageUpload: React.FC<ImageUploadProps> = props => {
[isActiveClassName]: isDragActive [isActiveClassName]: isDragActive
})} })}
> >
<div {!hideUploadIcon && (
className={classNames(iconContainerClassName, { <div
[iconContainerActiveClassName]: isDragActive className={classNames(iconContainerClassName, {
})} [iconContainerActiveClassName]: isDragActive
> })}
<input >
{...getInputProps()} <input
className={classes.fileField} {...getInputProps()}
accept="image/*" className={classes.fileField}
/> accept="image/*"
<ImageIcon className={classes.photosIcon} />
<Typography className={classes.uploadText}>
<FormattedMessage
defaultMessage="Drop here to upload"
description="image upload"
/> />
</Typography> <ImageIcon className={classes.photosIcon} />
</div> <Typography className={classes.uploadText}>
<FormattedMessage
defaultMessage="Drop here to upload"
description="image upload"
/>
</Typography>
</div>
)}
</div> </div>
{children && children({ isDragActive })} {children && children({ isDragActive })}
</> </>

View file

@ -83,9 +83,6 @@ const useStyles = makeStyles(
imageUploadActive: { imageUploadActive: {
zIndex: 1 zIndex: 1
}, },
imageUploadIcon: {
display: "none"
},
imageUploadIconActive: { imageUploadIconActive: {
display: "block" display: "block"
}, },
@ -253,7 +250,7 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
className={classes.imageUpload} className={classes.imageUpload}
isActiveClassName={classes.imageUploadActive} isActiveClassName={classes.imageUploadActive}
disableClick={true} disableClick={true}
iconContainerClassName={classes.imageUploadIcon} hideUploadIcon={true}
iconContainerActiveClassName={classes.imageUploadIconActive} iconContainerActiveClassName={classes.imageUploadIconActive}
onImageUpload={handleImageUpload} onImageUpload={handleImageUpload}
> >

View file

@ -18,7 +18,7 @@ import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections";
import { FetchMoreProps, ListActions } from "@saleor/types"; import { FetchMoreProps, ListActions, ReorderAction } from "@saleor/types";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
@ -73,6 +73,7 @@ export interface ProductUpdatePageProps extends ListActions {
fetchCollections: (query: string) => void; fetchCollections: (query: string) => void;
onVariantsAdd: () => void; onVariantsAdd: () => void;
onVariantShow: (id: string) => () => void; onVariantShow: (id: string) => () => void;
onVariantReorder: ReorderAction;
onImageDelete: (id: string) => () => void; onImageDelete: (id: string) => () => void;
onBack?(); onBack?();
onDelete(); onDelete();
@ -120,6 +121,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
onVariantAdd, onVariantAdd,
onVariantsAdd, onVariantsAdd,
onVariantShow, onVariantShow,
onVariantReorder,
isChecked, isChecked,
selected, selected,
toggle, toggle,
@ -302,6 +304,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
onRowClick={onVariantShow} onRowClick={onVariantShow}
onVariantAdd={onVariantAdd} onVariantAdd={onVariantAdd}
onVariantsAdd={onVariantsAdd} onVariantsAdd={onVariantsAdd}
onVariantReorder={onVariantReorder}
toolbar={toolbar} toolbar={toolbar}
isChecked={isChecked} isChecked={isChecked}
selected={selected} selected={selected}

View file

@ -14,6 +14,7 @@ import useFormset, {
} from "@saleor/hooks/useFormset"; } from "@saleor/hooks/useFormset";
import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data"; import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { ReorderAction } from "@saleor/types";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -56,6 +57,7 @@ interface ProductVariantCreatePageProps {
onBack: () => void; onBack: () => void;
onSubmit: (data: ProductVariantCreatePageSubmitData) => void; onSubmit: (data: ProductVariantCreatePageSubmitData) => void;
onVariantClick: (variantId: string) => void; onVariantClick: (variantId: string) => void;
onVariantReorder: ReorderAction;
} }
const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
@ -69,7 +71,8 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
weightUnit, weightUnit,
onBack, onBack,
onSubmit, onSubmit,
onVariantClick onVariantClick,
onVariantReorder
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const attributeInput = React.useMemo( const attributeInput = React.useMemo(
@ -131,6 +134,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
return onVariantClick(variantId); return onVariantClick(variantId);
} }
}} }}
onReorder={onVariantReorder}
/> />
</div> </div>
<div> <div>

View file

@ -1,13 +1,17 @@
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import {
SortableTableBody,
SortableTableRow
} from "@saleor/components/SortableTable";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import TableCellAvatar from "@saleor/components/TableCellAvatar";
import { ReorderAction } from "@saleor/types";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -26,16 +30,18 @@ const useStyles = makeStyles(
cursor: "pointer" cursor: "pointer"
}, },
tabActive: { tabActive: {
"&:before": { "& > td:first-child": {
background: theme.palette.primary.main, "&:before": {
content: '""', background: theme.palette.primary.main,
height: "100%", content: '""',
left: 0, height: "100%",
position: "absolute", left: 0,
top: 0, position: "absolute",
width: 2 top: 0,
}, width: 2
position: "relative" },
position: "relative"
}
} }
}), }),
{ name: "ProductVariantNavigation" } { name: "ProductVariantNavigation" }
@ -49,10 +55,18 @@ interface ProductVariantNavigationProps {
| ProductVariantCreateData_product_variants[]; | ProductVariantCreateData_product_variants[];
onAdd?: () => void; onAdd?: () => void;
onRowClick: (variantId: string) => void; onRowClick: (variantId: string) => void;
onReorder: ReorderAction;
} }
const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props => { const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props => {
const { current, fallbackThumbnail, variants, onAdd, onRowClick } = props; const {
current,
fallbackThumbnail,
variants,
onAdd,
onRowClick,
onReorder
} = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
@ -66,18 +80,18 @@ const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props
})} })}
/> />
<ResponsiveTable> <ResponsiveTable>
<TableBody> <SortableTableBody onSortEnd={onReorder}>
{renderCollection(variants, variant => ( {renderCollection(variants, (variant, variantIndex) => (
<TableRow <SortableTableRow
hover={!!variant} hover={!!variant}
key={variant ? variant.id : "skeleton"} key={variant ? variant.id : "skeleton"}
className={classes.link} index={variantIndex}
className={classNames(classes.link, {
[classes.tabActive]: variant && variant.id === current
})}
onClick={variant ? () => onRowClick(variant.id) : undefined} onClick={variant ? () => onRowClick(variant.id) : undefined}
> >
<TableCellAvatar <TableCellAvatar
className={classNames({
[classes.tabActive]: variant && variant.id === current
})}
thumbnail={maybe( thumbnail={maybe(
() => variant.images[0].url, () => variant.images[0].url,
fallbackThumbnail fallbackThumbnail
@ -86,11 +100,11 @@ const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
{variant ? variant.name || variant.sku : <Skeleton />} {variant ? variant.name || variant.sku : <Skeleton />}
</TableCell> </TableCell>
</TableRow> </SortableTableRow>
))} ))}
{onAdd ? ( {onAdd ? (
<TableRow> <TableRow>
<TableCell colSpan={2}> <TableCell colSpan={3}>
<Button color="primary" onClick={onAdd}> <Button color="primary" onClick={onAdd}>
<FormattedMessage <FormattedMessage
defaultMessage="Add variant" defaultMessage="Add variant"
@ -110,7 +124,7 @@ const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
</TableBody> </SortableTableBody>
</ResponsiveTable> </ResponsiveTable>
</Card> </Card>
); );

View file

@ -19,6 +19,7 @@ import {
getAttributeInputFromVariant, getAttributeInputFromVariant,
getStockInputFromVariant getStockInputFromVariant
} from "@saleor/products/utils/data"; } from "@saleor/products/utils/data";
import { ReorderAction } from "@saleor/types";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { diff } from "fast-array-diff"; import { diff } from "fast-array-diff";
@ -60,6 +61,7 @@ interface ProductVariantPageProps {
placeholderImage?: string; placeholderImage?: string;
header: string; header: string;
warehouses: WarehouseFragment[]; warehouses: WarehouseFragment[];
onVariantReorder: ReorderAction;
onAdd(); onAdd();
onBack(); onBack();
onDelete(); onDelete();
@ -82,7 +84,8 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
onDelete, onDelete,
onImageSelect, onImageSelect,
onSubmit, onSubmit,
onVariantClick onVariantClick,
onVariantReorder
}) => { }) => {
const attributeInput = React.useMemo( const attributeInput = React.useMemo(
() => getAttributeInputFromVariant(variant), () => getAttributeInputFromVariant(variant),
@ -188,6 +191,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
return onVariantClick(variantId); return onVariantClick(variantId);
} }
}} }}
onReorder={onVariantReorder}
/> />
</div> </div>
<div> <div>

View file

@ -3,9 +3,7 @@ import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import Hidden from "@material-ui/core/Hidden"; import Hidden from "@material-ui/core/Hidden";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox"; import Checkbox from "@saleor/components/Checkbox";
@ -14,13 +12,17 @@ import Money from "@saleor/components/Money";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; import ResponsiveTable from "@saleor/components/ResponsiveTable";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import {
SortableTableBody,
SortableTableRow
} from "@saleor/components/SortableTable";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import { ProductVariant_costPrice } from "@saleor/fragments/types/ProductVariant"; import { ProductVariant_costPrice } from "@saleor/fragments/types/ProductVariant";
import React from "react"; import React from "react";
import { FormattedMessage, IntlShape, useIntl } from "react-intl"; import { FormattedMessage, IntlShape, useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "../../../misc";
import { ListActions } from "../../../types"; import { ListActions, ReorderAction } from "../../../types";
import { import {
ProductDetails_product_variants, ProductDetails_product_variants,
ProductDetails_product_variants_stocks_warehouse ProductDetails_product_variants_stocks_warehouse
@ -171,12 +173,13 @@ interface ProductVariantsProps extends ListActions {
disabled: boolean; disabled: boolean;
variants: ProductDetails_product_variants[]; variants: ProductDetails_product_variants[];
fallbackPrice?: ProductVariant_costPrice; fallbackPrice?: ProductVariant_costPrice;
onVariantReorder: ReorderAction;
onRowClick: (id: string) => () => void; onRowClick: (id: string) => () => void;
onVariantAdd?(); onVariantAdd?();
onVariantsAdd?(); onVariantsAdd?();
} }
const numberOfColumns = 5; const numberOfColumns = 6;
export const ProductVariants: React.FC<ProductVariantsProps> = props => { export const ProductVariants: React.FC<ProductVariantsProps> = props => {
const { const {
@ -186,6 +189,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
onRowClick, onRowClick,
onVariantAdd, onVariantAdd,
onVariantsAdd, onVariantsAdd,
onVariantReorder,
isChecked, isChecked,
selected, selected,
toggle, toggle,
@ -266,6 +270,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
items={variants} items={variants}
toggleAll={toggleAll} toggleAll={toggleAll}
toolbar={toolbar} toolbar={toolbar}
dragRows
> >
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
<FormattedMessage <FormattedMessage
@ -291,8 +296,8 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
/> />
</TableCell> </TableCell>
</TableHead> </TableHead>
<TableBody> <SortableTableBody onSortEnd={onVariantReorder}>
{renderCollection(variants, variant => { {renderCollection(variants, (variant, variantIndex) => {
const isSelected = variant ? isChecked(variant.id) : false; const isSelected = variant ? isChecked(variant.id) : false;
const numAvailable = const numAvailable =
variant && variant.stocks variant && variant.stocks
@ -303,11 +308,12 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
: null; : null;
return ( return (
<TableRow <SortableTableRow
selected={isSelected} selected={isSelected}
hover={!!variant} hover={!!variant}
onClick={onRowClick(variant.id)} onClick={onRowClick(variant.id)}
key={variant ? variant.id : "skeleton"} key={variant ? variant.id : "skeleton"}
index={variantIndex || 0}
className={classes.link} className={classes.link}
> >
<TableCell padding="checkbox"> <TableCell padding="checkbox">
@ -354,10 +360,10 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
) )
)} )}
</TableCell> </TableCell>
</TableRow> </SortableTableRow>
); );
})} })}
</TableBody> </SortableTableBody>
</ResponsiveTable> </ResponsiveTable>
)} )}
</Card> </Card>

View file

@ -289,6 +289,7 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
onVariantsAdd={() => navigate(productVariantCreatorUrl(id))} onVariantsAdd={() => navigate(productVariantCreatorUrl(id))}
onVariantShow={variantId => () => onVariantShow={variantId => () =>
navigate(productVariantEditUrl(product.id, variantId))} navigate(productVariantEditUrl(product.id, variantId))}
onVariantReorder={() => undefined} // TODO: ...
onImageUpload={handleImageUpload} onImageUpload={handleImageUpload}
onImageEdit={handleImageEdit} onImageEdit={handleImageEdit}
onImageDelete={handleImageDelete} onImageDelete={handleImageDelete}

View file

@ -202,6 +202,7 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
onVariantClick={variantId => { onVariantClick={variantId => {
navigate(productVariantEditUrl(productId, variantId)); navigate(productVariantEditUrl(productId, variantId));
}} }}
onVariantReorder={() => undefined} // TODO: ...
/> />
<ProductVariantDeleteDialog <ProductVariantDeleteDialog
confirmButtonState={deleteVariantOpts.status} confirmButtonState={deleteVariantOpts.status}

View file

@ -126,6 +126,7 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
onBack={handleBack} onBack={handleBack}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onVariantClick={handleVariantClick} onVariantClick={handleVariantClick}
onVariantReorder={() => undefined} // TODO: ...
saveButtonBarState={variantCreateResult.status} saveButtonBarState={variantCreateResult.status}
warehouses={ warehouses={
warehouses.data?.warehouses.edges.map(edge => edge.node) || [] warehouses.data?.warehouses.edges.map(edge => edge.node) || []

View file

@ -34,6 +34,7 @@ const props: ProductUpdatePageProps = {
onImageUpload: () => undefined, onImageUpload: () => undefined,
onSubmit: () => undefined, onSubmit: () => undefined,
onVariantAdd: () => undefined, onVariantAdd: () => undefined,
onVariantReorder: () => undefined,
onVariantShow: () => undefined, onVariantShow: () => undefined,
onVariantsAdd: () => undefined, onVariantsAdd: () => undefined,
placeholderImage, placeholderImage,

View file

@ -23,6 +23,7 @@ storiesOf("Views / Products / Create product variant", module)
onBack={() => undefined} onBack={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={undefined} onVariantClick={undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />
@ -54,6 +55,7 @@ storiesOf("Views / Products / Create product variant", module)
onBack={() => undefined} onBack={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={undefined} onVariantClick={undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />
@ -69,6 +71,7 @@ storiesOf("Views / Products / Create product variant", module)
onBack={() => undefined} onBack={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={undefined} onVariantClick={undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />
@ -87,6 +90,7 @@ storiesOf("Views / Products / Create product variant", module)
onBack={() => undefined} onBack={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={undefined} onVariantClick={undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />

View file

@ -24,6 +24,7 @@ storiesOf("Views / Products / Product variant details", module)
onImageSelect={() => undefined} onImageSelect={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={() => undefined} onVariantClick={() => undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />
@ -41,6 +42,7 @@ storiesOf("Views / Products / Product variant details", module)
onImageSelect={() => undefined} onImageSelect={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={() => undefined} onVariantClick={() => undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />
@ -56,6 +58,7 @@ storiesOf("Views / Products / Product variant details", module)
onImageSelect={() => undefined} onImageSelect={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={() => undefined} onVariantClick={() => undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
errors={[ errors={[
{ {