Fix strict null check in attributes (#3005)

This commit is contained in:
poulch 2023-01-26 09:38:19 +01:00 committed by GitHub
parent 01172aed95
commit 993595caac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 207 additions and 178 deletions

View file

@ -197,7 +197,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = props => {
label={intl.formatMessage(messages.entityType)}
name="entityType"
onChange={onChange}
value={data.entityType}
value={data.entityType ?? undefined}
/>
)}
</div>

View file

@ -40,7 +40,7 @@ const useStyles = makeStyles(
);
interface UnitData {
unit?: MeasurementUnitsEnum;
unit: MeasurementUnitsEnum | null | undefined;
system?: UnitSystem;
type?: UnitType;
}
@ -145,7 +145,10 @@ export const NumericUnits: React.FC<NumericUnitsProps> = ({
label={formatMessage(M.messages.unitSystem)}
choices={systemChoices}
onChange={({ target }: React.ChangeEvent<HTMLSelectElement>) =>
setUnitData({ system: target.value as UnitSystem })
setUnitData(data => ({
...data,
system: target.value as UnitSystem,
}))
}
value={system}
disabled={disabled}
@ -156,8 +159,8 @@ export const NumericUnits: React.FC<NumericUnitsProps> = ({
label={formatMessage(M.messages.unitOf)}
choices={typeChoices}
onChange={({ target }: React.ChangeEvent<HTMLSelectElement>) =>
setUnitData(({ system }) => ({
system,
setUnitData(data => ({
...data,
type: target.value as UnitType,
}))
}
@ -168,7 +171,7 @@ export const NumericUnits: React.FC<NumericUnitsProps> = ({
{...(type && !unit && errorProps)}
testId="unit"
label={formatMessage(M.messages.unit)}
choices={type ? unitChoices[system][type] : []}
choices={type && system ? unitChoices[system][type] : []}
onChange={({ target }: React.ChangeEvent<HTMLSelectElement>) =>
setUnitData(data => ({
...data,
@ -177,7 +180,7 @@ export const NumericUnits: React.FC<NumericUnitsProps> = ({
}
disabled={!type || disabled}
value={
type && unitMapping[system][type].includes(unit)
type && system && unit && unitMapping[system][type].includes(unit)
? unit
: undefined
}

View file

@ -11,7 +11,7 @@ import { TablePaginationWithContext } from "@dashboard/components/TablePaginatio
import TableRowLink from "@dashboard/components/TableRowLink";
import { AttributeFragment } from "@dashboard/graphql";
import { translateBoolean } from "@dashboard/intl";
import { maybe, renderCollection } from "@dashboard/misc";
import { renderCollection } from "@dashboard/misc";
import { ListActions, ListProps, SortPage } from "@dashboard/types";
import { getArrowDirection } from "@dashboard/utils/sort";
import { TableBody, TableCell, TableFooter } from "@material-ui/core";
@ -95,7 +95,7 @@ const AttributeList: React.FC<AttributeListProps> = ({
className={classes.colSlug}
direction={
sort.sort === AttributeListUrlSortField.slug
? getArrowDirection(sort.asc)
? getArrowDirection(!!sort.asc)
: undefined
}
arrowPosition="right"
@ -107,7 +107,7 @@ const AttributeList: React.FC<AttributeListProps> = ({
className={classes.colName}
direction={
sort.sort === AttributeListUrlSortField.name
? getArrowDirection(sort.asc)
? getArrowDirection(!!sort.asc)
: undefined
}
onClick={() => onSort(AttributeListUrlSortField.name)}
@ -122,7 +122,7 @@ const AttributeList: React.FC<AttributeListProps> = ({
className={classes.colVisible}
direction={
sort.sort === AttributeListUrlSortField.visible
? getArrowDirection(sort.asc)
? getArrowDirection(!!sort.asc)
: undefined
}
textAlign="center"
@ -138,7 +138,7 @@ const AttributeList: React.FC<AttributeListProps> = ({
className={classes.colSearchable}
direction={
sort.sort === AttributeListUrlSortField.searchable
? getArrowDirection(sort.asc)
? getArrowDirection(!!sort.asc)
: undefined
}
textAlign="center"
@ -154,7 +154,7 @@ const AttributeList: React.FC<AttributeListProps> = ({
className={classes.colFaceted}
direction={
sort.sort === AttributeListUrlSortField.useInFacetedSearch
? getArrowDirection(sort.asc)
? getArrowDirection(!!sort.asc)
: undefined
}
textAlign="center"
@ -185,14 +185,14 @@ const AttributeList: React.FC<AttributeListProps> = ({
key={attribute ? attribute.id : "skeleton"}
href={attribute && attributeUrl(attribute.id)}
className={classes.link}
data-test-id={"id-" + maybe(() => attribute.id)}
data-test-id={`id-${attribute?.id}`}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(attribute.id)}
onChange={() => toggle(attribute?.id ?? "")}
/>
</TableCell>
<TableCell className={classes.colSlug} data-test-id="slug">
@ -204,7 +204,7 @@ const AttributeList: React.FC<AttributeListProps> = ({
<TableCell
className={classes.colVisible}
data-test-id="visible"
data-test-visible={maybe(() => attribute.visibleInStorefront)}
data-test-visible={attribute?.visibleInStorefront}
>
{attribute ? (
translateBoolean(attribute.visibleInStorefront, intl)
@ -215,9 +215,7 @@ const AttributeList: React.FC<AttributeListProps> = ({
<TableCell
className={classes.colSearchable}
data-test-id="searchable"
data-test-searchable={maybe(
() => attribute.filterableInDashboard,
)}
data-test-searchable={attribute?.filterableInDashboard}
>
{attribute ? (
translateBoolean(attribute.filterableInDashboard, intl)
@ -228,9 +226,9 @@ const AttributeList: React.FC<AttributeListProps> = ({
<TableCell
className={classes.colFaceted}
data-test-id="use-in-faceted-search"
data-test-use-in-faceted-search={maybe(
() => attribute.filterableInStorefront,
)}
data-test-use-in-faceted-search={
attribute?.filterableInStorefront
}
>
{attribute ? (
translateBoolean(attribute.filterableInStorefront, intl)

View file

@ -22,7 +22,6 @@ import {
import { SubmitPromise } from "@dashboard/hooks/useForm";
import useNavigator from "@dashboard/hooks/useNavigator";
import { sectionNames } from "@dashboard/intl";
import { maybe } from "@dashboard/misc";
import { ListSettings, ReorderAction } from "@dashboard/types";
import { mapEdgesToItems, mapMetadataItemToInput } from "@dashboard/utils/maps";
import useMetadataChangeTrigger from "@dashboard/utils/metadata/useMetadataChangeTrigger";
@ -37,11 +36,13 @@ import AttributeProperties from "../AttributeProperties";
import AttributeValues from "../AttributeValues";
export interface AttributePageProps {
attribute: AttributeDetailsFragment | null;
attribute?: AttributeDetailsFragment | null | undefined;
disabled: boolean;
errors: AttributeErrorFragment[];
saveButtonBarState: ConfirmButtonTransitionState;
values: AttributeDetailsQuery["attribute"]["choices"];
values?:
| NonNullable<AttributeDetailsQuery["attribute"]>["choices"]
| undefined;
onDelete: () => void;
onSubmit: (data: AttributePageFormData) => SubmitPromise;
onValueAdd: () => void;
@ -64,7 +65,7 @@ export interface AttributePageFormData extends MetadataFormData {
availableInGrid: boolean;
filterableInDashboard: boolean;
inputType: AttributeInputTypeEnum;
entityType: AttributeEntityTypeEnum;
entityType: AttributeEntityTypeEnum | null;
filterableInStorefront: boolean;
name: string;
slug: string;
@ -102,59 +103,51 @@ const AttributePage: React.FC<AttributePageProps> = ({
makeChangeHandler: makeMetadataChangeHandler,
} = useMetadataChangeTrigger();
const initialForm: AttributePageFormData =
attribute === null
? {
availableInGrid: true,
entityType: null,
filterableInDashboard: true,
filterableInStorefront: true,
inputType: AttributeInputTypeEnum.DROPDOWN,
metadata: [],
name: "",
privateMetadata: [],
slug: "",
storefrontSearchPosition: "",
type: AttributeTypeEnum.PRODUCT_TYPE,
valueRequired: true,
visibleInStorefront: true,
unit: undefined,
}
: {
availableInGrid: attribute?.availableInGrid ?? true,
entityType: attribute?.entityType ?? null,
filterableInDashboard: attribute?.filterableInDashboard ?? true,
filterableInStorefront: attribute?.filterableInStorefront ?? true,
inputType: attribute?.inputType ?? AttributeInputTypeEnum.DROPDOWN,
metadata: attribute?.metadata?.map(mapMetadataItemToInput),
name: attribute?.name ?? "",
privateMetadata: attribute?.privateMetadata?.map(
mapMetadataItemToInput,
),
slug: attribute?.slug ?? "",
storefrontSearchPosition:
attribute?.storefrontSearchPosition.toString() ?? "",
type: attribute?.type || AttributeTypeEnum.PRODUCT_TYPE,
valueRequired: !!attribute?.valueRequired ?? true,
visibleInStorefront: attribute?.visibleInStorefront ?? true,
unit: attribute?.unit || null,
};
const initialForm: AttributePageFormData = !attribute
? {
availableInGrid: true,
entityType: null,
filterableInDashboard: true,
filterableInStorefront: true,
inputType: AttributeInputTypeEnum.DROPDOWN,
metadata: [],
name: "",
privateMetadata: [],
slug: "",
storefrontSearchPosition: "",
type: AttributeTypeEnum.PRODUCT_TYPE,
valueRequired: true,
visibleInStorefront: true,
unit: undefined,
}
: {
availableInGrid: attribute.availableInGrid,
entityType: attribute.entityType,
filterableInDashboard: attribute.filterableInDashboard,
filterableInStorefront: attribute.filterableInStorefront,
inputType: attribute?.inputType ?? AttributeInputTypeEnum.DROPDOWN,
metadata: attribute.metadata.map(mapMetadataItemToInput),
name: attribute?.name ?? "",
privateMetadata: attribute.privateMetadata.map(mapMetadataItemToInput),
slug: attribute?.slug ?? "",
storefrontSearchPosition: attribute.storefrontSearchPosition.toString(),
type: attribute?.type ?? AttributeTypeEnum.PRODUCT_TYPE,
valueRequired: !!attribute.valueRequired,
visibleInStorefront: attribute.visibleInStorefront,
unit: attribute?.unit ?? null,
};
const handleSubmit = (data: AttributePageFormData) => {
const metadata =
!attribute || isMetadataModified ? data.metadata : undefined;
const metadata = !attribute || isMetadataModified ? data.metadata : [];
const privateMetadata =
!attribute || isPrivateMetadataModified
? data.privateMetadata
: undefined;
const type = attribute === null ? data.type : undefined;
!attribute || isPrivateMetadataModified ? data.privateMetadata : [];
return onSubmit({
...data,
metadata,
privateMetadata,
slug: data.slug || slugify(data.name).toLowerCase(),
type,
type: data.type,
});
};
@ -185,13 +178,13 @@ const AttributePage: React.FC<AttributePageProps> = ({
</Backlink>
<PageHeader
title={
attribute === null
!attribute
? intl.formatMessage({
id: "8cUEPV",
defaultMessage: "Create New Attribute",
description: "page title",
})
: maybe(() => attribute.name)
: attribute.name
}
/>
<Grid>
@ -215,7 +208,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
<AttributeValues
inputType={data.inputType}
disabled={disabled}
values={mapEdgesToItems(values)}
values={mapEdgesToItems(values) ?? []}
onValueAdd={onValueAdd}
onValueDelete={onValueDelete}
onValueReorder={onValueReorder}
@ -248,7 +241,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
</div>
</Grid>
<Savebar
disabled={isSaveDisabled}
disabled={!!isSaveDisabled}
state={saveButtonBarState}
onCancel={() => navigate(attributeListUrl())}
onSubmit={submit}

View file

@ -41,11 +41,9 @@ const AttributeSwatchField: React.FC<AttributeSwatchFieldProps<
const handleFileUpload = async (file: File) => {
setProcessing(true);
const {
data: { fileUpload },
} = await uploadFile({ variables: { file } });
const { data } = await uploadFile({ variables: { file } });
if (fileUpload.errors?.length) {
if (data?.fileUpload?.errors?.length) {
notify({
status: "error",
title: intl.formatMessage(errorMessages.imgageUploadErrorTitle),
@ -53,8 +51,8 @@ const AttributeSwatchField: React.FC<AttributeSwatchFieldProps<
});
} else {
set({
fileUrl: fileUpload.uploadedFile.url,
contentType: fileUpload.uploadedFile.contentType,
fileUrl: data?.fileUpload?.uploadedFile?.url,
contentType: data?.fileUpload?.uploadedFile?.contentType ?? "",
value: undefined,
});
}
@ -95,7 +93,7 @@ const AttributeSwatchField: React.FC<AttributeSwatchFieldProps<
<FileUploadField
disabled={processing}
loading={processing}
file={{ label: null, value: null, file: null }}
file={{ label: "", value: "", file: undefined }}
onFileUpload={handleFileUpload}
onFileDelete={handleFileDelete}
inputProps={{

View file

@ -71,13 +71,13 @@ const useStyles = makeStyles(
{ name: "AttributeValues" },
);
const getSwatchCellStyle = (value: AttributeValueFragment) => {
const getSwatchCellStyle = (value?: AttributeValueFragment | undefined) => {
if (!value) {
return;
}
return value.file
? { backgroundImage: `url(${value.file.url})` }
: { backgroundColor: value.value };
: { backgroundColor: value.value ?? undefined };
};
const AttributeValues: React.FC<AttributeValuesProps> = ({
@ -198,7 +198,9 @@ const AttributeValues: React.FC<AttributeValuesProps> = ({
<IconButton
variant="secondary"
disabled={disabled}
onClick={stopPropagation(() => onValueDelete(value.id))}
onClick={stopPropagation(() =>
onValueDelete(value?.id ?? ""),
)}
>
<DeleteIcon />
</IconButton>

View file

@ -14,9 +14,9 @@ const messages = defineMessages({
});
export function getAttributeSlugErrorMessage(
err: AttributeErrorFragment,
err: AttributeErrorFragment | undefined,
intl: IntlShape,
): string {
): string | undefined {
switch (err?.code) {
case AttributeErrorCode.UNIQUE:
return intl.formatMessage(messages.attributeSlugUnique);
@ -26,9 +26,9 @@ export function getAttributeSlugErrorMessage(
}
export function getAttributeValueErrorMessage(
err: AttributeErrorFragment,
err: AttributeErrorFragment | undefined,
intl: IntlShape,
): string {
): string | undefined {
switch (err?.code) {
case AttributeErrorCode.ALREADY_EXISTS:
return intl.formatMessage(messages.attributeValueAlreadyExists);

View file

@ -80,8 +80,10 @@ export const attribute: AttributeDetailsQuery["attribute"] = {
visibleInStorefront: true,
};
export const attributes: Array<AttributeListQuery["attributes"]["edges"][0]["node"] &
ProductDetailsQuery["product"]["attributes"][0]["attribute"]> = [
export const attributes: Array<NonNullable<
AttributeListQuery["attributes"]
>["edges"][0]["node"] &
NonNullable<ProductDetailsQuery["product"]>["attributes"][0]["attribute"]> = [
{
__typename: "Attribute" as "Attribute",
entityType: AttributeEntityTypeEnum.PRODUCT,

View file

@ -66,7 +66,7 @@ export function filterable(
attribute: Pick<AttributeFragment, "inputType">,
): boolean {
return ATTRIBUTE_TYPES_WITH_CONFIGURABLE_FACED_NAVIGATION.includes(
attribute.inputType,
attribute.inputType!,
);
}
@ -86,9 +86,9 @@ export function attributeValueFragmentToFormData(
data: AttributeValueFragment | null,
): AttributeValueEditDialogFormData {
return {
name: data?.name,
value: data?.value,
contentType: data?.file?.contentType,
name: data?.name ?? "",
value: data?.value ?? "",
contentType: data?.file?.contentType ?? "",
fileUrl: data?.file?.url,
};
}
@ -265,7 +265,7 @@ export const mergeAttributeValues = (
) => {
const attribute = attributes.find(attribute => attribute.id === attributeId);
return attribute.value
return attribute?.value
? [...attribute.value, ...attributeValues]
: attributeValues;
};
@ -332,8 +332,8 @@ export const getAttributesOfUploadedFiles = (
const attribute = fileValuesToUpload[index];
return {
file: uploadFileResult.data.fileUpload.uploadedFile.url,
contentType: uploadFileResult.data.fileUpload.uploadedFile.contentType,
file: uploadFileResult.data?.fileUpload?.uploadedFile?.url,
contentType: uploadFileResult.data?.fileUpload?.uploadedFile?.contentType,
id: attribute.id,
values: [],
};
@ -380,7 +380,7 @@ export const getFileAttributeDisplayData = (
export const getPageReferenceAttributeDisplayData = (
attribute: AttributeInput,
referencePages: RelayToFlat<SearchPagesQuery["search"]>,
referencePages: RelayToFlat<NonNullable<SearchPagesQuery["search"]>>,
) => ({
...attribute,
data: {
@ -388,12 +388,18 @@ export const getPageReferenceAttributeDisplayData = (
references:
referencePages?.length > 0 && attribute.value?.length > 0
? mapPagesToChoices(
attribute.value.map(value => {
attribute.value.reduce<
RelayToFlat<NonNullable<SearchPagesQuery["search"]>>
>((acc, value) => {
const reference = referencePages.find(
reference => reference.id === value,
);
return { ...reference };
}),
if (reference) {
acc.push(reference);
}
return acc;
}, []),
)
: [],
},
@ -401,7 +407,7 @@ export const getPageReferenceAttributeDisplayData = (
export const getProductReferenceAttributeDisplayData = (
attribute: AttributeInput,
referenceProducts: RelayToFlat<SearchProductsQuery["search"]>,
referenceProducts: RelayToFlat<NonNullable<SearchProductsQuery["search"]>>,
) => ({
...attribute,
data: {
@ -409,12 +415,18 @@ export const getProductReferenceAttributeDisplayData = (
references:
referenceProducts?.length > 0 && attribute.value?.length > 0
? mapNodeToChoice(
attribute.value.map(value => {
attribute.value.reduce<
RelayToFlat<NonNullable<SearchProductsQuery["search"]>>
>((acc, value) => {
const reference = referenceProducts.find(
reference => reference.id === value,
);
return { ...reference };
}),
if (reference) {
acc.push(reference);
}
return acc;
}, []),
)
: [],
},
@ -422,7 +434,7 @@ export const getProductReferenceAttributeDisplayData = (
export const getProductVariantReferenceAttributeDisplayData = (
attribute: AttributeInput,
referenceProducts: RelayToFlat<SearchProductsQuery["search"]>,
referenceProducts: RelayToFlat<NonNullable<SearchProductsQuery["search"]>>,
) => ({
...attribute,
data: {
@ -430,12 +442,19 @@ export const getProductVariantReferenceAttributeDisplayData = (
references:
referenceProducts?.length > 0 && attribute.value?.length > 0
? mapNodeToChoice(
attribute.value.map(value => {
const reference = mapReferenceProductsToVariants(
referenceProducts,
).find(reference => reference.id === value);
return { ...reference };
}),
attribute.value.reduce<Array<Node & Record<"name", string>>>(
(acc, value) => {
const reference = mapReferenceProductsToVariants(
referenceProducts,
).find(reference => reference.id === value);
if (reference) {
acc.push(reference);
}
return acc;
},
[],
),
)
: [],
},
@ -443,8 +462,8 @@ export const getProductVariantReferenceAttributeDisplayData = (
export const getReferenceAttributeDisplayData = (
attribute: AttributeInput,
referencePages: RelayToFlat<SearchPagesQuery["search"]>,
referenceProducts: RelayToFlat<SearchProductsQuery["search"]>,
referencePages: RelayToFlat<NonNullable<SearchPagesQuery["search"]>>,
referenceProducts: RelayToFlat<NonNullable<SearchProductsQuery["search"]>>,
) => {
if (attribute.data.entityType === AttributeEntityTypeEnum.PAGE) {
return getPageReferenceAttributeDisplayData(attribute, referencePages);
@ -466,8 +485,8 @@ export const getReferenceAttributeDisplayData = (
export const getAttributesDisplayData = (
attributes: AttributeInput[],
attributesWithNewFileValue: FormsetData<null, File>,
referencePages: RelayToFlat<SearchPagesQuery["search"]>,
referenceProducts: RelayToFlat<SearchProductsQuery["search"]>,
referencePages: RelayToFlat<NonNullable<SearchPagesQuery["search"]>>,
referenceProducts: RelayToFlat<NonNullable<SearchProductsQuery["search"]>>,
) =>
attributes.map(attribute => {
if (attribute.data.inputType === AttributeInputTypeEnum.REFERENCE) {
@ -499,11 +518,11 @@ export const getReferenceAttributeEntityTypeFromAttribute = (
attributes?.find(attribute => attribute.id === attributeId)?.data?.entityType;
export const mapReferenceProductsToVariants = (
referenceProducts: RelayToFlat<SearchProductsQuery["search"]>,
referenceProducts: RelayToFlat<NonNullable<SearchProductsQuery["search"]>>,
) =>
referenceProducts.flatMap(product =>
product.variants.map(variant => ({
(product.variants || []).map(variant => ({
...variant,
name: product.name + " " + variant.name,
name: `${product.name} ${variant.name}`,
})),
);

View file

@ -136,7 +136,7 @@ const createAttribute = ({
value,
}: CreateAttribute): AttributeInput => ({
data: {
entityType: null,
entityType: undefined,
inputType,
isRequired: false,
// those values don't matter
@ -146,7 +146,7 @@ const createAttribute = ({
},
id: ATTR_ID,
label: "MyAttribute",
value: value !== null ? [value] : [],
value: value !== null && value !== undefined ? [value] : [],
});
const createSelectAttribute = (value: string) =>
@ -478,7 +478,7 @@ describe("Sending only changed attributes", () => {
});
describe("works with file attributes", () => {
it("removes existing image (img -> null)", () => {
const attribute = createFileAttribute(null);
const attribute = createFileAttribute("");
const prevAttribute = createNumericAttribute("bob.jpg");
const result = prepareAttributesInput({
@ -494,7 +494,7 @@ describe("Sending only changed attributes", () => {
});
it("adds new image (null -> img)", () => {
const attribute = createFileAttribute("bob.jpg");
const prevAttribute = createNumericAttribute(null);
const prevAttribute = createNumericAttribute("");
const uploadUrl = "http://some-url.com/media/file_upload/bob.jpg";
const result = prepareAttributesInput({

View file

@ -48,7 +48,7 @@ export function createAttributeMultiChangeHandler(
const newAttributeValues = toggle(
value,
attribute.value,
attribute?.value ?? [],
(a, b) => a === b,
);
@ -88,6 +88,7 @@ export function createFetchReferencesHandler(
) {
fetchReferencePages(value);
} else if (
attribute.data?.entityType &&
[
AttributeEntityTypeEnum.PRODUCT,
AttributeEntityTypeEnum.PRODUCT_VARIANT,
@ -116,6 +117,7 @@ export function createFetchMoreReferencesHandler(
if (attribute.data.entityType === AttributeEntityTypeEnum.PAGE) {
return fetchMoreReferencePages;
} else if (
attribute.data?.entityType &&
[
AttributeEntityTypeEnum.PRODUCT,
AttributeEntityTypeEnum.PRODUCT_VARIANT,
@ -145,7 +147,7 @@ export function createAttributeFileChangeHandler(
addAttributeNewFileValue({
data: null,
id: attributeId,
label: null,
label: "",
value,
});
}
@ -167,8 +169,8 @@ export function createAttributeValueReorderHandler(
);
const reorderedValues = move(
attribute.value[reorder.oldIndex],
attribute.value,
attribute?.value?.[reorder.oldIndex] ?? "",
attribute?.value ?? [],
(a, b) => a === b,
reorder.newIndex,
);
@ -194,7 +196,7 @@ function getFileInput(
}
return {
file: attribute.data.selectedValues?.[0]?.file?.url,
contentType: attribute.data.selectedValues?.[0]?.file.contentType,
contentType: attribute.data.selectedValues?.[0]?.file?.contentType,
id: attribute.id,
};
}
@ -318,7 +320,9 @@ export const handleDeleteMultipleAttributeValues = async (
attributes: Array<
| PageSelectedAttributeFragment
| ProductFragment["attributes"][0]
| ProductVariantDetailsQuery["productVariant"]["nonSelectionAttributes"][0]
| NonNullable<
ProductVariantDetailsQuery["productVariant"]
>["nonSelectionAttributes"][0]
>,
deleteAttributeValue: (
variables: AttributeValueDeleteMutationVariables,

View file

@ -86,7 +86,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
const [attributeCreate, attributeCreateOpts] = useAttributeCreateMutation({
onCompleted: data => {
if (data.attributeCreate.errors.length === 0) {
if (data?.attributeCreate?.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
@ -94,7 +94,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
defaultMessage: "Successfully created attribute",
}),
});
navigate(attributeUrl(data.attributeCreate.attribute.id));
navigate(attributeUrl(data?.attributeCreate?.attribute?.id ?? ""));
}
},
});
@ -113,8 +113,10 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
React.useEffect(() => setValueErrors([]), [params.action]);
const handleValueDelete = () => {
const newValues = remove(values[id], values, areValuesEqual);
setValues(newValues);
if (id) {
const newValues = remove(values[id], values, areValuesEqual);
setValues(newValues);
}
closeModal();
};
@ -122,7 +124,9 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
if (isSelected(input, values, areValuesEqual)) {
setValueErrors([attributeValueAlreadyExistsError]);
} else {
setValues(updateAtIndex(input, values, id));
if (id) {
setValues(updateAtIndex(input, values, id));
}
closeModal();
}
};
@ -164,7 +168,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
});
return {
id: result.data.attributeCreate?.attribute?.id || null,
id: result.data?.attributeCreate?.attribute?.id ?? undefined,
errors: getMutationErrors(result),
};
};
@ -179,8 +183,8 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
<AttributePage
attribute={null}
disabled={attributeCreateOpts.loading}
errors={attributeCreateOpts.data?.attributeCreate.errors || []}
onDelete={undefined}
errors={attributeCreateOpts?.data?.attributeCreate?.errors || []}
onDelete={() => undefined}
onSubmit={handleSubmit}
onValueAdd={() => openModal("add-value")}
onValueDelete={id =>
@ -212,7 +216,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
file: value?.fileUrl
? {
url: value.fileUrl,
contentType: value.contentType,
contentType: value.contentType ?? "",
__typename: "File",
}
: null,
@ -251,16 +255,16 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
{values.length > 0 && (
<>
<AttributeValueDeleteDialog
attributeName={undefined}
attributeName=""
open={params.action === "remove-value"}
name={getStringOrPlaceholder(values[id]?.name)}
name={getStringOrPlaceholder(id ? values[id]?.name : "")}
confirmButtonState="default"
onClose={closeModal}
onConfirm={handleValueDelete}
/>
<AttributeValueEditDialog
inputType={data.inputType}
attributeValue={values[id]}
attributeValue={id ? values[id] : null}
confirmButtonState="default"
disabled={false}
errors={valueErrors}

View file

@ -91,7 +91,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
const [attributeDelete, attributeDeleteOpts] = useAttributeDeleteMutation({
onCompleted: data => {
if (data?.attributeDelete.errors.length === 0) {
if (data?.attributeDelete?.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
@ -109,7 +109,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
attributeValueDeleteOpts,
] = useAttributeValueDeleteMutation({
onCompleted: data => {
if (data?.attributeValueDelete.errors.length === 0) {
if (data?.attributeValueDelete?.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
@ -128,7 +128,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
attributeValueUpdateOpts,
] = useAttributeValueUpdateMutation({
onCompleted: data => {
if (data?.attributeValueUpdate.errors.length === 0) {
if (data?.attributeValueUpdate?.errors.length === 0) {
notifySaved();
closeModal();
}
@ -137,7 +137,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
const [attributeUpdate, attributeUpdateOpts] = useAttributeUpdateMutation({
onCompleted: data => {
if (data?.attributeUpdate.errors.length === 0) {
if (data?.attributeUpdate?.errors.length === 0) {
notifySaved();
}
},
@ -148,7 +148,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
attributeValueCreateOpts,
] = useAttributeValueCreateMutation({
onCompleted: data => {
if (data?.attributeValueCreate.errors.length === 0) {
if (data?.attributeValueCreate?.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
@ -164,11 +164,11 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
const [attributeValueReorder] = useAttributeValueReorderMutation({
onCompleted: data => {
if (data?.attributeReorderValues.errors.length !== 0) {
if (data?.attributeReorderValues?.errors.length !== 0) {
notify({
status: "error",
text: getAttributeErrorMessage(
data?.attributeReorderValues.errors[0],
data?.attributeReorderValues?.errors[0],
intl,
),
});
@ -185,16 +185,16 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
attributeReorderValues: {
__typename: "AttributeReorderValues",
attribute: {
...data?.attribute,
...data?.attribute!,
choices: {
__typename: "AttributeValueCountableConnection",
pageInfo: {
...data?.attribute.choices.pageInfo,
...data?.attribute?.choices?.pageInfo!,
},
edges: move(
data?.attribute.choices.edges[oldIndex],
data?.attribute.choices.edges,
(a, b) => a.node.id === b.node.id,
data?.attribute?.choices?.edges[oldIndex]!,
data?.attribute?.choices?.edges ?? [],
(a, b) => a?.node.id === b?.node.id,
newIndex,
),
},
@ -205,7 +205,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
variables: {
id,
move: {
id: data?.attribute.choices.edges[oldIndex].node.id,
id: data?.attribute?.choices?.edges[oldIndex].node.id ?? "",
sortOrder: newIndex - oldIndex,
},
firstValues: valuesPaginationState.first,
@ -237,7 +237,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
);
const handleSubmit = createMetadataUpdateHandler(
data?.attribute,
data?.attribute!,
handleUpdate,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables }),
@ -247,7 +247,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
<AttributePage
attribute={data?.attribute}
disabled={loading}
errors={attributeUpdateOpts.data?.attributeUpdate.errors || []}
errors={attributeUpdateOpts.data?.attributeUpdate?.errors || []}
onDelete={() => openModal("remove")}
onSubmit={handleSubmit}
onValueAdd={() => openModal("add-value")}
@ -266,7 +266,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
values={data?.attribute?.choices}
settings={settings}
onUpdateListSettings={updateListSettings}
pageInfo={pageInfo}
pageInfo={pageInfo ?? { hasNextPage: false, hasPreviousPage: false }}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
>
@ -291,7 +291,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
name={getStringOrPlaceholder(
data?.attribute?.choices?.edges?.find(
value => params.id === value.node.id,
)?.node.name,
)?.node?.name ?? "",
)}
useName={true}
confirmButtonState={attributeValueDeleteOpts.status}
@ -299,7 +299,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
onConfirm={() =>
attributeValueDelete({
variables: {
id: params.id,
id: params?.id ?? "",
firstValues: valuesPaginationState.first,
lastValues: valuesPaginationState.last,
afterValues: valuesPaginationState.after,
@ -314,7 +314,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
confirmButtonState={attributeValueCreateOpts.status}
disabled={loading}
errors={
attributeValueCreateOpts.data?.attributeValueCreate.errors || []
attributeValueCreateOpts.data?.attributeValueCreate?.errors || []
}
open={params.action === "add-value"}
onClose={closeModal}
@ -336,21 +336,22 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
attributeValue={attributeValueFragmentToFormData(
data?.attribute?.choices?.edges?.find(
value => params.id === value.node.id,
)?.node,
)?.node ?? null,
)}
confirmButtonState={attributeValueUpdateOpts.status}
disabled={loading}
errors={
attributeValueUpdateOpts.data?.attributeValueUpdate.errors || []
attributeValueUpdateOpts.data?.attributeValueUpdate?.errors || []
}
open={params.action === "edit-value"}
onClose={closeModal}
onSubmit={input =>
attributeValueUpdate({
variables: {
id: data?.attribute.choices.edges.find(
value => params.id === value.node.id,
).node.id,
id:
data?.attribute?.choices?.edges?.find(
value => params.id === value.node.id,
)?.node?.id || "",
input,
firstValues: valuesPaginationState.first,
lastValues: valuesPaginationState.last,

View file

@ -73,7 +73,7 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
attributeBulkDeleteOpts,
] = useAttributeBulkDeleteMutation({
onCompleted: data => {
if (data.attributeBulkDelete.errors.length === 0) {
if (data.attributeBulkDelete?.errors.length === 0) {
closeModal();
notify({
status: "success",
@ -132,7 +132,7 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
};
const paginationValues = usePaginator({
pageInfo: maybe(() => data.attributes.pageInfo),
pageInfo: data?.attributes?.pageInfo,
paginationState,
queryString: params,
});
@ -142,7 +142,7 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
return (
<PaginatorContext.Provider value={paginationValues}>
<AttributeListPage
attributes={mapEdgesToItems(data?.attributes)}
attributes={mapEdgesToItems(data?.attributes) ?? []}
currentTab={currentTab}
disabled={loading || attributeBulkDeleteOpts.loading}
filterOpts={getFilterOpts(params)}
@ -176,12 +176,14 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
/>
<AttributeBulkDeleteDialog
confirmButtonState={attributeBulkDeleteOpts.status}
open={params.action === "remove" && maybe(() => params.ids.length > 0)}
open={
params.action === "remove" && !!params.ids && params.ids.length > 0
}
onConfirm={() =>
attributeBulkDelete({ variables: { ids: params.ids } })
attributeBulkDelete({ variables: { ids: params?.ids ?? [] } })
}
onClose={closeModal}
quantity={maybe(() => params.ids.length)}
quantity={params.ids?.length ?? 0}
/>
<SaveFilterTabDialog
open={params.action === "save-search"}

View file

@ -4,7 +4,7 @@ import {
} from "@dashboard/attributes/components/AttributeListPage";
import { FilterElement } from "@dashboard/components/Filter";
import { AttributeFilterInput } from "@dashboard/graphql";
import { maybe, parseBoolean } from "@dashboard/misc";
import { parseBoolean } from "@dashboard/misc";
import {
createFilterTabUtils,
@ -25,19 +25,19 @@ export function getFilterOpts(
return {
filterableInStorefront: {
active: params.filterableInStorefront !== undefined,
value: maybe(() => parseBoolean(params.filterableInStorefront, true)),
value: parseBoolean(params.filterableInStorefront, true),
},
isVariantOnly: {
active: params.isVariantOnly !== undefined,
value: maybe(() => parseBoolean(params.isVariantOnly, true)),
value: parseBoolean(params.isVariantOnly, true),
},
valueRequired: {
active: params.valueRequired !== undefined,
value: maybe(() => parseBoolean(params.valueRequired, true)),
value: parseBoolean(params.valueRequired, true),
},
visibleInStorefront: {
active: params.visibleInStorefront !== undefined,
value: maybe(() => parseBoolean(params.visibleInStorefront, true)),
value: parseBoolean(params.visibleInStorefront, true),
},
};
}

View file

@ -17,7 +17,7 @@ export function getSortQueryField(
case AttributeListUrlSortField.visible:
return AttributeSortField.VISIBLE_IN_STOREFRONT;
default:
return undefined;
return AttributeSortField.NAME;
}
}

View file

@ -431,7 +431,10 @@ export function findValueInEnum<TEnum extends {}>(
return needle as unknown as TEnum[keyof TEnum];
}
export function parseBoolean(a: string, defaultValue: boolean): boolean {
export function parseBoolean(
a: string | undefined,
defaultValue: boolean,
): boolean {
if (a === undefined) {
return defaultValue;
}