Skip to content

Commit

Permalink
THEME-738: Identity | Account Management (#1739)
Browse files Browse the repository at this point in the history
* THEMES-738: copied in v1 account management code, began converting to v2.

* THEMES-738: saving work, need to convert social sign on first.

* THEMES-738: converted most of the account management block.

* THEMES-738: updated components to use v2 social sign on block.

* THEMES-768: updated styling for account management to better match mock ups.

* THEMES-738: finished correcting styling for account management page.

* THEMES-738: updated tests to use react testing library.

* THEMES-738: fixed tests for editable form input.

* THEMES-738: tiny edits to testing files so jest --changed-since can see those tests for the coverage report.

* THEMES-738: added testing ignores for Identity SDK functionality.

* THEMES-738: updated styling for paragraphs in the edit blocks. Also updated styles for the commerce theme.

* Apply suggestions from code review

---------

Co-authored-by: nschubach <[email protected]>
  • Loading branch information
vgalatro and nschubach authored Oct 10, 2023
1 parent a4cd643 commit 9ae76f1
Show file tree
Hide file tree
Showing 17 changed files with 1,390 additions and 2 deletions.
25 changes: 25 additions & 0 deletions blocks/identity-block/_index.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
@use "@wpmedia/arc-themes-components/scss";

.b-account-management {
&__edit {
@include scss.block-components("account-management-edit");
@include scss.block-properties("account-management-edit");
}

&__edit-label {
@include scss.block-components("account-management-edit-label");
@include scss.block-properties("account-management-edit-label");
}

&__section {
@include scss.block-components("account-management-section");
@include scss.block-properties("account-management-section");
}

&__social-edit {
@include scss.block-components("account-management-social-edit");
@include scss.block-properties("account-management-social-edit");
}

@include scss.block-components("account-management");
@include scss.block-properties("account-management");
}

.b-forgot-password {
@include scss.block-components("forgot-password");
@include scss.block-properties("forgot-password");
Expand Down
101 changes: 101 additions & 0 deletions blocks/identity-block/components/editable-form-input/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useState, useRef } from "react";
import { Button, Paragraph } from "@wpmedia/arc-themes-components";

// handles submit and display of form
// will toggle back to not editable upon successful submit
export function ConditionalFormContainer({ showForm, children, onSubmit, setIsEditable }) {
const formRef = useRef();

// handleSubmit from headline submit form
const handleSubmit = (event) => {
event.preventDefault();

const valid = formRef.current.checkValidity();
if (valid) {
const namedFields = Array.from(formRef.current.elements)
.filter((element) => element?.name && typeof element?.name !== "undefined")
.reduce(
(accumulator, element) => ({
...accumulator,
[element.name]: element.value,
}),
{}
);
onSubmit(namedFields).then(() => setIsEditable(false));
}
};

return showForm ? (
<form data-testid="conditional-form" onSubmit={handleSubmit} ref={formRef}>
{children}
</form>
) : (
<>{children}</>
);
}

function EditableFieldPresentational({
blockClassName,
cancelEdit,
cancelText,
children,
editText,
formErrorText,
initialValue,
label,
onSubmit,
saveText,
}) {
const [isEditable, setIsEditable] = useState(!!formErrorText);
return (
<section className={`${blockClassName}__edit`}>
<ConditionalFormContainer
onSubmit={onSubmit}
showForm={isEditable}
setIsEditable={setIsEditable}
>
<div>
{isEditable ? (
<>
{children}
{formErrorText ? (
<section role="alert">
<Paragraph>{formErrorText}</Paragraph>
</section>
) : null}
<div>
<Button
onClick={() => {
if (cancelEdit) {
cancelEdit();
}
setIsEditable(false);
}}
size="small"
type="submit"
>
<span>{cancelText}</span>
</Button>
<Button size="small" type="submit">
<span>{saveText}</span>
</Button>
</div>
</>
) : (
<>
<div className={`${blockClassName}__edit-label`}>
<Paragraph>{label}</Paragraph>
<Button onClick={() => setIsEditable(true)} size="small" type="button">
<span>{editText}</span>
</Button>
</div>
<Paragraph>{initialValue}</Paragraph>
</>
)}
</div>
</ConditionalFormContainer>
</section>
);
}

export default EditableFieldPresentational;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";

import { FormInputField, FIELD_TYPES } from "@wpmedia/shared-styles";
import EditableFormInput from ".";

export default {
title: "Blocks/Identity/Components/EditableFormInput",
parameters: {
chromatic: { viewports: [320, 1200] },
},
};

export const emailField = () => (
<EditableFormInput
initialValue="[email protected]"
label="Email address"
editText="Edit"
saveText="Save"
cancelText="Cancel"
>
<FormInputField
type={FIELD_TYPES.EMAIL}
label="Email address"
defaultValue="[email protected]"
showDefaultError={false}
required
autoComplete="email"
name="email"
validationErrorMessage="Please enter a valid email address"
/>
</EditableFormInput>
);
123 changes: 123 additions & 0 deletions blocks/identity-block/components/editable-form-input/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";

import EditableFormInputField, { ConditionalFormContainer } from ".";

describe("Editable form input field", () => {
it("conditional form renders a form when show form elected", () => {
render(<ConditionalFormContainer showForm />);
expect(screen.getByTestId("conditional-form")).not.toBeNull();
});

it("conditional form does not render a form when show form not elected", () => {
render(<ConditionalFormContainer showForm={false} />);
expect(screen.queryByTestId("conditional-form")).toBeNull();
});

it("editable form field shows initial value, label, and edit button when not editable and hides children", () => {
render(
<EditableFormInputField
initialValue="initial value"
editText="edit text"
label="label"
onSubmit={() => {}}
>
<p id="test-child">Test child</p>
</EditableFormInputField>
);
expect(screen.getByText("initial value")).not.toBeNull();
expect(screen.getByText("edit text")).not.toBeNull();
expect(screen.getByText("label")).not.toBeNull();
});

it("shows error text if passed in with formErrorText prop", () => {
render(
<EditableFormInputField
initialValue="initial value"
editText="edit text"
label="label"
onSubmit={() => {}}
formErrorText="Error Text"
>
<p id="test-child">Test child</p>
</EditableFormInputField>
);

expect(screen.getByText("Error Text")).not.toBeNull();
});

it("editable form field hides edit button when editable and shows children", async () => {
render(
<EditableFormInputField
initialValue="initial value"
editText="edit text"
label="label"
onSubmit={() => {}}
>
<p id="test-child">Test child</p>
</EditableFormInputField>
);

fireEvent.click(screen.getByRole("button"));
expect(screen.queryByText("initial value")).toBeNull();
expect(screen.queryByText("edit text")).toBeNull();
});

it("does not submit if the input is invalid", () => {
const callback = jest.fn(() => Promise.resolve());

render(
<ConditionalFormContainer onSubmit={callback} setIsEditable={() => {}} showForm>
<input name="inputField" type="email" defaultValue="invalid" />
</ConditionalFormContainer>
);

fireEvent.submit(screen.getByTestId("conditional-form"));

expect(callback).not.toHaveBeenCalled();
});

it("does submit if the input is valid", () => {
const callback = jest.fn(() => Promise.resolve());

render(
<ConditionalFormContainer onSubmit={callback} showForm setIsEditable={() => {}}>
<input name="inputField" type="email" defaultValue="[email protected]" />
</ConditionalFormContainer>
);

fireEvent.submit(screen.getByTestId("conditional-form"));

expect(callback).toHaveBeenCalledWith({
inputField: "[email protected]",
});
});

it("calls passed in cancelEdit function when using cancel button", () => {
const callback = jest.fn();

render(
<EditableFormInputField
cancelEdit={callback}
cancelText="cancel change"
formErrorText="Error"
>
<input name="inputField" type="email" defaultValue="invalid" />
</EditableFormInputField>
);

fireEvent.click(screen.getByText("cancel change"));

expect(callback).toHaveBeenCalled();
});

it("calls passed in cancelEdit function when using cancel button", () => {
render(
<EditableFormInputField formErrorText="Error">
<input name="inputField" type="email" defaultValue="invalid" />
</EditableFormInputField>
);

expect(screen.getByText("Error")).not.toBeNull();
});
});
1 change: 0 additions & 1 deletion blocks/identity-block/components/identity/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ describe("Identity useIdentity Hook", () => {
},
],
};

