Repository: mirumee/saleor-dashboard (ts)

Issue:
Ability to change staff user's password
Staff users should be able to change their passwords. Implment a modal with a form to change the password:

![image](https://user-images.githubusercontent.com/5421321/69947149-555a3000-14ed-11ea-8033-d2de36538d0b.png)

GraphQL mutation to use here is `passwordChange`. It works for currently authenticated user.

Pull request description:
Add ability to reset own password
I want to merge this change because it resolves #283 

### Screenshots
<img width="1680" alt="Screenshot 2019-12-03 at 17 46 06" src="https://user-images.githubusercontent.com/6833443/70071010-e10ab400-15f4-11ea-967a-b5a6fbcc0dd7.png">
<img width="1680" alt="Screenshot 2019-12-03 at 17 46 28" src="https://user-images.githubusercontent.com/6833443/70071011-e1a34a80-15f4-11ea-85fb-a00bdf6c58cd.png">
<img width="1680" alt="Screenshot 2019-12-03 at 17 46 36" src="https://user-images.githubusercontent.com/6833443/70071012-e1a34a80-15f4-11ea-9909-df96814130f4.png">


### Pull Request Checklist

1. [x] All visible strings are translated with proper context.
1. [x] All data-formatting is locale-aware (dates, numbers, and so on).
1. [x] Translated strings are extracted to `.pot` file.
1. [x] Number of API calls is optimized.
1. [x] The changes are tested.
1. [x] Type definitions are up to date.
1. [x] Changes are mentioned in the changelog.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b2430dd3f..f8144c1ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Fix dropdown clickable areas - #281 by @dominik-zeglen
 - Use eslint - #285 by @dominik-zeglen
 - Enforce using "name" property in style hooks - #288 by @dominik-zeglen
+- Add ability to reset own password - #289 by @dominik-zeglen
 
 ## 2.0.0
 
diff --git a/locale/messages.pot b/locale/messages.pot
index 28902c762..2d58e0b2e 100644
--- a/locale/messages.pot
+++ b/locale/messages.pot
@@ -1,6 +1,6 @@
 msgid ""
 msgstr ""
-"POT-Creation-Date: 2019-11-26T14:34:48.426Z\n"
+"POT-Creation-Date: 2019-12-04T14:17:34.264Z\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "MIME-Version: 1.0\n"
@@ -1855,6 +1855,14 @@ msgctxt "description"
 msgid "Category name"
 msgstr ""
 
+#: build/locale/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.json
+#. [src.staff.components.StaffPasswordResetDialog.2521568990] - dialog header
+#. defaultMessage is:
+#. Change Password
+msgctxt "dialog header"
+msgid "Change Password"
+msgstr ""
+
 #: build/locale/src/staff/components/StaffProperties/StaffProperties.json
 #. [src.staff.components.StaffProperties.2771097267] - button
 #. defaultMessage is:
@@ -1863,6 +1871,14 @@ msgctxt "button"
 msgid "Change photo"
 msgstr ""
 
+#: build/locale/src/staff/components/StaffPassword/StaffPassword.json
+#. [src.staff.components.StaffPassword.1434811103] - button
+#. defaultMessage is:
+#. Change your password
+msgctxt "button"
+msgid "Change your password"
+msgstr ""
+
 #: build/locale/src/products/components/ProductPricing/ProductPricing.json
 #. [src.products.components.ProductPricing.3015886868]
 #. defaultMessage is:
@@ -4911,6 +4927,14 @@ msgctxt "description"
 msgid "New Password"
 msgstr ""
 
+#: build/locale/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.json
+#. [src.staff.components.StaffPasswordResetDialog.1254879564] - input label
+#. defaultMessage is:
+#. New Password
+msgctxt "input label"
+msgid "New Password"
+msgstr ""
+
 #: build/locale/src/products/views/ProductCreate.json
 #. [src.products.views.1591632382] - page header
 #. defaultMessage is:
@@ -4927,6 +4951,14 @@ msgctxt "variant name"
 msgid "New Variant"
 msgstr ""
 
+#: build/locale/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.json
+#. [src.staff.components.StaffPasswordResetDialog.1651415182]
+#. defaultMessage is:
+#. New password must be at least 8 characters long
+msgctxt "description"
+msgid "New password must be at least 8 characters long"
+msgstr ""
+
 #: build/locale/src/taxes/components/CountryTaxesPage/CountryTaxesPage.json
 #. [src.taxes.components.CountryTaxesPage.1451721797] - tax rate
 #. defaultMessage is:
@@ -5947,6 +5979,14 @@ msgctxt "description"
 msgid "Password"
 msgstr ""
 
+#: build/locale/src/staff/components/StaffPassword/StaffPassword.json
+#. [src.staff.components.StaffPassword.2237029987] - header
+#. defaultMessage is:
+#. Password
+msgctxt "header"
+msgid "Password"
+msgstr ""
+
 #: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
 #. [src.auth.components.NewPasswordPage.4253911811]
 #. defaultMessage is:
@@ -6215,6 +6255,14 @@ msgctxt "previous step, button"
 msgid "Previous"
 msgstr ""
 
+#: build/locale/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.json
+#. [src.staff.components.StaffPasswordResetDialog.53359254] - input label
+#. defaultMessage is:
+#. Previous Password
+msgctxt "input label"
+msgid "Previous Password"
+msgstr ""
+
 #: build/locale/src/categories/components/CategoryProductList/CategoryProductList.json
 #. [src.categories.components.CategoryProductList.1134347598] - product price
 #. defaultMessage is:
@@ -9667,6 +9715,14 @@ msgctxt "description"
 msgid "Yes"
 msgstr ""
 
+#: build/locale/src/staff/components/StaffPassword/StaffPassword.json
+#. [src.staff.components.StaffPassword.1274006906]
+#. defaultMessage is:
+#. You should change your password every month to avoid security issues.
+msgctxt "description"
+msgid "You should change your password every month to avoid security issues."
+msgstr ""
+
 #: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json
 #. [src.products.components.ProductVariantCreateDialog.1009678918] - header
 #. defaultMessage is:
diff --git a/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx b/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx
index f8e1d9b80..dfb3ae516 100644
--- a/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx
+++ b/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx
@@ -19,6 +19,7 @@ import { PermissionEnum } from "../../../types/globalTypes";
 import { StaffMemberDetails_user } from "../../types/StaffMemberDetails";
 import StaffPreferences from "../StaffPreferences";
 import StaffProperties from "../StaffProperties/StaffProperties";
+import StaffPassword from "../StaffPassword/StaffPassword";
 
 interface FormData {
   hasFullAccess: boolean;
@@ -39,6 +40,7 @@ export interface StaffDetailsPageProps {
   saveButtonBarState: ConfirmButtonTransitionState;
   staffMember: StaffMemberDetails_user;
   onBack: () => void;
+  onChangePassword: () => void;
   onDelete: () => void;
   onImageDelete: () => void;
   onSubmit: (data: FormData) => void;
@@ -55,6 +57,7 @@ const StaffDetailsPage: React.FC<StaffDetailsPageProps> = ({
   saveButtonBarState,
   staffMember,
   onBack,
+  onChangePassword,
   onDelete,
   onImageDelete,
   onImageUpload,
@@ -100,6 +103,12 @@ const StaffDetailsPage: React.FC<StaffDetailsPageProps> = ({
                 onImageUpload={onImageUpload}
                 onImageDelete={onImageDelete}
               />
+              {canEditPreferences && (
+                <>
+                  <CardSpacer />
+                  <StaffPassword onChangePassword={onChangePassword} />
+                </>
+              )}
             </div>
             <div>
               {canEditPreferences && (
diff --git a/src/staff/components/StaffPassword/StaffPassword.tsx b/src/staff/components/StaffPassword/StaffPassword.tsx
new file mode 100644
index 000000000..1d9582540
--- /dev/null
+++ b/src/staff/components/StaffPassword/StaffPassword.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import Button from "@material-ui/core/Button";
+import Card from "@material-ui/core/Card";
+import CardContent from "@material-ui/core/CardContent";
+import Typography from "@material-ui/core/Typography";
+import { FormattedMessage, useIntl } from "react-intl";
+
+import CardTitle from "@saleor/components/CardTitle";
+
+interface StaffPasswordProps {
+  onChangePassword: () => void;
+}
+
+const StaffPassword: React.FC<StaffPasswordProps> = ({ onChangePassword }) => {
+  const intl = useIntl();
+
+  return (
+    <Card>
+      <CardTitle
+        title={intl.formatMessage({
+          defaultMessage: "Password",
+          description: "header"
+        })}
+        toolbar={
+          <Button color="primary" onClick={onChangePassword}>
+            <FormattedMessage
+              defaultMessage="Change your password"
+              description="button"
+            />
+          </Button>
+        }
+      />
+      <CardContent>
+        <Typography>
+          <FormattedMessage defaultMessage="You should change your password every month to avoid security issues." />
+        </Typography>
+      </CardContent>
+    </Card>
+  );
+};
+
+StaffPassword.displayName = "StaffPassword";
+export default StaffPassword;
diff --git a/src/staff/components/StaffPassword/index.ts b/src/staff/components/StaffPassword/index.ts
new file mode 100644
index 000000000..9131e4472
--- /dev/null
+++ b/src/staff/components/StaffPassword/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./StaffPassword";
+export * from "./StaffPassword";
diff --git a/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx b/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx
new file mode 100644
index 000000000..93420361b
--- /dev/null
+++ b/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx
@@ -0,0 +1,111 @@
+import React from "react";
+import Button from "@material-ui/core/Button";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import TextField from "@material-ui/core/TextField";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import { FormattedMessage, useIntl } from "react-intl";
+
+import { DialogProps, UserError } from "@saleor/types";
+import { buttonMessages } from "@saleor/intl";
+import Form from "@saleor/components/Form";
+import ConfirmButton, {
+  ConfirmButtonTransitionState
+} from "@saleor/components/ConfirmButton";
+import FormSpacer from "@saleor/components/FormSpacer";
+import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
+
+interface StaffPasswordResetDialogFormData {
+  newPassword: string;
+  oldPassword: string;
+}
+export interface StaffPasswordResetDialogProps extends DialogProps {
+  confirmButtonState: ConfirmButtonTransitionState;
+  errors: UserError[];
+  onSubmit: (data: StaffPasswordResetDialogFormData) => void;
+}
+
+const initialForm: StaffPasswordResetDialogFormData = {
+  newPassword: "",
+  oldPassword: ""
+};
+
+const StaffPasswordResetDialog: React.FC<StaffPasswordResetDialogProps> = ({
+  confirmButtonState,
+  errors: apiErrors,
+  open,
+  onClose,
+  onSubmit
+}) => {
+  const intl = useIntl();
+  const dialogErrors = useModalDialogErrors(apiErrors, open);
+
+  return (
+    <Dialog onClose={onClose} open={open} fullWidth maxWidth="sm">
+      <DialogTitle>
+        <FormattedMessage
+          defaultMessage="Change Password"
+          description="dialog header"
+        />
+      </DialogTitle>
+      <Form errors={dialogErrors} initial={initialForm} onSubmit={onSubmit}>
+        {({ change, data, errors, submit }) => (
+          <>
+            <DialogContent>
+              <TextField
+                error={!!errors.oldPassword}
+                fullWidth
+                helperText={errors.oldPassword}
+                label={intl.formatMessage({
+                  defaultMessage: "Previous Password",
+                  description: "input label"
+                })}
+                name="oldPassword"
+                type="password"
+                onChange={change}
+              />
+              <FormSpacer />
+              <TextField
+                error={!!errors.newPassword}
+                fullWidth
+                helperText={
+                  errors.newPassword ||
+                  intl.formatMessage({
+                    defaultMessage:
+                      "New password must be at least 8 characters long"
+                  })
+                }
+                label={intl.formatMessage({
+                  defaultMessage: "New Password",
+                  description: "input label"
+                })}
+                name="newPassword"
+                type="password"
+                onChange={change}
+              />
+            </DialogContent>
+            <DialogActions>
+              <Button onClick={onClose}>
+                <FormattedMessage {...buttonMessages.back} />
+              </Button>
+              <ConfirmButton
+                disabled={data.newPassword.length < 8}
+                transitionState={confirmButtonState}
+                color="primary"
+                variant="contained"
+                type="submit"
+                onClick={submit}
+              >
+                <FormattedMessage {...buttonMessages.save} />
+              </ConfirmButton>
+            </DialogActions>
+          </>
+        )}
+      </Form>
+    </Dialog>
+  );
+};
+
+StaffPasswordResetDialog.displayName = "StaffPasswordResetDialog";
+export default StaffPasswordResetDialog;
diff --git a/src/staff/components/StaffPasswordResetDialog/index.ts b/src/staff/components/StaffPasswordResetDialog/index.ts
new file mode 100644
index 000000000..4bffd3362
--- /dev/null
+++ b/src/staff/components/StaffPasswordResetDialog/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./StaffPasswordResetDialog";
+export * from "./StaffPasswordResetDialog";
diff --git a/src/staff/mutations.ts b/src/staff/mutations.ts
index a3939ecc3..ee1dde297 100644
--- a/src/staff/mutations.ts
+++ b/src/staff/mutations.ts
@@ -1,5 +1,6 @@
 import gql from "graphql-tag";
 
+import makeMutation from "@saleor/hooks/makeMutation";
 import { TypedMutation } from "../mutations";
 import { staffMemberDetailsFragment } from "./queries";
 import { StaffAvatarDelete } from "./types/StaffAvatarDelete";
@@ -19,6 +20,10 @@ import {
   StaffMemberUpdate,
   StaffMemberUpdateVariables
 } from "./types/StaffMemberUpdate";
+import {
+  ChangeStaffPassword,
+  ChangeStaffPasswordVariables
+} from "./types/ChangeStaffPassword";
 
 const staffMemberAddMutation = gql`
   ${staffMemberDetailsFragment}
@@ -114,3 +119,18 @@ export const TypedStaffAvatarDeleteMutation = TypedMutation<
   StaffAvatarDelete,
   StaffMemberDeleteVariables
 >(staffAvatarDeleteMutation);
+
+const changeStaffPassword = gql`
+  mutation ChangeStaffPassword($newPassword: String!, $oldPassword: String!) {
+    passwordChange(newPassword: $newPassword, oldPassword: $oldPassword) {
+      errors {
+        field
+        message
+      }
+    }
+  }
+`;
+export const useChangeStaffPassword = makeMutation<
+  ChangeStaffPassword,
+  ChangeStaffPasswordVariables
+>(changeStaffPassword);
diff --git a/src/staff/types/ChangeStaffPassword.ts b/src/staff/types/ChangeStaffPassword.ts
new file mode 100644
index 000000000..18c15db12
--- /dev/null
+++ b/src/staff/types/ChangeStaffPassword.ts
@@ -0,0 +1,27 @@
+/* tslint:disable */
+/* eslint-disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: ChangeStaffPassword
+// ====================================================
+
+export interface ChangeStaffPassword_passwordChange_errors {
+  __typename: "Error";
+  field: string | null;
+  message: string | null;
+}
+
+export interface ChangeStaffPassword_passwordChange {
+  __typename: "PasswordChange";
+  errors: ChangeStaffPassword_passwordChange_errors[] | null;
+}
+
+export interface ChangeStaffPassword {
+  passwordChange: ChangeStaffPassword_passwordChange | null;
+}
+
+export interface ChangeStaffPasswordVariables {
+  newPassword: string;
+  oldPassword: string;
+}
diff --git a/src/staff/urls.ts b/src/staff/urls.ts
index 7bdf6e201..b99a09235 100644
--- a/src/staff/urls.ts
+++ b/src/staff/urls.ts
@@ -27,7 +27,10 @@ export const staffListUrl = (params?: StaffListUrlQueryParams) =>
   staffListPath + "?" + stringifyQs(params);
 
 export const staffMemberDetailsPath = (id: string) => urlJoin(staffSection, id);
-export type StaffMemberDetailsUrlDialog = "remove" | "remove-avatar";
+export type StaffMemberDetailsUrlDialog =
+  | "change-password"
+  | "remove"
+  | "remove-avatar";
 export type StaffMemberDetailsUrlQueryParams = Dialog<
   StaffMemberDetailsUrlDialog
 >;
diff --git a/src/staff/views/StaffDetails.tsx b/src/staff/views/StaffDetails.tsx
index e3739efc0..85a9c4bc6 100644
--- a/src/staff/views/StaffDetails.tsx
+++ b/src/staff/views/StaffDetails.tsx
@@ -15,7 +15,8 @@ import {
   TypedStaffAvatarDeleteMutation,
   TypedStaffAvatarUpdateMutation,
   TypedStaffMemberDeleteMutation,
-  TypedStaffMemberUpdateMutation
+  TypedStaffMemberUpdateMutation,
+  useChangeStaffPassword
 } from "../mutations";
 import { TypedStaffMemberDetailsQuery } from "../queries";
 import { StaffAvatarDelete } from "../types/StaffAvatarDelete";
@@ -27,6 +28,8 @@ import {
   staffMemberDetailsUrl,
   StaffMemberDetailsUrlQueryParams
 } from "../urls";
+import StaffPasswordResetDialog from "../components/StaffPasswordResetDialog";
+import { ChangeStaffPassword } from "../types/ChangeStaffPassword";
 
 interface OrderListProps {
   id: string;
@@ -40,6 +43,32 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
   const intl = useIntl();
   const shop = useShop();
 
+  const closeModal = () =>
+    navigate(
+      staffMemberDetailsUrl(id, {
+        ...params,
+        action: undefined
+      })
+    );
+
+  const handleChangePassword = (data: ChangeStaffPassword) => {
+    if (data.passwordChange.errors.length === 0) {
+      notify({
+        text: intl.formatMessage(commonMessages.savedChanges)
+      });
+      closeModal();
+    }
+  };
+  const [changePassword, changePasswordOpts] = useChangeStaffPassword({
+    onCompleted: handleChangePassword
+  });
+
+  const changePasswordTransitionState = getMutationState(
+    changePasswordOpts.called,
+    changePasswordOpts.loading,
+    maybe(() => changePasswordOpts.data.passwordChange.errors)
+  );
+
   return (
     <TypedStaffMemberDetailsQuery
       displayLoader
@@ -128,6 +157,13 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
                                 canRemove={!isUserSameAsViewer}
                                 disabled={loading}
                                 onBack={() => navigate(staffListUrl())}
+                                onChangePassword={() =>
+                                  navigate(
+                                    staffMemberDetailsUrl(id, {
+                                      action: "change-password"
+                                    })
+                                  )
+                                }
                                 onDelete={() =>
                                   navigate(
                                     staffMemberDetailsUrl(id, {
@@ -175,9 +211,7 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
                                 })}
                                 confirmButtonState={deleteTransitionState}
                                 variant="delete"
-                                onClose={() =>
-                                  navigate(staffMemberDetailsUrl(id))
-                                }
+                                onClose={closeModal}
                                 onConfirm={deleteStaffMember}
                               >
                                 <DialogContentText>
@@ -197,9 +231,7 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
                                 })}
                                 confirmButtonState={deleteAvatarTransitionState}
                                 variant="delete"
-                                onClose={() =>
-                                  navigate(staffMemberDetailsUrl(id))
-                                }
+                                onClose={closeModal}
                                 onConfirm={deleteStaffAvatar}
                               >
                                 <DialogContentText>
@@ -215,6 +247,24 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
                                   />
                                 </DialogContentText>
                               </ActionDialog>
+                              <StaffPasswordResetDialog
+                                confirmButtonState={
+                                  changePasswordTransitionState
+                                }
+                                errors={maybe(
+                                  () =>
+                                    changePasswordOpts.data.passwordChange
+                                      .errors,
+                                  []
+                                )}
+                                open={params.action === "change-password"}
+                                onClose={closeModal}
+                                onSubmit={data =>
+                                  changePassword({
+                                    variables: data
+                                  })
+                                }
+                              />
                             </>
                           );
                         }}
diff --git a/src/storybook/stories/staff/StaffDetailsPage.tsx b/src/storybook/stories/staff/StaffDetailsPage.tsx
index 455e7d7fe..4559cd2aa 100644
--- a/src/storybook/stories/staff/StaffDetailsPage.tsx
+++ b/src/storybook/stories/staff/StaffDetailsPage.tsx
@@ -16,6 +16,7 @@ const props: Omit<StaffDetailsPageProps, "classes"> = {
   canRemove: true,
   disabled: false,
   onBack: () => undefined,
+  onChangePassword: () => undefined,
   onDelete: () => undefined,
   onImageDelete: () => undefined,
   onImageUpload: () => undefined,


Test patch:
diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap
index ec1f35e51..a03cbb98b 100644
--- a/src/storybook/__snapshots__/Stories.test.ts.snap
+++ b/src/storybook/__snapshots__/Stories.test.ts.snap
@@ -121349,6 +121349,52 @@ exports[`Storyshots Views / Staff / Staff member details himself 1`] = `
               </div>
             </div>
           </div>
+          <div
+            class="CardSpacer-spacer-id"
+          />
+          <div
+            class="MuiPaper-root-id MuiPaper-elevation0-id MuiCard-root-id MuiPaper-rounded-id"
+          >
+            <div
+              class="CardTitle-root-id"
+            >
+              <span
+                class="MuiTypography-root-id CardTitle-title-id MuiTypography-h5-id"
+              >
+                Password
+              </span>
+              <div
+                class="CardTitle-toolbar-id"
+              >
+                <button
+                  class="MuiButtonBase-root-id MuiButton-root-id MuiButton-text-id MuiButton-textPrimary-id"
+                  tabindex="0"
+                  type="button"
+                >
+                  <span
+                    class="MuiButton-label-id"
+                  >
+                    Change your password
+                  </span>
+                </button>
+              </div>
+            </div>
+            <div
+              class="CardTitle-children-id"
+            />
+            <hr
+              class="CardTitle-hr-id"
+            />
+            <div
+              class="MuiCardContent-root-id"
+            >
+              <div
+                class="MuiTypography-root-id MuiTypography-body1-id"
+              >
+                You should change your password every month to avoid security issues.
+              </div>
+            </div>
+          </div>
         </div>
         <div>
           <div