saleor-dashboard/src/components/Attributes/Attributes.tsx
Dawid Tarasiuk b07bb08ade 1863 - Support reference attribute reordering (#946)
* Support reference attribute reordering

* Update attribute handlers tests
2021-01-20 11:50:53 +01:00

411 lines
13 KiB
TypeScript

import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import makeStyles from "@material-ui/core/styles/makeStyles";
import Typography from "@material-ui/core/Typography";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import CardTitle from "@saleor/components/CardTitle";
import Hr from "@saleor/components/Hr";
import MultiAutocompleteSelectField, {
MultiAutocompleteChoiceType
} from "@saleor/components/MultiAutocompleteSelectField";
import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType
} from "@saleor/components/SingleAutocompleteSelectField";
import { AttributeValueFragment } from "@saleor/fragments/types/AttributeValueFragment";
import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment";
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { ReorderEvent } from "@saleor/types";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
import { getProductErrorMessage } from "@saleor/utils/errors";
import getPageErrorMessage from "@saleor/utils/errors/page";
import classNames from "classnames";
import React from "react";
import {
defineMessages,
FormattedMessage,
IntlShape,
useIntl
} from "react-intl";
import FileUploadField, { FileChoiceType } from "../FileUploadField";
import SortableChipsField, {
SortableChipsFieldValueType
} from "../SortableChipsField";
import BasicAttributeRow from "./BasicAttributeRow";
import ExtendedAttributeRow from "./ExtendedAttributeRow";
import { VariantAttributeScope } from "./types";
export interface AttributeInputData {
inputType: AttributeInputTypeEnum;
variantAttributeScope?: VariantAttributeScope;
isRequired: boolean;
values: AttributeValueFragment[];
selectedValues?: AttributeValueFragment[];
references?: SearchPages_search_edges_node[];
}
export type AttributeInput = FormsetAtomicData<AttributeInputData, string[]>;
export type AttributeFileInput = FormsetAtomicData<AttributeInputData, File[]>;
export interface AttributesProps {
attributes: AttributeInput[];
disabled: boolean;
loading: boolean;
errors: Array<
ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment
>;
title?: React.ReactNode;
onChange: FormsetChange<string>;
onMultiChange: FormsetChange<string>;
onFileChange: FormsetChange<File>;
onReferencesRemove: FormsetChange<string[]>;
onReferencesAddClick: (attribute: AttributeInput) => void;
onReferencesReorder: FormsetChange<ReorderEvent>;
}
const useStyles = makeStyles(
theme => ({
attributeSection: {
"&:last-of-type": {
paddingBottom: 0
},
padding: theme.spacing(2, 0)
},
attributeSectionLabel: {
alignItems: "center",
display: "flex"
},
card: {
overflow: "visible"
},
cardContent: {
"&:last-child": {
paddingBottom: theme.spacing(1)
},
paddingTop: theme.spacing(1)
},
expansionBar: {
display: "flex"
},
expansionBarButton: {
marginBottom: theme.spacing(1)
},
expansionBarButtonIcon: {
transition: theme.transitions.duration.short + "ms"
},
expansionBarLabel: {
color: theme.palette.text.disabled,
fontSize: 14
},
expansionBarLabelContainer: {
alignItems: "center",
display: "flex",
flex: 1
},
fileField: {
float: "right"
},
rotate: {
transform: "rotate(180deg)"
},
uploadFileButton: {
float: "right"
},
uploadFileContent: {
color: theme.palette.primary.main,
float: "right",
fontSize: "1rem"
}
}),
{ name: "Attributes" }
);
function getMultiChoices(
values: AttributeValueFragment[]
): MultiAutocompleteChoiceType[] {
return values.map(value => ({
label: value.name,
value: value.slug
}));
}
function getMultiDisplayValue(
attribute: AttributeInput
): MultiAutocompleteChoiceType[] {
if (!attribute.value) {
return [];
}
return attribute.value.map(attributeValue => {
const definedAttributeValue = attribute.data.values.find(
definedValue => definedValue.slug === attributeValue
);
if (!!definedAttributeValue) {
return {
label: definedAttributeValue.name,
value: definedAttributeValue.slug
};
}
return {
label: attributeValue,
value: attributeValue
};
});
}
function getReferenceDisplayValue(
attribute: AttributeInput
): SortableChipsFieldValueType[] {
if (!attribute.value) {
return [];
}
return attribute.value.map(attributeValue => {
const definedAttributeValue = attribute.data.values.find(
definedValue => definedValue.reference === attributeValue
);
// If value has been previously assigned, use it's data
if (!!definedAttributeValue) {
return {
label: definedAttributeValue.name,
value: definedAttributeValue.reference
};
}
const definedAttributeReference = attribute.data.references?.find(
reference => reference.id === attributeValue
);
// If value has not been yet assigned, use data of reference
if (!!definedAttributeReference) {
return {
label: definedAttributeReference.title,
value: definedAttributeReference.id
};
}
return {
label: attributeValue,
value: attributeValue
};
});
}
function getSingleChoices(
values: AttributeValueFragment[]
): SingleAutocompleteChoiceType[] {
return values.map(value => ({
label: value.name,
value: value.slug
}));
}
function getFileChoice(attribute: AttributeInput): FileChoiceType {
const attributeValue = attribute.value?.length > 0 && attribute.value[0];
const definedAttributeValue = attribute.data.values.find(
definedValue => definedValue.slug === attributeValue
);
if (definedAttributeValue) {
return {
file: definedAttributeValue.file,
label: definedAttributeValue.name,
value: definedAttributeValue.slug
};
}
return {
label: attributeValue,
value: attributeValue
};
}
const messages = defineMessages({
attributesNumber: {
defaultMessage: "{number} Attributes",
description: "number of attributes"
},
header: {
defaultMessage: "Attributes",
description: "attributes, section header"
},
multipleValueLable: {
defaultMessage: "Values",
description: "attribute values"
},
valueLabel: {
defaultMessage: "Value",
description: "attribute value"
}
});
function getErrorMessage(
err: ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment,
intl: IntlShape
): string {
switch (err?.__typename) {
case "ProductError":
return getProductErrorMessage(err, intl);
case "PageError":
return getPageErrorMessage(err, intl);
}
}
const Attributes: React.FC<AttributesProps> = ({
attributes,
disabled,
loading,
errors,
title,
onChange,
onMultiChange,
onFileChange,
onReferencesRemove,
onReferencesAddClick,
onReferencesReorder
}) => {
const intl = useIntl();
const classes = useStyles({});
const [expanded, setExpansionStatus] = React.useState(true);
const toggleExpansion = () => setExpansionStatus(!expanded);
return (
<Card className={classes.card}>
<CardTitle title={title || intl.formatMessage(messages.header)} />
<CardContent className={classes.cardContent}>
<div className={classes.expansionBar}>
<div className={classes.expansionBarLabelContainer}>
<Typography className={classes.expansionBarLabel} variant="caption">
<FormattedMessage
{...messages.attributesNumber}
values={{
number: attributes.length
}}
/>
</Typography>
</div>
<IconButton
className={classes.expansionBarButton}
onClick={toggleExpansion}
data-test="attributes-expand"
>
<ArrowDropDownIcon
className={classNames(classes.expansionBarButtonIcon, {
[classes.rotate]: expanded
})}
/>
</IconButton>
</div>
{expanded && attributes.length > 0 && (
<>
<Hr />
{attributes.map((attribute, attributeIndex) => {
const error = errors.find(err =>
err.attributes?.includes(attribute.id)
);
return (
<React.Fragment key={attribute.id}>
{attributeIndex > 0 && <Hr />}
{attribute.data.inputType ===
AttributeInputTypeEnum.REFERENCE ? (
<ExtendedAttributeRow
label={attribute.label}
selectLabel={intl.formatMessage({
defaultMessage: "Assign references",
description: "button label"
})}
onSelect={() => onReferencesAddClick(attribute)}
disabled={disabled}
>
<SortableChipsField
values={getReferenceDisplayValue(attribute)}
onValueDelete={value =>
onReferencesRemove(
attribute.id,
attribute.value?.filter(id => id !== value)
)
}
onValueReorder={event =>
onReferencesReorder(attribute.id, event)
}
loading={loading}
error={!!error}
helperText={getErrorMessage(error, intl)}
/>
</ExtendedAttributeRow>
) : attribute.data.inputType ===
AttributeInputTypeEnum.FILE ? (
<BasicAttributeRow label={attribute.label}>
<FileUploadField
className={classes.fileField}
disabled={disabled}
loading={loading}
file={getFileChoice(attribute)}
onFileUpload={file => onFileChange(attribute.id, file)}
onFileDelete={() =>
onFileChange(attribute.id, undefined)
}
error={!!error}
helperText={getErrorMessage(error, intl)}
inputProps={{
name: `attribute:${attribute.label}`
}}
/>
</BasicAttributeRow>
) : attribute.data.inputType ===
AttributeInputTypeEnum.DROPDOWN ? (
<BasicAttributeRow label={attribute.label}>
<SingleAutocompleteSelectField
choices={getSingleChoices(attribute.data.values)}
disabled={disabled}
displayValue={
attribute.data.values.find(
value => value.slug === attribute.value[0]
)?.name ||
attribute.value[0] ||
""
}
emptyOption={!attribute.data.isRequired}
error={!!error}
helperText={getErrorMessage(error, intl)}
name={`attribute:${attribute.label}`}
label={intl.formatMessage(messages.valueLabel)}
value={attribute.value[0]}
onChange={event =>
onChange(attribute.id, event.target.value)
}
allowCustomValues={!attribute.data.isRequired}
/>
</BasicAttributeRow>
) : (
<BasicAttributeRow label={attribute.label}>
<MultiAutocompleteSelectField
choices={getMultiChoices(attribute.data.values)}
displayValues={getMultiDisplayValue(attribute)}
disabled={disabled}
error={!!error}
helperText={getErrorMessage(error, intl)}
label={intl.formatMessage(messages.multipleValueLable)}
name={`attribute:${attribute.label}`}
value={attribute.value}
onChange={event =>
onMultiChange(attribute.id, event.target.value)
}
allowCustomValues={!attribute.data.isRequired}
/>
</BasicAttributeRow>
)}
</React.Fragment>
);
})}
</>
)}
</CardContent>
</Card>
);
};
Attributes.displayName = "Attributes";
export default Attributes;