From e13b26d7d09ba0daf1e5f111cbab6a581a683744 Mon Sep 17 00:00:00 2001
From: xaralis <filip.varecha@fragaria.cz>
Date: Tue, 29 Dec 2020 11:36:25 +0100
Subject: [PATCH] feat: post can optionally be archived when rejecting it

---
 src/actions/posts.js                          |  21 ++--
 src/components/modals/ModalConfirm.jsx        |  79 +++++-------
 src/components/modals/ModalWithActions.jsx    |  47 +++++++
 .../posts/RejectPostModalConfirm.jsx          |  78 ++++++++++++
 src/containers/PostsContainer.jsx             | 118 +++++++++++++++---
 src/hooks.js                                  |  25 ++--
 6 files changed, 288 insertions(+), 80 deletions(-)
 create mode 100644 src/components/modals/ModalWithActions.jsx
 create mode 100644 src/components/posts/RejectPostModalConfirm.jsx

diff --git a/src/actions/posts.js b/src/actions/posts.js
index 2c0cd2d..c323377 100644
--- a/src/actions/posts.js
+++ b/src/actions/posts.js
@@ -208,9 +208,10 @@ export const archive = createAsyncAction(
  * @param {CF2021.ProposalPost} proposal
  * @param {CF2021.ProposalPostState} state
  */
-const updateProposalState = async (proposal, state) => {
+const updateProposalState = async (proposal, state, additionalPayload) => {
   const body = JSON.stringify({
     state: postsStateMappingRev[state],
+    ...(additionalPayload || {}),
   });
   await fetch(`/posts/${proposal.id}`, {
     method: "PUT",
@@ -277,16 +278,16 @@ export const rejectProposal = createAsyncAction(
   /**
    * @param {CF2021.ProposalPost} proposal
    */
-  (proposal) => {
-    return updateProposalState(proposal, "rejected");
+  ({ proposal, archive }) => {
+    return updateProposalState(proposal, "rejected", { is_archived: archive });
   },
   {
     shortCircuitHook: ({ args }) => {
-      if (args.type !== "procedure-proposal") {
+      if (args.proposal.type !== "procedure-proposal") {
         return errorResult();
       }
 
-      if (args.state !== "announced") {
+      if (args.proposal.state !== "announced") {
         return errorResult();
       }
 
@@ -302,16 +303,18 @@ export const rejectProposalByChairman = createAsyncAction(
   /**
    * @param {CF2021.ProposalPost} proposal
    */
-  (proposal) => {
-    return updateProposalState(proposal, "rejected-by-chairman");
+  ({ proposal, archive }) => {
+    return updateProposalState(proposal, "rejected-by-chairman", {
+      is_archived: archive,
+    });
   },
   {
     shortCircuitHook: ({ args }) => {
-      if (args.type !== "procedure-proposal") {
+      if (args.proposal.type !== "procedure-proposal") {
         return errorResult();
       }
 
-      if (!["pending", "announced"].includes(args.state)) {
+      if (!["pending", "announced"].includes(args.proposal.state)) {
         return errorResult();
       }
 
diff --git a/src/components/modals/ModalConfirm.jsx b/src/components/modals/ModalConfirm.jsx
index cfe8d26..8b4d876 100644
--- a/src/components/modals/ModalConfirm.jsx
+++ b/src/components/modals/ModalConfirm.jsx
@@ -1,16 +1,8 @@
 import React from "react";
 
 import Button from "components/Button";
-import {
-  Card,
-  CardActions,
-  CardBody,
-  CardBodyText,
-  CardHeadline,
-} from "components/cards";
-import ErrorMessage from "components/ErrorMessage";
 
-import Modal from "./Modal";
+import ModalWithActions from "./ModalWithActions";
 
 const ModalConfirm = ({
   title,
@@ -23,44 +15,39 @@ const ModalConfirm = ({
   error,
   ...props
 }) => {
+  const actions = (
+    <>
+      <Button
+        hoverActive
+        color="blue-300"
+        className="text-sm"
+        onClick={onConfirm}
+        loading={confirming}
+      >
+        {yesActionLabel}
+      </Button>
+      <Button
+        hoverActive
+        color="grey-125"
+        className="text-sm"
+        onClick={onCancel}
+      >
+        {cancelActionLabel}
+      </Button>
+    </>
+  );
+
   return (
-    <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
-      <Card>
-        <CardBody>
-          <div className="flex items-center justify-between mb-4">
-            <CardHeadline>{title}</CardHeadline>
-            <button onClick={onCancel} type="button">
-              <i className="ico--cross"></i>
-            </button>
-          </div>
-          <CardBodyText>{children}</CardBodyText>
-          {error && (
-            <ErrorMessage className="mt-2">
-              Při provádění akce došlo k problému: {error}
-            </ErrorMessage>
-          )}
-        </CardBody>
-        <CardActions right className="space-x-1">
-          <Button
-            hoverActive
-            color="blue-300"
-            className="text-sm"
-            onClick={onConfirm}
-            loading={confirming}
-          >
-            {yesActionLabel}
-          </Button>
-          <Button
-            hoverActive
-            color="red-600"
-            className="text-sm"
-            onClick={onCancel}
-          >
-            {cancelActionLabel}
-          </Button>
-        </CardActions>
-      </Card>
-    </Modal>
+    <ModalWithActions
+      onClose={onCancel}
+      title={title}
+      error={error}
+      actions={actions}
+      containerClassName="max-w-md"
+      {...props}
+    >
+      {children}
+    </ModalWithActions>
   );
 };
 
diff --git a/src/components/modals/ModalWithActions.jsx b/src/components/modals/ModalWithActions.jsx
new file mode 100644
index 0000000..242fd67
--- /dev/null
+++ b/src/components/modals/ModalWithActions.jsx
@@ -0,0 +1,47 @@
+import React from "react";
+
+import {
+  Card,
+  CardActions,
+  CardBody,
+  CardBodyText,
+  CardHeadline,
+} from "components/cards";
+import ErrorMessage from "components/ErrorMessage";
+
+import Modal from "./Modal";
+
+const ModalConfirm = ({
+  title,
+  children,
+  actions,
+  error,
+  onClose,
+  ...props
+}) => {
+  return (
+    <Modal onRequestClose={onClose} {...props}>
+      <Card>
+        <CardBody>
+          <div className="flex items-center justify-between mb-4">
+            <CardHeadline>{title}</CardHeadline>
+            <button onClick={onClose} type="button">
+              <i className="ico--cross"></i>
+            </button>
+          </div>
+          <CardBodyText>{children}</CardBodyText>
+          {error && (
+            <ErrorMessage className="mt-2">
+              Při provádění akce došlo k problému: {error}
+            </ErrorMessage>
+          )}
+        </CardBody>
+        <CardActions right className="space-x-1">
+          {actions}
+        </CardActions>
+      </Card>
+    </Modal>
+  );
+};
+
+export default React.memo(ModalConfirm);
diff --git a/src/components/posts/RejectPostModalConfirm.jsx b/src/components/posts/RejectPostModalConfirm.jsx
new file mode 100644
index 0000000..51f1e3e
--- /dev/null
+++ b/src/components/posts/RejectPostModalConfirm.jsx
@@ -0,0 +1,78 @@
+import React from "react";
+
+import Button from "components/Button";
+import {
+  Card,
+  CardActions,
+  CardBody,
+  CardBodyText,
+  CardHeadline,
+} from "components/cards";
+import ErrorMessage from "components/ErrorMessage";
+
+import Modal from "./Modal";
+
+const RejectPostModalConfirm = ({
+  title,
+  children,
+  yesActionLabel = "OK",
+  yesAndArchiveActionLabel = "OK",
+  cancelActionLabel = "Zrušit",
+  onCancel,
+  onConfirm,
+  onConfirmAndArchive,
+  confirming,
+  error,
+  ...props
+}) => {
+  return (
+    <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
+      <Card>
+        <CardBody>
+          <div className="flex items-center justify-between mb-4">
+            <CardHeadline>{title}</CardHeadline>
+            <button onClick={onCancel} type="button">
+              <i className="ico--cross"></i>
+            </button>
+          </div>
+          <CardBodyText>{children}</CardBodyText>
+          {error && (
+            <ErrorMessage className="mt-2">
+              Při provádění akce došlo k problému: {error}
+            </ErrorMessage>
+          )}
+        </CardBody>
+        <CardActions right className="space-x-1">
+          <Button
+            hoverActive
+            color="blue-300"
+            className="text-sm"
+            onClick={onConfirm}
+            loading={confirming}
+          >
+            {yesActionLabel}
+          </Button>
+          <Button
+            hoverActive
+            color="blue-300"
+            className="text-sm"
+            onClick={onConfirm}
+            loading={confirming}
+          >
+            {yesAndArchiveActionLabel}
+          </Button>
+          <Button
+            hoverActive
+            color="red-600"
+            className="text-sm"
+            onClick={onCancel}
+          >
+            {cancelActionLabel}
+          </Button>
+        </CardActions>
+      </Card>
+    </Modal>
+  );
+};
+
+export default React.memo(RejectPostModalConfirm);
diff --git a/src/containers/PostsContainer.jsx b/src/containers/PostsContainer.jsx
index daf2a39..5daa03d 100644
--- a/src/containers/PostsContainer.jsx
+++ b/src/containers/PostsContainer.jsx
@@ -15,8 +15,10 @@ import {
   rejectProposalByChairman,
 } from "actions/posts";
 import { ban, unban } from "actions/users";
+import Button from "components/Button";
 import ErrorMessage from "components/ErrorMessage";
 import ModalConfirm from "components/modals/ModalConfirm";
+import ModalWithActions from "components/modals/ModalWithActions";
 import PostEditModal from "components/posts/PostEditModal";
 import PostList from "components/posts/PostList";
 import { useActionState, useItemActionConfirm } from "hooks";
@@ -68,13 +70,19 @@ const PostsContainer = ({ className, showAddPostCta }) => {
     setPostToReject,
     onRejectConfirm,
     onRejectCancel,
-  ] = useItemActionConfirm(rejectProposal);
+  ] = useItemActionConfirm(rejectProposal, (item, archive) => ({
+    proposal: item,
+    archive,
+  }));
   const [
     postToRejectByChairman,
     setPostToRejectByChairman,
     onRejectByChairmanConfirm,
     onRejectByChairmanCancel,
-  ] = useItemActionConfirm(rejectProposalByChairman);
+  ] = useItemActionConfirm(rejectProposalByChairman, (item, archive) => ({
+    proposal: item,
+    archive,
+  }));
 
   const { isAuthenticated, user } = AuthStore.useState();
   const { window, items } = PostStore.useState((state) =>
@@ -104,12 +112,29 @@ const PostsContainer = ({ className, showAddPostCta }) => {
   );
   const [rejectingProposal, rejectingProposalError] = useActionState(
     rejectProposal,
-    postToReject
+    {
+      proposal: postToReject,
+      archive: false,
+    }
   );
+  const [
+    rejectingAndArchivingProposal,
+    rejectingAndArchivingProposalError,
+  ] = useActionState(rejectProposal, { proposal: postToReject, archive: true });
   const [
     rejectingProposalByChairman,
     rejectingProposalByChairmanError,
-  ] = useActionState(rejectProposalByChairman, postToRejectByChairman);
+  ] = useActionState(rejectProposalByChairman, {
+    proposal: postToRejectByChairman,
+    archive: false,
+  });
+  const [
+    rejectingProposalByChairmanAndArchiving,
+    rejectingProposalByChairmanAndArchivingError,
+  ] = useActionState(rejectProposalByChairman, {
+    proposal: postToRejectByChairman,
+    archive: true,
+  });
 
   const { 2: loadResult } = loadPosts.useWatch();
 
@@ -130,7 +155,7 @@ const PostsContainer = ({ className, showAddPostCta }) => {
         setConfirmingEdit(false);
       }
     },
-    [postToEdit, setPostToEdit]
+    [postToEdit, setPostToEdit, setConfirmingEdit]
   );
 
   const cancelEdit = useCallback(() => {
@@ -253,29 +278,88 @@ const PostsContainer = ({ className, showAddPostCta }) => {
       >
         Procedurální návrh bude <strong>schválen</strong>. Opravdu to chcete?
       </ModalConfirm>
-      <ModalConfirm
+      <ModalWithActions
         isOpen={!!postToReject}
-        onConfirm={onRejectConfirm}
         onCancel={onRejectCancel}
-        confirming={rejectingProposal}
-        error={rejectingProposalError}
+        error={rejectingProposalError || rejectingAndArchivingProposalError}
         title="Zamítnout procedurální návrh?"
-        yesActionLabel="Zamítnout návrh"
+        containerClassName="max-w-lg"
+        actions={
+          <>
+            <Button
+              hoverActive
+              color="blue-300"
+              className="text-sm"
+              onClick={() => onRejectConfirm(false)}
+              loading={rejectingProposal}
+            >
+              ZamĂ­tnout
+            </Button>
+            <Button
+              hoverActive
+              color="blue-300"
+              className="text-sm"
+              onClick={() => onRejectConfirm(true)}
+              loading={rejectingAndArchivingProposal}
+            >
+              ZamĂ­tnout a archivovat
+            </Button>
+            <Button
+              hoverActive
+              color="grey-125"
+              className="text-sm"
+              onClick={onRejectCancel}
+            >
+              Zrušit
+            </Button>
+          </>
+        }
       >
         Procedurální návrh bude <strong>zamítnut</strong>. Opravdu to chcete?
-      </ModalConfirm>
-      <ModalConfirm
+      </ModalWithActions>
+      <ModalWithActions
         isOpen={!!postToRejectByChairman}
-        onConfirm={onRejectByChairmanConfirm}
         onCancel={onRejectByChairmanCancel}
-        confirming={rejectingProposalByChairman}
-        error={rejectingProposalByChairmanError}
+        error={
+          rejectingProposalByChairmanError ||
+          rejectingProposalByChairmanAndArchivingError
+        }
         title="Zamítnout procedurální návrh předsedajícícm?"
-        yesActionLabel="Zamítnout návrh předsedajícím"
+        containerClassName="max-w-lg"
+        actions={
+          <>
+            <Button
+              hoverActive
+              color="blue-300"
+              className="text-sm"
+              onClick={() => onRejectByChairmanConfirm(false)}
+              loading={rejectingProposalByChairman}
+            >
+              ZamĂ­tnout
+            </Button>
+            <Button
+              hoverActive
+              color="blue-300"
+              className="text-sm"
+              onClick={() => onRejectByChairmanConfirm(true)}
+              loading={rejectingProposalByChairmanAndArchiving}
+            >
+              ZamĂ­tnout a archivovat
+            </Button>
+            <Button
+              hoverActive
+              color="grey-125"
+              className="text-sm"
+              onClick={onRejectCancel}
+            >
+              Zrušit
+            </Button>
+          </>
+        }
       >
         Procedurální návrh bude <strong>zamítnut předsedajícím</strong>. Opravdu
         to chcete?
-      </ModalConfirm>
+      </ModalWithActions>
       {postToEdit && (
         <PostEditModal
           isOpen={true}
diff --git a/src/hooks.js b/src/hooks.js
index 7573be0..648d410 100644
--- a/src/hooks.js
+++ b/src/hooks.js
@@ -1,17 +1,26 @@
 import { useCallback, useState } from "react";
 
-export const useItemActionConfirm = (actionFn) => {
+const baseActionParamsBuilder = (item, args) => {
+  return item;
+};
+
+export const useItemActionConfirm = (actionFn, actionParamsBuilder = null) => {
   const [item, setItem] = useState(null);
 
-  const onActionConfirm = useCallback(async () => {
-    if (item) {
-      const result = await actionFn.run(item);
+  const onActionConfirm = useCallback(
+    async (args) => {
+      if (item) {
+        const result = await actionFn.run(
+          (actionParamsBuilder || baseActionParamsBuilder)(item, args)
+        );
 
-      if (!result.error) {
-        setItem(null);
+        if (!result.error) {
+          setItem(null);
+        }
       }
-    }
-  }, [item, setItem, actionFn]);
+    },
+    [item, setItem, actionFn, actionParamsBuilder]
+  );
 
   const onActionCancel = useCallback(() => {
     setItem(null);
-- 
GitLab