Handle case when attribute value is empty on initial render (#2068)

* Handle case when attribute value is empty on initial render

* Fix useRichText when initial data is undefined

* Update snapshots

* Handle loading state in useRichText

* Add placeholder RichTextEditor when data is loading

* Update snapshots
This commit is contained in:
Jonatan Witoszek 2022-05-31 14:52:59 +02:00 committed by GitHub
parent 7df8268101
commit 47ec01dd7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 183 additions and 10 deletions

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "saleor-dashboard", "name": "saleor-dashboard",
"version": "3.1.0-dev", "version": "3.4.0-dev",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View file

@ -3,6 +3,7 @@ import { Card, CardContent, TextField } from "@material-ui/core";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import RichTextEditor from "@saleor/components/RichTextEditor"; import RichTextEditor from "@saleor/components/RichTextEditor";
import { RichTextEditorLoading } from "@saleor/components/RichTextEditor/RichTextEditorLoading";
import { ProductErrorFragment } from "@saleor/graphql"; import { ProductErrorFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors"; import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
@ -58,7 +59,7 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
/> />
</div> </div>
<FormSpacer /> <FormSpacer />
{isReadyForMount && ( {isReadyForMount ? (
<RichTextEditor <RichTextEditor
defaultValue={defaultValue} defaultValue={defaultValue}
editorRef={editorRef} editorRef={editorRef}
@ -72,6 +73,14 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
})} })}
name="description" name="description"
/> />
) : (
<RichTextEditorLoading
label={intl.formatMessage({
id: "8HRy+U",
defaultMessage: "Category Description"
})}
name="description"
/>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View file

@ -78,6 +78,7 @@ function useCategoryUpdateForm(
const richText = useRichText({ const richText = useRichText({
initial: category?.description, initial: category?.description,
loading: !category,
triggerChange triggerChange
}); });

View file

@ -3,6 +3,7 @@ import { Card, CardContent, TextField } from "@material-ui/core";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import RichTextEditor from "@saleor/components/RichTextEditor"; import RichTextEditor from "@saleor/components/RichTextEditor";
import { RichTextEditorLoading } from "@saleor/components/RichTextEditor/RichTextEditorLoading";
import { CollectionErrorFragment } from "@saleor/graphql"; import { CollectionErrorFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors"; import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
@ -56,7 +57,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
fullWidth fullWidth
/> />
<FormSpacer /> <FormSpacer />
{isReadyForMount && ( {isReadyForMount ? (
<RichTextEditor <RichTextEditor
defaultValue={defaultValue} defaultValue={defaultValue}
editorRef={editorRef} editorRef={editorRef}
@ -67,6 +68,11 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
name="description" name="description"
disabled={disabled} disabled={disabled}
/> />
) : (
<RichTextEditorLoading
label={intl.formatMessage(commonMessages.description)}
name="description"
/>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View file

@ -96,6 +96,7 @@ function useCollectionUpdateForm(
const richText = useRichText({ const richText = useRichText({
initial: collection?.description, initial: collection?.description,
loading: !collection,
triggerChange triggerChange
}); });

View file

@ -0,0 +1,28 @@
import React from "react";
import RichTextEditor, { RichTextEditorProps } from "./RichTextEditor";
interface RichTextEditorLoadingProps
extends Omit<
RichTextEditorProps,
| "disabled"
| "editorRef"
| "onChange"
| "defaultValue"
| "error"
| "helperText"
> {
helperText?: RichTextEditorProps["helperText"];
}
export const RichTextEditorLoading = (props: RichTextEditorLoadingProps) => (
<RichTextEditor
{...props}
disabled={true}
readOnly={true}
error={null}
helperText={props.helperText ?? ""}
defaultValue={{ blocks: [] }}
editorRef={{ current: null }}
/>
);

View file

@ -163,6 +163,7 @@ function usePageForm(
const richText = useRichText({ const richText = useRichText({
initial: pageExists ? page?.content : null, initial: pageExists ? page?.content : null,
loading: pageExists ? !page : false,
triggerChange triggerChange
}); });

View file

@ -2,6 +2,7 @@ import { Card, CardContent, TextField } from "@material-ui/core";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import RichTextEditor from "@saleor/components/RichTextEditor"; import RichTextEditor from "@saleor/components/RichTextEditor";
import { RichTextEditorLoading } from "@saleor/components/RichTextEditor/RichTextEditorLoading";
import { PageErrorFragment } from "@saleor/graphql"; import { PageErrorFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui"; import { makeStyles } from "@saleor/macaw-ui";
@ -64,7 +65,7 @@ const PageInfo: React.FC<PageInfoProps> = props => {
onChange={onChange} onChange={onChange}
/> />
<FormSpacer /> <FormSpacer />
{isReadyForMount && ( {isReadyForMount ? (
<RichTextEditor <RichTextEditor
defaultValue={defaultValue} defaultValue={defaultValue}
editorRef={editorRef} editorRef={editorRef}
@ -79,6 +80,15 @@ const PageInfo: React.FC<PageInfoProps> = props => {
})} })}
name={"content" as keyof PageData} name={"content" as keyof PageData}
/> />
) : (
<RichTextEditorLoading
label={intl.formatMessage({
id: "gMwpNC",
defaultMessage: "Content",
description: "page content"
})}
name={"content" as keyof PageData}
/>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View file

@ -5,6 +5,7 @@ import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Hr from "@saleor/components/Hr"; import Hr from "@saleor/components/Hr";
import RichTextEditor from "@saleor/components/RichTextEditor"; import RichTextEditor from "@saleor/components/RichTextEditor";
import { RichTextEditorLoading } from "@saleor/components/RichTextEditor/RichTextEditorLoading";
import { ProductErrorFragment } from "@saleor/graphql"; import { ProductErrorFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors"; import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
@ -60,7 +61,7 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
onChange={onChange} onChange={onChange}
/> />
<FormSpacer /> <FormSpacer />
{isReadyForMount && ( {isReadyForMount ? (
<RichTextEditor <RichTextEditor
editorRef={editorRef} editorRef={editorRef}
defaultValue={defaultValue} defaultValue={defaultValue}
@ -71,6 +72,11 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
label={intl.formatMessage(commonMessages.description)} label={intl.formatMessage(commonMessages.description)}
name="description" name="description"
/> />
) : (
<RichTextEditorLoading
label={intl.formatMessage(commonMessages.description)}
name="description"
/>
)} )}
<FormSpacer /> <FormSpacer />
<Hr /> <Hr />

View file

@ -281,6 +281,7 @@ function useProductUpdateForm(
const stocks = useFormset(getStockInputFromProduct(product)); const stocks = useFormset(getStockInputFromProduct(product));
const richText = useRichText({ const richText = useRichText({
initial: product?.description, initial: product?.description,
loading: !product,
triggerChange triggerChange
}); });

View file

@ -3,6 +3,7 @@ import { Card, CardContent, TextField } from "@material-ui/core";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import RichTextEditor from "@saleor/components/RichTextEditor"; import RichTextEditor from "@saleor/components/RichTextEditor";
import { RichTextEditorLoading } from "@saleor/components/RichTextEditor/RichTextEditorLoading";
import { ShippingErrorFragment } from "@saleor/graphql"; import { ShippingErrorFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui"; import { makeStyles } from "@saleor/macaw-ui";
@ -100,7 +101,7 @@ const ShippingRateInfo: React.FC<ShippingRateInfoProps> = props => {
onChange={onChange} onChange={onChange}
/> />
<CardSpacer /> <CardSpacer />
{isReadyForMount && ( {isReadyForMount ? (
<RichTextEditor <RichTextEditor
defaultValue={defaultValue} defaultValue={defaultValue}
editorRef={editorRef} editorRef={editorRef}
@ -111,6 +112,11 @@ const ShippingRateInfo: React.FC<ShippingRateInfoProps> = props => {
label={intl.formatMessage(messages.description)} label={intl.formatMessage(messages.description)}
name="description" name="description"
/> />
) : (
<RichTextEditorLoading
label={intl.formatMessage(messages.description)}
name="description"
/>
)} )}
<CardSpacer /> <CardSpacer />
<div className={classes.deliveryTimeFields}> <div className={classes.deliveryTimeFields}>

View file

@ -130,6 +130,7 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
const richText = useRichText({ const richText = useRichText({
initial: rate?.description, initial: rate?.description,
loading: !rate,
triggerChange triggerChange
}); });

View file

@ -41756,6 +41756,20 @@ exports[`Storyshots Views / Categories / Update category loading 1`] = `
<div <div
class="FormSpacer-spacer-id" class="FormSpacer-spacer-id"
/> />
<div
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
data-test-id="rich-text-editor-description"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
data-shrink="true"
>
Category Description
</label>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
</div>
</div> </div>
</div> </div>
<div <div
@ -54202,6 +54216,20 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details l
<div <div
class="FormSpacer-spacer-id" class="FormSpacer-spacer-id"
/> />
<div
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
data-test-id="rich-text-editor-description"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
data-shrink="true"
>
Description
</label>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
</div>
</div> </div>
</div> </div>
<div <div
@ -155768,6 +155796,20 @@ exports[`Storyshots Views / Pages / Page details loading 1`] = `
<div <div
class="FormSpacer-spacer-id" class="FormSpacer-spacer-id"
/> />
<div
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
data-test-id="rich-text-editor-content"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
data-shrink="true"
>
Content
</label>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
</div>
</div> </div>
</div> </div>
<div <div
@ -202808,6 +202850,20 @@ exports[`Storyshots Views / Products / Product edit when loading data 1`] = `
<div <div
class="FormSpacer-spacer-id" class="FormSpacer-spacer-id"
/> />
<div
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
data-test-id="rich-text-editor-description"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
data-shrink="true"
>
Description
</label>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
</div>
<div <div
class="FormSpacer-spacer-id" class="FormSpacer-spacer-id"
/> />
@ -233540,6 +233596,20 @@ exports[`Storyshots Views / Shipping / Shipping rate loading 1`] = `
<div <div
class="CardSpacer-spacer-id" class="CardSpacer-spacer-id"
/> />
<div
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
data-test-id="rich-text-editor-description"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
data-shrink="true"
>
Shipping Rate Description
</label>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
</div>
<div <div
class="CardSpacer-spacer-id" class="CardSpacer-spacer-id"
/> />

View file

@ -3,6 +3,7 @@ import { Typography } from "@material-ui/core";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog"; import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import RichTextEditor from "@saleor/components/RichTextEditor"; import RichTextEditor from "@saleor/components/RichTextEditor";
import RichTextEditorContent from "@saleor/components/RichTextEditor/RichTextEditorContent"; import RichTextEditorContent from "@saleor/components/RichTextEditor/RichTextEditorContent";
import { RichTextEditorLoading } from "@saleor/components/RichTextEditor/RichTextEditorLoading";
import { SubmitPromise } from "@saleor/hooks/useForm"; import { SubmitPromise } from "@saleor/hooks/useForm";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import useRichText from "@saleor/utils/richText/useRichText"; import useRichText from "@saleor/utils/richText/useRichText";
@ -51,7 +52,7 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
return edit ? ( return edit ? (
<form onSubmit={submit}> <form onSubmit={submit}>
{isReadyForMount && ( {isReadyForMount ? (
<RichTextEditor <RichTextEditor
defaultValue={defaultValue} defaultValue={defaultValue}
editorRef={editorRef} editorRef={editorRef}
@ -66,6 +67,15 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
name="translation" name="translation"
data-test-id="translation-field" data-test-id="translation-field"
/> />
) : (
<RichTextEditorLoading
label={intl.formatMessage({
id: "/vCXIP",
defaultMessage: "Translation"
})}
name="translation"
data-test-id="translation-field"
/>
)} )}
<TranslationFieldsSave <TranslationFieldsSave
saveButtonState={saveButtonState} saveButtonState={saveButtonState}

View file

@ -41,6 +41,11 @@ export const useMultipleRichText = <TKey extends string>({
const getDefaultValue = useCallback( const getDefaultValue = useCallback(
(id: TKey) => { (id: TKey) => {
if (initial[id] === undefined) {
setShouldMountById(id, true);
return "";
}
try { try {
const result = JSON.parse(initial[id]); const result = JSON.parse(initial[id]);
setShouldMountById(id, true); setShouldMountById(id, true);

View file

@ -23,13 +23,15 @@ describe("useRichText", () => {
it("properly informs RichTextEditor when data is ready to mount", () => { it("properly informs RichTextEditor when data is ready to mount", () => {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let initial: string | undefined; let initial: string | undefined;
let loading = true;
const { result, rerender } = renderHook(() => const { result, rerender } = renderHook(() =>
useRichText({ initial, triggerChange }) useRichText({ initial, loading, triggerChange })
); );
expect(result.current.isReadyForMount).toBe(false); expect(result.current.isReadyForMount).toBe(false);
initial = JSON.stringify(fixtures.short); // for JSON.parse() initial = JSON.stringify(fixtures.short); // for JSON.parse()
loading = false;
rerender(); rerender();
expect(result.current.defaultValue).toStrictEqual(fixtures.short); expect(result.current.defaultValue).toStrictEqual(fixtures.short);
@ -39,13 +41,15 @@ describe("useRichText", () => {
it("returns undefined when JSON cannot be parsed", () => { it("returns undefined when JSON cannot be parsed", () => {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let initial: string | undefined; let initial: string | undefined;
let loading = true;
const { result, rerender } = renderHook(() => const { result, rerender } = renderHook(() =>
useRichText({ initial, triggerChange }) useRichText({ initial, loading, triggerChange })
); );
expect(result.current.isReadyForMount).toBe(false); expect(result.current.isReadyForMount).toBe(false);
initial = "this-isnt-valid-json"; initial = "this-isnt-valid-json";
loading = false;
rerender(); rerender();
expect(result.current.defaultValue).toBe(undefined); expect(result.current.defaultValue).toBe(undefined);

View file

@ -4,10 +4,15 @@ import { useMemo, useRef, useState } from "react";
interface UseRichTextOptions { interface UseRichTextOptions {
initial: string | null; initial: string | null;
loading?: boolean;
triggerChange: () => void; triggerChange: () => void;
} }
export function useRichText({ initial, triggerChange }: UseRichTextOptions) { export function useRichText({
initial,
loading,
triggerChange
}: UseRichTextOptions) {
const editorRef = useRef<EditorCore>(null); const editorRef = useRef<EditorCore>(null);
const [isReadyForMount, setIsReadyForMount] = useState(false); const [isReadyForMount, setIsReadyForMount] = useState(false);
@ -24,6 +29,15 @@ export function useRichText({ initial, triggerChange }: UseRichTextOptions) {
}; };
const defaultValue = useMemo<OutputData | undefined>(() => { const defaultValue = useMemo<OutputData | undefined>(() => {
if (loading) {
return;
}
if (initial === undefined) {
setIsReadyForMount(true);
return "";
}
try { try {
const result = JSON.parse(initial); const result = JSON.parse(initial);
setIsReadyForMount(true); setIsReadyForMount(true);