1862 - Add references field to attributes section (#923)
* Add references field to attributes section * Update messagees and test shapshots * Remove unused style in sortable chips component
This commit is contained in:
parent
55a330040e
commit
ee05b090b8
19 changed files with 1468 additions and 176 deletions
|
@ -1543,6 +1543,10 @@
|
||||||
"src_dot_components_dot_AttributeUnassignDialog_dot_2037985699": {
|
"src_dot_components_dot_AttributeUnassignDialog_dot_2037985699": {
|
||||||
"string": "Are you sure you want to unassign {attributeName} from {itemTypeName}?"
|
"string": "Are you sure you want to unassign {attributeName} from {itemTypeName}?"
|
||||||
},
|
},
|
||||||
|
"src_dot_components_dot_Attributes_dot_3824528779": {
|
||||||
|
"context": "button label",
|
||||||
|
"string": "Assign references"
|
||||||
|
},
|
||||||
"src_dot_components_dot_Attributes_dot_attributesNumber": {
|
"src_dot_components_dot_Attributes_dot_attributesNumber": {
|
||||||
"context": "number of attributes",
|
"context": "number of attributes",
|
||||||
"string": "{number} Attributes"
|
"string": "{number} Attributes"
|
||||||
|
|
|
@ -393,6 +393,7 @@ type Attribute implements Node & ObjectWithMetadata {
|
||||||
privateMetadata: [MetadataItem]!
|
privateMetadata: [MetadataItem]!
|
||||||
metadata: [MetadataItem]!
|
metadata: [MetadataItem]!
|
||||||
inputType: AttributeInputTypeEnum
|
inputType: AttributeInputTypeEnum
|
||||||
|
entityType: AttributeEntityTypeEnum
|
||||||
name: String
|
name: String
|
||||||
slug: String
|
slug: String
|
||||||
type: AttributeTypeEnum
|
type: AttributeTypeEnum
|
||||||
|
@ -431,6 +432,7 @@ type AttributeCreate {
|
||||||
|
|
||||||
input AttributeCreateInput {
|
input AttributeCreateInput {
|
||||||
inputType: AttributeInputTypeEnum
|
inputType: AttributeInputTypeEnum
|
||||||
|
entityType: AttributeEntityTypeEnum
|
||||||
name: String!
|
name: String!
|
||||||
slug: String
|
slug: String
|
||||||
type: AttributeTypeEnum!
|
type: AttributeTypeEnum!
|
||||||
|
@ -450,6 +452,10 @@ type AttributeDelete {
|
||||||
attribute: Attribute
|
attribute: Attribute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AttributeEntityTypeEnum {
|
||||||
|
PAGE
|
||||||
|
}
|
||||||
|
|
||||||
type AttributeError {
|
type AttributeError {
|
||||||
field: String
|
field: String
|
||||||
message: String
|
message: String
|
||||||
|
@ -490,6 +496,7 @@ enum AttributeInputTypeEnum {
|
||||||
DROPDOWN
|
DROPDOWN
|
||||||
MULTISELECT
|
MULTISELECT
|
||||||
FILE
|
FILE
|
||||||
|
REFERENCE
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttributeReorderValues {
|
type AttributeReorderValues {
|
||||||
|
@ -566,6 +573,7 @@ type AttributeValue implements Node {
|
||||||
type: AttributeValueType @deprecated(reason: "Use the `inputType` field to determine the type of attribute's value. This field will be removed after 2020-07-31.")
|
type: AttributeValueType @deprecated(reason: "Use the `inputType` field to determine the type of attribute's value. This field will be removed after 2020-07-31.")
|
||||||
translation(languageCode: LanguageCodeEnum!): AttributeValueTranslation
|
translation(languageCode: LanguageCodeEnum!): AttributeValueTranslation
|
||||||
inputType: AttributeInputTypeEnum
|
inputType: AttributeInputTypeEnum
|
||||||
|
reference: ID
|
||||||
file: File
|
file: File
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -598,6 +606,7 @@ input AttributeValueInput {
|
||||||
values: [String]
|
values: [String]
|
||||||
file: String
|
file: String
|
||||||
contentType: String
|
contentType: String
|
||||||
|
references: [ID!]
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttributeValueTranslatableContent implements Node {
|
type AttributeValueTranslatableContent implements Node {
|
||||||
|
@ -643,6 +652,7 @@ type BulkProductError {
|
||||||
message: String
|
message: String
|
||||||
code: ProductErrorCode!
|
code: ProductErrorCode!
|
||||||
attributes: [ID!]
|
attributes: [ID!]
|
||||||
|
values: [ID!]
|
||||||
index: Int
|
index: Int
|
||||||
warehouses: [ID!]
|
warehouses: [ID!]
|
||||||
channels: [ID!]
|
channels: [ID!]
|
||||||
|
@ -653,6 +663,7 @@ type BulkStockError {
|
||||||
message: String
|
message: String
|
||||||
code: ProductErrorCode!
|
code: ProductErrorCode!
|
||||||
attributes: [ID!]
|
attributes: [ID!]
|
||||||
|
values: [ID!]
|
||||||
index: Int
|
index: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1082,6 +1093,7 @@ type CollectionChannelListingError {
|
||||||
message: String
|
message: String
|
||||||
code: ProductErrorCode!
|
code: ProductErrorCode!
|
||||||
attributes: [ID!]
|
attributes: [ID!]
|
||||||
|
values: [ID!]
|
||||||
channels: [ID!]
|
channels: [ID!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2596,6 +2608,7 @@ type Mutation {
|
||||||
productTypeBulkDelete(ids: [ID]!): ProductTypeBulkDelete
|
productTypeBulkDelete(ids: [ID]!): ProductTypeBulkDelete
|
||||||
productTypeUpdate(id: ID!, input: ProductTypeInput!): ProductTypeUpdate
|
productTypeUpdate(id: ID!, input: ProductTypeInput!): ProductTypeUpdate
|
||||||
productTypeReorderAttributes(moves: [ReorderInput]!, productTypeId: ID!, type: ProductAttributeType!): ProductTypeReorderAttributes
|
productTypeReorderAttributes(moves: [ReorderInput]!, productTypeId: ID!, type: ProductAttributeType!): ProductTypeReorderAttributes
|
||||||
|
productReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, productId: ID!): ProductReorderAttributeValues
|
||||||
digitalContentCreate(input: DigitalContentUploadInput!, variantId: ID!): DigitalContentCreate
|
digitalContentCreate(input: DigitalContentUploadInput!, variantId: ID!): DigitalContentCreate
|
||||||
digitalContentDelete(variantId: ID!): DigitalContentDelete
|
digitalContentDelete(variantId: ID!): DigitalContentDelete
|
||||||
digitalContentUpdate(input: DigitalContentInput!, variantId: ID!): DigitalContentUpdate
|
digitalContentUpdate(input: DigitalContentInput!, variantId: ID!): DigitalContentUpdate
|
||||||
|
@ -2611,6 +2624,7 @@ type Mutation {
|
||||||
productVariantSetDefault(productId: ID!, variantId: ID!): ProductVariantSetDefault
|
productVariantSetDefault(productId: ID!, variantId: ID!): ProductVariantSetDefault
|
||||||
productVariantTranslate(id: ID!, input: NameTranslationInput!, languageCode: LanguageCodeEnum!): ProductVariantTranslate
|
productVariantTranslate(id: ID!, input: NameTranslationInput!, languageCode: LanguageCodeEnum!): ProductVariantTranslate
|
||||||
productVariantChannelListingUpdate(id: ID!, input: [ProductVariantChannelListingAddInput!]!): ProductVariantChannelListingUpdate
|
productVariantChannelListingUpdate(id: ID!, input: [ProductVariantChannelListingAddInput!]!): ProductVariantChannelListingUpdate
|
||||||
|
productVariantReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, variantId: ID!): ProductVariantReorderAttributeValues
|
||||||
variantImageAssign(imageId: ID!, variantId: ID!): VariantImageAssign
|
variantImageAssign(imageId: ID!, variantId: ID!): VariantImageAssign
|
||||||
variantImageUnassign(imageId: ID!, variantId: ID!): VariantImageUnassign
|
variantImageUnassign(imageId: ID!, variantId: ID!): VariantImageUnassign
|
||||||
paymentCapture(amount: PositiveDecimal, paymentId: ID!): PaymentCapture
|
paymentCapture(amount: PositiveDecimal, paymentId: ID!): PaymentCapture
|
||||||
|
@ -2630,6 +2644,7 @@ type Mutation {
|
||||||
pageAttributeAssign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeAssign
|
pageAttributeAssign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeAssign
|
||||||
pageAttributeUnassign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeUnassign
|
pageAttributeUnassign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeUnassign
|
||||||
pageTypeReorderAttributes(moves: [ReorderInput!]!, pageTypeId: ID!): PageTypeReorderAttributes
|
pageTypeReorderAttributes(moves: [ReorderInput!]!, pageTypeId: ID!): PageTypeReorderAttributes
|
||||||
|
pageReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, pageId: ID!): PageReorderAttributeValues
|
||||||
draftOrderComplete(id: ID!): DraftOrderComplete
|
draftOrderComplete(id: ID!): DraftOrderComplete
|
||||||
draftOrderCreate(input: DraftOrderCreateInput!): DraftOrderCreate
|
draftOrderCreate(input: DraftOrderCreateInput!): DraftOrderCreate
|
||||||
draftOrderDelete(id: ID!): DraftOrderDelete
|
draftOrderDelete(id: ID!): DraftOrderDelete
|
||||||
|
@ -3290,6 +3305,7 @@ type PageError {
|
||||||
message: String
|
message: String
|
||||||
code: PageErrorCode!
|
code: PageErrorCode!
|
||||||
attributes: [ID!]
|
attributes: [ID!]
|
||||||
|
values: [ID!]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PageErrorCode {
|
enum PageErrorCode {
|
||||||
|
@ -3324,6 +3340,12 @@ input PageInput {
|
||||||
seo: SeoInput
|
seo: SeoInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PageReorderAttributeValues {
|
||||||
|
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||||
|
page: Page
|
||||||
|
pageErrors: [PageError!]!
|
||||||
|
}
|
||||||
|
|
||||||
enum PageSortField {
|
enum PageSortField {
|
||||||
TITLE
|
TITLE
|
||||||
SLUG
|
SLUG
|
||||||
|
@ -3821,6 +3843,7 @@ type ProductChannelListingError {
|
||||||
message: String
|
message: String
|
||||||
code: ProductErrorCode!
|
code: ProductErrorCode!
|
||||||
attributes: [ID!]
|
attributes: [ID!]
|
||||||
|
values: [ID!]
|
||||||
channels: [ID!]
|
channels: [ID!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3879,6 +3902,7 @@ type ProductError {
|
||||||
message: String
|
message: String
|
||||||
code: ProductErrorCode!
|
code: ProductErrorCode!
|
||||||
attributes: [ID!]
|
attributes: [ID!]
|
||||||
|
values: [ID!]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ProductErrorCode {
|
enum ProductErrorCode {
|
||||||
|
@ -4026,6 +4050,12 @@ type ProductPricingInfo {
|
||||||
priceRangeLocalCurrency: TaxedMoneyRange
|
priceRangeLocalCurrency: TaxedMoneyRange
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductReorderAttributeValues {
|
||||||
|
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||||
|
product: Product
|
||||||
|
productErrors: [ProductError!]!
|
||||||
|
}
|
||||||
|
|
||||||
input ProductStockFilterInput {
|
input ProductStockFilterInput {
|
||||||
warehouseIds: [ID!]
|
warehouseIds: [ID!]
|
||||||
quantity: IntRangeInput
|
quantity: IntRangeInput
|
||||||
|
@ -4172,8 +4202,12 @@ type ProductVariant implements Node & ObjectWithMetadata {
|
||||||
weight: Weight
|
weight: Weight
|
||||||
privateMetadata: [MetadataItem]!
|
privateMetadata: [MetadataItem]!
|
||||||
metadata: [MetadataItem]!
|
metadata: [MetadataItem]!
|
||||||
|
quantity: Int! @deprecated(reason: "Use the stock field instead. This field will be removed after 2020-07-31.")
|
||||||
|
quantityAllocated: Int @deprecated(reason: "Use the stock field instead. This field will be removed after 2020-07-31.")
|
||||||
|
stockQuantity: Int! @deprecated(reason: "Use the quantityAvailable field instead. This field will be removed after 2020-07-31.")
|
||||||
channelListings: [ProductVariantChannelListing!]
|
channelListings: [ProductVariantChannelListing!]
|
||||||
pricing: VariantPricingInfo
|
pricing: VariantPricingInfo
|
||||||
|
isAvailable: Boolean @deprecated(reason: "Use the stock field instead. This field will be removed after 2020-07-31.")
|
||||||
attributes(variantSelection: VariantAttributeScope): [SelectedAttribute!]!
|
attributes(variantSelection: VariantAttributeScope): [SelectedAttribute!]!
|
||||||
costPrice: Money
|
costPrice: Money
|
||||||
margin: Int
|
margin: Int
|
||||||
|
@ -4278,6 +4312,12 @@ type ProductVariantReorder {
|
||||||
productErrors: [ProductError!]!
|
productErrors: [ProductError!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductVariantReorderAttributeValues {
|
||||||
|
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||||
|
productVariant: ProductVariant
|
||||||
|
productErrors: [ProductError!]!
|
||||||
|
}
|
||||||
|
|
||||||
type ProductVariantSetDefault {
|
type ProductVariantSetDefault {
|
||||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||||
product: Product
|
product: Product
|
||||||
|
|
|
@ -12,7 +12,10 @@ const props: AttributesProps = {
|
||||||
loading: false,
|
loading: false,
|
||||||
onChange: () => undefined,
|
onChange: () => undefined,
|
||||||
onFileChange: () => undefined,
|
onFileChange: () => undefined,
|
||||||
onMultiChange: () => undefined
|
onMultiChange: () => undefined,
|
||||||
|
onReferencesChange: () => undefined,
|
||||||
|
onReferencesChangeClick: () => undefined,
|
||||||
|
onReferencesReorder: () => undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
storiesOf("Attributes / Attributes", module)
|
storiesOf("Attributes / Attributes", module)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
|
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
|
||||||
import CardTitle from "@saleor/components/CardTitle";
|
import CardTitle from "@saleor/components/CardTitle";
|
||||||
import Grid from "@saleor/components/Grid";
|
|
||||||
import Hr from "@saleor/components/Hr";
|
import Hr from "@saleor/components/Hr";
|
||||||
import MultiAutocompleteSelectField, {
|
import MultiAutocompleteSelectField, {
|
||||||
MultiAutocompleteChoiceType
|
MultiAutocompleteChoiceType
|
||||||
|
@ -17,6 +16,7 @@ import { AttributeValueFragment } from "@saleor/fragments/types/AttributeValueFr
|
||||||
import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment";
|
import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment";
|
||||||
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
|
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
|
||||||
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
|
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
|
||||||
|
import { ReorderAction } from "@saleor/types";
|
||||||
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
|
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
|
||||||
import { getProductErrorMessage } from "@saleor/utils/errors";
|
import { getProductErrorMessage } from "@saleor/utils/errors";
|
||||||
import getPageErrorMessage from "@saleor/utils/errors/page";
|
import getPageErrorMessage from "@saleor/utils/errors/page";
|
||||||
|
@ -30,6 +30,9 @@ import {
|
||||||
} from "react-intl";
|
} from "react-intl";
|
||||||
|
|
||||||
import FileUploadField, { FileChoiceType } from "../FileUploadField";
|
import FileUploadField, { FileChoiceType } from "../FileUploadField";
|
||||||
|
import SortableChipsField from "../SortableChipsField";
|
||||||
|
import BasicAttributeRow from "./BasicAttributeRow";
|
||||||
|
import ExtendedAttributeRow from "./ExtendedAttributeRow";
|
||||||
import { VariantAttributeScope } from "./types";
|
import { VariantAttributeScope } from "./types";
|
||||||
|
|
||||||
export interface AttributeInputData {
|
export interface AttributeInputData {
|
||||||
|
@ -51,7 +54,10 @@ export interface AttributesProps {
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
onChange: FormsetChange;
|
onChange: FormsetChange;
|
||||||
onMultiChange: FormsetChange;
|
onMultiChange: FormsetChange;
|
||||||
onFileChange?: FormsetChange; // TODO: temporairy optional, should be changed to required, after all pages implement it
|
onFileChange: FormsetChange;
|
||||||
|
onReferencesChange?: FormsetChange; // TODO: temporairy optional, should be changed to required, after all pages implement it
|
||||||
|
onReferencesChangeClick?: () => void; // TODO: temporairy optional, should be changed to required, after all pages implement it
|
||||||
|
onReferencesReorder?: ReorderAction; // TODO: temporairy optional, should be changed to required, after all pages implement it
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
|
@ -210,7 +216,10 @@ const Attributes: React.FC<AttributesProps> = ({
|
||||||
title,
|
title,
|
||||||
onChange,
|
onChange,
|
||||||
onMultiChange,
|
onMultiChange,
|
||||||
onFileChange
|
onFileChange,
|
||||||
|
onReferencesChange,
|
||||||
|
onReferencesChangeClick,
|
||||||
|
onReferencesReorder
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const classes = useStyles({});
|
const classes = useStyles({});
|
||||||
|
@ -255,24 +264,38 @@ const Attributes: React.FC<AttributesProps> = ({
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={attribute.id}>
|
<React.Fragment key={attribute.id}>
|
||||||
{attributeIndex > 0 && <Hr />}
|
{attributeIndex > 0 && <Hr />}
|
||||||
<Grid className={classes.attributeSection} variant="uniform">
|
|
||||||
<div
|
|
||||||
className={classes.attributeSectionLabel}
|
|
||||||
data-test="attribute-label"
|
|
||||||
>
|
|
||||||
<Typography>{attribute.label}</Typography>
|
|
||||||
</div>
|
|
||||||
<div data-test="attribute-value">
|
|
||||||
{attribute.data.inputType ===
|
{attribute.data.inputType ===
|
||||||
|
AttributeInputTypeEnum.REFERENCE ? (
|
||||||
|
<ExtendedAttributeRow
|
||||||
|
label={attribute.label}
|
||||||
|
selectLabel={intl.formatMessage({
|
||||||
|
defaultMessage: "Assign references",
|
||||||
|
description: "button label"
|
||||||
|
})}
|
||||||
|
onSelect={onReferencesChangeClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SortableChipsField
|
||||||
|
values={getMultiDisplayValue(attribute)}
|
||||||
|
onValueDelete={value =>
|
||||||
|
onReferencesChange(
|
||||||
|
attribute.id,
|
||||||
|
attribute.value?.filter(id => id !== value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onValueReorder={onReferencesReorder}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</ExtendedAttributeRow>
|
||||||
|
) : attribute.data.inputType ===
|
||||||
AttributeInputTypeEnum.FILE ? (
|
AttributeInputTypeEnum.FILE ? (
|
||||||
|
<BasicAttributeRow label={attribute.label}>
|
||||||
<FileUploadField
|
<FileUploadField
|
||||||
className={classes.fileField}
|
className={classes.fileField}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
file={getFileChoice(attribute)}
|
file={getFileChoice(attribute)}
|
||||||
onFileUpload={file =>
|
onFileUpload={file => onFileChange(attribute.id, file)}
|
||||||
onFileChange(attribute.id, file)
|
|
||||||
}
|
|
||||||
onFileDelete={() =>
|
onFileDelete={() =>
|
||||||
onFileChange(attribute.id, undefined)
|
onFileChange(attribute.id, undefined)
|
||||||
}
|
}
|
||||||
|
@ -282,8 +305,10 @@ const Attributes: React.FC<AttributesProps> = ({
|
||||||
name: `attribute:${attribute.label}`
|
name: `attribute:${attribute.label}`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</BasicAttributeRow>
|
||||||
) : attribute.data.inputType ===
|
) : attribute.data.inputType ===
|
||||||
AttributeInputTypeEnum.DROPDOWN ? (
|
AttributeInputTypeEnum.DROPDOWN ? (
|
||||||
|
<BasicAttributeRow label={attribute.label}>
|
||||||
<SingleAutocompleteSelectField
|
<SingleAutocompleteSelectField
|
||||||
choices={getSingleChoices(attribute.data.values)}
|
choices={getSingleChoices(attribute.data.values)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -305,16 +330,16 @@ const Attributes: React.FC<AttributesProps> = ({
|
||||||
}
|
}
|
||||||
allowCustomValues={!attribute.data.isRequired}
|
allowCustomValues={!attribute.data.isRequired}
|
||||||
/>
|
/>
|
||||||
|
</BasicAttributeRow>
|
||||||
) : (
|
) : (
|
||||||
|
<BasicAttributeRow label={attribute.label}>
|
||||||
<MultiAutocompleteSelectField
|
<MultiAutocompleteSelectField
|
||||||
choices={getMultiChoices(attribute.data.values)}
|
choices={getMultiChoices(attribute.data.values)}
|
||||||
displayValues={getMultiDisplayValue(attribute)}
|
displayValues={getMultiDisplayValue(attribute)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
error={!!error}
|
error={!!error}
|
||||||
helperText={getErrorMessage(error, intl)}
|
helperText={getErrorMessage(error, intl)}
|
||||||
label={intl.formatMessage(
|
label={intl.formatMessage(messages.multipleValueLable)}
|
||||||
messages.multipleValueLable
|
|
||||||
)}
|
|
||||||
name={`attribute:${attribute.label}`}
|
name={`attribute:${attribute.label}`}
|
||||||
value={attribute.value}
|
value={attribute.value}
|
||||||
onChange={event =>
|
onChange={event =>
|
||||||
|
@ -322,9 +347,8 @@ const Attributes: React.FC<AttributesProps> = ({
|
||||||
}
|
}
|
||||||
allowCustomValues={!attribute.data.isRequired}
|
allowCustomValues={!attribute.data.isRequired}
|
||||||
/>
|
/>
|
||||||
|
</BasicAttributeRow>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</Grid>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
44
src/components/Attributes/BasicAttributeRow.tsx
Normal file
44
src/components/Attributes/BasicAttributeRow.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import Grid from "@saleor/components/Grid";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
attributeSection: {
|
||||||
|
"&:last-of-type": {
|
||||||
|
paddingBottom: 0
|
||||||
|
},
|
||||||
|
padding: theme.spacing(2, 0)
|
||||||
|
},
|
||||||
|
attributeSectionLabel: {
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ name: "BasicAttributeRow" }
|
||||||
|
);
|
||||||
|
|
||||||
|
interface BasicAttributeRowProps {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BasicAttributeRow: React.FC<BasicAttributeRowProps> = props => {
|
||||||
|
const { label, children } = props;
|
||||||
|
const classes = useStyles(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid className={classes.attributeSection} variant="uniform">
|
||||||
|
<div
|
||||||
|
className={classes.attributeSectionLabel}
|
||||||
|
data-test="attribute-label"
|
||||||
|
>
|
||||||
|
<Typography>{label}</Typography>
|
||||||
|
</div>
|
||||||
|
<div data-test="attribute-value">{children}</div>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BasicAttributeRow.displayName = "BasicAttributeRow";
|
||||||
|
export default BasicAttributeRow;
|
65
src/components/Attributes/ExtendedAttributeRow.tsx
Normal file
65
src/components/Attributes/ExtendedAttributeRow.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import Button from "@material-ui/core/Button";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import Grid from "@saleor/components/Grid";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
attributeSection: {
|
||||||
|
"&:last-of-type": {
|
||||||
|
paddingBottom: 0
|
||||||
|
},
|
||||||
|
padding: theme.spacing(2, 0)
|
||||||
|
},
|
||||||
|
attributeSectionButton: {
|
||||||
|
float: "right"
|
||||||
|
},
|
||||||
|
attributeSectionLabel: {
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ name: "ExtendedAttributeRow" }
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ExtendedAttributeRowProps {
|
||||||
|
label: string;
|
||||||
|
selectLabel: string;
|
||||||
|
disabled: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtendedAttributeRow: React.FC<ExtendedAttributeRowProps> = props => {
|
||||||
|
const { label, selectLabel, disabled, onSelect, children } = props;
|
||||||
|
const classes = useStyles(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid className={classes.attributeSection} variant="uniform">
|
||||||
|
<div
|
||||||
|
className={classes.attributeSectionLabel}
|
||||||
|
data-test="attribute-label"
|
||||||
|
>
|
||||||
|
<Typography>{label}</Typography>
|
||||||
|
</div>
|
||||||
|
<div data-test="attribute-selector">
|
||||||
|
<Button
|
||||||
|
className={classes.attributeSectionButton}
|
||||||
|
disabled={disabled}
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
data-test="button-attribute-selector"
|
||||||
|
onClick={onSelect}
|
||||||
|
>
|
||||||
|
{selectLabel}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
<div data-test="attribute-value">{children}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ExtendedAttributeRow.displayName = "ExtendedAttributeRow";
|
||||||
|
export default ExtendedAttributeRow;
|
|
@ -79,15 +79,49 @@ const FILE_ATTRIBUTE: AttributeInput = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
id: "ifudbgidfsb",
|
id: "fguygygugyu",
|
||||||
label: "File Attribute",
|
label: "File Attribute",
|
||||||
value: []
|
value: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const REFERENCE_ATTRIBUTE: AttributeInput = {
|
||||||
|
data: {
|
||||||
|
inputType: AttributeInputTypeEnum.REFERENCE,
|
||||||
|
isRequired: true,
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
__typename: "AttributeValue",
|
||||||
|
file: null,
|
||||||
|
id: "vbnhgcvjhbvhj",
|
||||||
|
name: "References First Value",
|
||||||
|
slug: "references-first-value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: "AttributeValue",
|
||||||
|
file: null,
|
||||||
|
id: "gucngdfdfvdvd",
|
||||||
|
name: "References Second Value",
|
||||||
|
slug: "references-second-value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: "AttributeValue",
|
||||||
|
file: null,
|
||||||
|
id: "dfdfdsfdsfdse",
|
||||||
|
name: "References Third Value",
|
||||||
|
slug: "references-third-value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
id: "kclsmcdsmcs",
|
||||||
|
label: "References Attribute",
|
||||||
|
value: []
|
||||||
|
};
|
||||||
|
|
||||||
export const ATTRIBUTES: AttributeInput[] = [
|
export const ATTRIBUTES: AttributeInput[] = [
|
||||||
DROPDOWN_ATTRIBUTE,
|
DROPDOWN_ATTRIBUTE,
|
||||||
MULTISELECT_ATTRIBUTE,
|
MULTISELECT_ATTRIBUTE,
|
||||||
FILE_ATTRIBUTE
|
FILE_ATTRIBUTE,
|
||||||
|
REFERENCE_ATTRIBUTE
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ATTRIBUTES_SELECTED: AttributeInput[] = [
|
export const ATTRIBUTES_SELECTED: AttributeInput[] = [
|
||||||
|
@ -105,5 +139,13 @@ export const ATTRIBUTES_SELECTED: AttributeInput[] = [
|
||||||
{
|
{
|
||||||
...FILE_ATTRIBUTE,
|
...FILE_ATTRIBUTE,
|
||||||
value: [FILE_ATTRIBUTE.data.values[0].slug]
|
value: [FILE_ATTRIBUTE.data.values[0].slug]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...REFERENCE_ATTRIBUTE,
|
||||||
|
value: [
|
||||||
|
REFERENCE_ATTRIBUTE.data.values[0].slug,
|
||||||
|
REFERENCE_ATTRIBUTE.data.values[1].slug,
|
||||||
|
REFERENCE_ATTRIBUTE.data.values[2].slug
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import Chip, { ChipProps } from "@saleor/components/Chip";
|
import Chip, { ChipProps } from "@saleor/components/Chip";
|
||||||
|
import CardDecorator from "@saleor/storybook/CardDecorator";
|
||||||
|
import Decorator from "@saleor/storybook/Decorator";
|
||||||
import { storiesOf } from "@storybook/react";
|
import { storiesOf } from "@storybook/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import CardDecorator from "../../CardDecorator";
|
|
||||||
import Decorator from "../../Decorator";
|
|
||||||
|
|
||||||
const props: ChipProps = {
|
const props: ChipProps = {
|
||||||
label: "Lorem Ipsum"
|
label: "Lorem Ipsum"
|
||||||
};
|
};
|
29
src/components/SortableChip/SortableChip.stories.tsx
Normal file
29
src/components/SortableChip/SortableChip.stories.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import SortableChip, {
|
||||||
|
SortableChipProps
|
||||||
|
} from "@saleor/components/SortableChip";
|
||||||
|
import CardDecorator from "@saleor/storybook/CardDecorator";
|
||||||
|
import Decorator from "@saleor/storybook/Decorator";
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import React from "react";
|
||||||
|
import { SortableContainer } from "react-sortable-hoc";
|
||||||
|
|
||||||
|
const Container = SortableContainer(props => props.children);
|
||||||
|
|
||||||
|
const props: SortableChipProps = {
|
||||||
|
index: 0,
|
||||||
|
label: "Lorem Ipsum"
|
||||||
|
};
|
||||||
|
|
||||||
|
storiesOf("Generics / Sortable chip", module)
|
||||||
|
.addDecorator(CardDecorator)
|
||||||
|
.addDecorator(Decorator)
|
||||||
|
.add("default", () => (
|
||||||
|
<Container>
|
||||||
|
<SortableChip {...props} />
|
||||||
|
</Container>
|
||||||
|
))
|
||||||
|
.add("with x", () => (
|
||||||
|
<Container>
|
||||||
|
<SortableChip {...props} onClose={() => undefined} />
|
||||||
|
</Container>
|
||||||
|
));
|
68
src/components/SortableChip/SortableChip.tsx
Normal file
68
src/components/SortableChip/SortableChip.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import CloseIcon from "@material-ui/icons/Close";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import React from "react";
|
||||||
|
import { SortableElement, SortableElementProps } from "react-sortable-hoc";
|
||||||
|
|
||||||
|
import SortableHandle from "./SortableHandle";
|
||||||
|
|
||||||
|
export interface SortableChipProps extends SortableElementProps {
|
||||||
|
className?: string;
|
||||||
|
label: React.ReactNode;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
closeIcon: {
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: 16,
|
||||||
|
marginLeft: theme.spacing(),
|
||||||
|
verticalAlign: "middle"
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex"
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: 18,
|
||||||
|
display: "inline-block",
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
padding: "6px 12px"
|
||||||
|
},
|
||||||
|
sortableHandle: {
|
||||||
|
marginRight: theme.spacing(1)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ name: "SortableChip" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const SortableChip = SortableElement<SortableChipProps>(props => {
|
||||||
|
const { className, label, onClose } = props;
|
||||||
|
|
||||||
|
const classes = useStyles(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(classes.root, className)}>
|
||||||
|
<div className={classes.content}>
|
||||||
|
<SortableHandle
|
||||||
|
className={classes.sortableHandle}
|
||||||
|
data-test="button-drag-handle"
|
||||||
|
/>
|
||||||
|
<Typography data-test="chip-label">{label}</Typography>
|
||||||
|
{onClose && (
|
||||||
|
<CloseIcon
|
||||||
|
className={classes.closeIcon}
|
||||||
|
onClick={onClose}
|
||||||
|
data-test="button-close"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SortableChip.displayName = "SortableChip";
|
||||||
|
export default SortableChip;
|
29
src/components/SortableChip/SortableHandle.tsx
Normal file
29
src/components/SortableChip/SortableHandle.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import Draggable from "@saleor/icons/Draggable";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import React from "react";
|
||||||
|
import { SortableHandle as SortableHandleHoc } from "react-sortable-hoc";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
{
|
||||||
|
drag: {
|
||||||
|
cursor: "grab"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ name: "SortableHandle" }
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SortableHandle {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableHandle = SortableHandleHoc(props => {
|
||||||
|
const { className, ...restProps } = props;
|
||||||
|
const classes = useStyles(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable className={classNames(classes.drag, className)} {...restProps} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SortableHandle;
|
2
src/components/SortableChip/index.ts
Normal file
2
src/components/SortableChip/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default } from "./SortableChip";
|
||||||
|
export * from "./SortableChip";
|
|
@ -0,0 +1,27 @@
|
||||||
|
import CardDecorator from "@saleor/storybook/CardDecorator";
|
||||||
|
import Decorator from "@saleor/storybook/Decorator";
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import SortableChipsField, {
|
||||||
|
SortableChipsFieldProps
|
||||||
|
} from "./SortableChipsField";
|
||||||
|
|
||||||
|
const props: SortableChipsFieldProps = {
|
||||||
|
onValueDelete: () => undefined,
|
||||||
|
onValueReorder: () => undefined,
|
||||||
|
values: [
|
||||||
|
{ label: "Item 1", value: "item-1" },
|
||||||
|
{ label: "Item 2", value: "item-2" },
|
||||||
|
{ label: "Item 3", value: "item-3" },
|
||||||
|
{ label: "Item 4", value: "item-4" },
|
||||||
|
{ label: "Item 5", value: "item-5" },
|
||||||
|
{ label: "Item 6", value: "item-6" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
storiesOf("Generics / Sortable chips field", module)
|
||||||
|
.addDecorator(CardDecorator)
|
||||||
|
.addDecorator(Decorator)
|
||||||
|
.add("default", () => <SortableChipsField {...props} />)
|
||||||
|
.add("loading", () => <SortableChipsField {...props} loading={true} />);
|
66
src/components/SortableChipsField/SortableChipsField.tsx
Normal file
66
src/components/SortableChipsField/SortableChipsField.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import { ReorderAction } from "@saleor/types";
|
||||||
|
import React from "react";
|
||||||
|
import { SortableContainerProps } from "react-sortable-hoc";
|
||||||
|
|
||||||
|
import Skeleton from "../Skeleton";
|
||||||
|
import DraggableChip from "../SortableChip";
|
||||||
|
import SortableContainer from "./SortableContainer";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
chip: {
|
||||||
|
background: "#fff",
|
||||||
|
color: theme.palette.primary.dark,
|
||||||
|
marginBottom: theme.spacing(1)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "SortableChipsField"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SortableChipsFieldValueType {
|
||||||
|
label: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortableChipsFieldProps extends SortableContainerProps {
|
||||||
|
loading?: boolean;
|
||||||
|
values: SortableChipsFieldValueType[];
|
||||||
|
onValueDelete: (id: string) => void;
|
||||||
|
onValueReorder: ReorderAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableChipsField: React.FC<SortableChipsFieldProps> = props => {
|
||||||
|
const { loading, values, onValueDelete, onValueReorder } = props;
|
||||||
|
const classes = useStyles(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableContainer
|
||||||
|
axis="xy"
|
||||||
|
lockAxis="xy"
|
||||||
|
useDragHandle
|
||||||
|
onSortEnd={onValueReorder}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{loading ? (
|
||||||
|
<Skeleton />
|
||||||
|
) : (
|
||||||
|
values.map((value, valueIndex) => (
|
||||||
|
<DraggableChip
|
||||||
|
className={classes.chip}
|
||||||
|
key={valueIndex}
|
||||||
|
index={valueIndex}
|
||||||
|
label={value.label}
|
||||||
|
onClose={() => onValueDelete(value.value)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SortableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SortableChipsField.displayName = "SortableChipsField";
|
||||||
|
export default SortableChipsField;
|
5
src/components/SortableChipsField/SortableContainer.tsx
Normal file
5
src/components/SortableChipsField/SortableContainer.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { SortableContainer as SortableContainerHoc } from "react-sortable-hoc";
|
||||||
|
|
||||||
|
const SortableContainer = SortableContainerHoc(({ children }) => children);
|
||||||
|
|
||||||
|
export default SortableContainer;
|
2
src/components/SortableChipsField/index.ts
Normal file
2
src/components/SortableChipsField/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default } from "./SortableChipsField";
|
||||||
|
export * from "./SortableChipsField";
|
File diff suppressed because it is too large
Load diff
|
@ -16,7 +16,6 @@ function loadStories() {
|
||||||
require("./stories/components/AutocompleteSelectMenu");
|
require("./stories/components/AutocompleteSelectMenu");
|
||||||
require("./stories/components/CardMenu");
|
require("./stories/components/CardMenu");
|
||||||
require("./stories/components/Checkbox");
|
require("./stories/components/Checkbox");
|
||||||
require("./stories/components/Chip");
|
|
||||||
require("./stories/components/ColumnPicker");
|
require("./stories/components/ColumnPicker");
|
||||||
require("./stories/components/Date");
|
require("./stories/components/Date");
|
||||||
require("./stories/components/DateTime");
|
require("./stories/components/DateTime");
|
||||||
|
|
|
@ -70,6 +70,10 @@ export enum AppTypeEnum {
|
||||||
THIRDPARTY = "THIRDPARTY",
|
THIRDPARTY = "THIRDPARTY",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AttributeEntityTypeEnum {
|
||||||
|
PAGE = "PAGE",
|
||||||
|
}
|
||||||
|
|
||||||
export enum AttributeErrorCode {
|
export enum AttributeErrorCode {
|
||||||
ALREADY_EXISTS = "ALREADY_EXISTS",
|
ALREADY_EXISTS = "ALREADY_EXISTS",
|
||||||
GRAPHQL_ERROR = "GRAPHQL_ERROR",
|
GRAPHQL_ERROR = "GRAPHQL_ERROR",
|
||||||
|
@ -83,6 +87,7 @@ export enum AttributeInputTypeEnum {
|
||||||
DROPDOWN = "DROPDOWN",
|
DROPDOWN = "DROPDOWN",
|
||||||
FILE = "FILE",
|
FILE = "FILE",
|
||||||
MULTISELECT = "MULTISELECT",
|
MULTISELECT = "MULTISELECT",
|
||||||
|
REFERENCE = "REFERENCE",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AttributeSortField {
|
export enum AttributeSortField {
|
||||||
|
@ -1022,6 +1027,7 @@ export interface AppTokenInput {
|
||||||
|
|
||||||
export interface AttributeCreateInput {
|
export interface AttributeCreateInput {
|
||||||
inputType?: AttributeInputTypeEnum | null;
|
inputType?: AttributeInputTypeEnum | null;
|
||||||
|
entityType?: AttributeEntityTypeEnum | null;
|
||||||
name: string;
|
name: string;
|
||||||
slug?: string | null;
|
slug?: string | null;
|
||||||
type: AttributeTypeEnum;
|
type: AttributeTypeEnum;
|
||||||
|
@ -1084,6 +1090,7 @@ export interface AttributeValueInput {
|
||||||
values?: (string | null)[] | null;
|
values?: (string | null)[] | null;
|
||||||
file?: string | null;
|
file?: string | null;
|
||||||
contentType?: string | null;
|
contentType?: string | null;
|
||||||
|
references?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BulkAttributeValueInput {
|
export interface BulkAttributeValueInput {
|
||||||
|
|
Loading…
Reference in a new issue