const Test = () => {
const { getSignedInIdentity } = useIdentity();
const getCurrent = getSignedInIdentity(testUser);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ describe("Identity Social Login Component", () => {
initializeFacebook: () => {},
},
}));

render(
<GoogleSignInProvider>
<SocialSignOn />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useState } from "react";
import { useFusionContext } from "fusion:context";
import getProperties from "fusion:properties";
import getTranslatedPhrases from "fusion:intl";
import { Input } from "@wpmedia/arc-themes-components";
import useIdentity from "../../../components/identity";
import EditableFieldPresentational from "../../../components/editable-form-input";

function EmailEditableFieldContainer({ blockClassName, email, setEmail }) {
const [formErrorText, setFormErrorText] = useState(null);

const { arcSite } = useFusionContext();
const { locale } = getProperties(arcSite);
const phrases = getTranslatedPhrases(locale);
const { Identity } = useIdentity();

const formEmailLabel = phrases.t("identity-block.email");
const emailRequirements = phrases.t("identity-block.email-requirements");
const editText = phrases.t("identity-block.edit");
const saveText = phrases.t("identity-block.save");
const cancelText = phrases.t("identity-block.cancel");
const emailError = phrases.t("identity-block.update-email-error");

// istanbul ignore next
const handleEmailUpdate = ({ email: newEmail }) =>
Identity.updateUserProfile({ email: newEmail })
.then((profileObject) => {
setEmail(profileObject.email);
})
.catch(() => {
setFormErrorText(emailError);
throw new Error();
});

const handleCancelEdit = () => {
setFormErrorText(null);
};

return (
<EditableFieldPresentational
cancelEdit={handleCancelEdit}
cancelText={cancelText}
blockClassName={blockClassName}
editText={editText}
formErrorText={formErrorText}
initialValue={email}
label={formEmailLabel}
onSubmit={handleEmailUpdate}
saveText={saveText}
>
<Input
type="email"
label={formEmailLabel}
defaultValue={email}
showDefaultError={false}
required
autoComplete="email"
name="email"
validationErrorMessage={emailRequirements}
/>
</EditableFieldPresentational>
);
}

export default EmailEditableFieldContainer;
Loading

0 comments on commit 9ae76f1

Please sign in to comment.