Skip to content
Snippets Groups Projects
Commit 0031b24f authored by xaralis's avatar xaralis
Browse files

feat: monitoring actions loading and notify users

parent 83317eb7
No related branches found
No related tags found
No related merge requests found
......@@ -142,6 +142,26 @@ export const hide = createAsyncAction(
}
);
/**
* Edit post content.
*/
export const edit = createAsyncAction(
/**
* @param {CF2021.Post} post
*/
async ({ post, newContent }) => {
try {
const body = JSON.stringify({
content: newContent,
});
await fetch(`/posts/${post.id}`, { method: "PUT", body });
return successResult();
} catch (err) {
return errorResult([], err.toString());
}
}
);
/**
*
* @param {CF2021.ProposalPost} proposal
......
......@@ -10,6 +10,7 @@ const Button = ({
iconChildren = null,
hoverActive = true,
fullwidth = false,
loading = false,
children,
routerTo,
...props
......@@ -21,6 +22,7 @@ const Button = ({
"btn--icon": !!icon,
"btn--hoveractive": hoverActive,
"btn--fullwidth md:btn--autowidth": fullwidth,
"btn--loading": loading,
},
className
);
......
import React from "react";
import classNames from "classnames";
const ErrorMessage = ({ className, children }) => {
return (
<div className={classNames("text-red-600 font-bold", className)}>
{children}
</div>
);
};
export default ErrorMessage;
......@@ -11,9 +11,9 @@ const AnnouncementList = ({
onEdit,
onSeen,
}) => {
const buildHandler = (responderFn) => (post) => (evt) => {
const buildHandler = (responderFn) => (announcement) => (evt) => {
evt.preventDefault();
responderFn(post);
responderFn(announcement);
};
const onAnnouncementEdit = buildHandler(onEdit);
......
......@@ -18,6 +18,7 @@ const ModalConfirm = ({
cancelActionLabel = "Zrušit",
onCancel,
onConfirm,
confirming,
...props
}) => {
return (
......@@ -38,6 +39,7 @@ const ModalConfirm = ({
color="blue-300"
className="text-sm"
onClick={onConfirm}
loading={confirming}
>
{yesActionLabel}
</Button>
......
......@@ -29,6 +29,7 @@ const Post = ({
onAcceptProcedureProposal,
onRejectProcedureProposal,
onRejectProcedureProposalByChairman,
onEdit,
onSeen,
}) => {
const { ref, inView } = useInView({
......@@ -128,6 +129,7 @@ const Post = ({
type === "procedure-proposal" && state === "announced";
const showRejectByChairmanAction =
type === "procedure-proposal" && ["announced", "pending"].includes(state);
const showEditAction = true;
const showBanAction = true;
const showHideAction = !archived;
......@@ -199,6 +201,13 @@ const Post = ({
title="Zamítnout procedurální návrh předsedajícím"
/>
)}
{showEditAction && (
<DropdownMenuItem
onClick={onEdit}
icon="ico--edit-pencil"
title="Upravit příspěvek"
/>
)}
{showBanAction && (
<DropdownMenuItem
onClick={onBanUser}
......
import React, { useState } from "react";
import Button from "components/Button";
import { Card, CardActions, CardBody, CardHeadline } from "components/cards";
import Modal from "components/modals/Modal";
const PostEditModal = ({ post, onCancel, onConfirm, ...props }) => {
const [text, setText] = useState(post.content);
const onTextInput = (evt) => {
setText(evt.target.value);
};
const confirm = (evt) => {
if (!!text) {
onConfirm(text);
}
};
return (
<Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
<Card>
<CardBody>
<div className="flex items-center justify-between mb-4">
<CardHeadline>Upravit text příspěvku</CardHeadline>
<button onClick={onCancel}>
<i className="ico--close"></i>
</button>
</div>
<div className="form-field">
<label className="form-field__label" htmlFor="field">
Nový text příspěvku
</label>
<div className="form-field__wrapper form-field__wrapper--shadowed">
<textarea
className="text-input form-field__control text-base"
value={text}
rows="8"
placeholder="Vyplňte text příspěvku"
onChange={onTextInput}
></textarea>
</div>
</div>
</CardBody>
<CardActions right className="space-x-1">
<Button
hoverActive
color="blue-300"
className="text-sm"
onClick={confirm}
>
Uložit
</Button>
<Button
hoverActive
color="red-600"
className="text-sm"
onClick={onCancel}
>
Zrušit
</Button>
</CardActions>
</Card>
</Modal>
);
};
export default PostEditModal;
......@@ -16,6 +16,7 @@ const PostList = ({
onAcceptProcedureProposal,
onRejectProcedureProposal,
onRejectProcedureProposalByChairman,
onEdit,
onSeen,
dimArchived,
}) => {
......@@ -26,6 +27,7 @@ const PostList = ({
const onPostLike = buildHandler(onLike);
const onPostDislike = buildHandler(onDislike);
const onPostEdit = buildHandler(onEdit);
const onPostHide = buildHandler(onHide);
const onPostBanUser = buildHandler(onBanUser);
const onPostAnnounceProcedureProposal = buildHandler(
......@@ -71,11 +73,14 @@ const PostList = ({
onRejectProcedureProposalByChairman={onPostRejectProcedureProposalByChairman(
item
)}
onEdit={onPostEdit(item)}
onSeen={onPostSeen(item)}
/>
))}
{!items.length && (
<p>Nikdo zatím žádný příspěvek do rozpravy nepřidal. Budeš první?</p>
<p className="p-4 lg:p-0 lg:py-3 ">
Nikdo zatím žádný příspěvek do rozpravy nepřidal. Budeš první?
</p>
)}
</div>
);
......
......@@ -3,6 +3,7 @@ import classNames from "classnames";
import { addAnnouncement } from "actions/announcements";
import Button from "components/Button";
import { useActionLoading } from "hooks";
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
......@@ -12,6 +13,12 @@ const AddAnnouncementForm = ({ className }) => {
const [linkValid, setLinkValid] = useState(false);
const [type, setType] = useState("announcement");
const addingAnnouncement = useActionLoading(addAnnouncement, {
content: text,
link,
type,
});
const onTextInput = (evt) => {
setText(evt.target.value);
};
......@@ -24,11 +31,14 @@ const AddAnnouncementForm = ({ className }) => {
}
};
const onAdd = (evt) => {
const onAdd = async (evt) => {
if (!!text) {
addAnnouncement.run({ content: text, link, type });
setText("");
setLink("");
const result = await addAnnouncement.run({ content: text, link, type });
if (!result.error) {
setText("");
setLink("");
}
}
};
......@@ -102,7 +112,10 @@ const AddAnnouncementForm = ({ className }) => {
onClick={onAdd}
className="text-sm mt-4"
hoverActive
disabled={!text || (type === "voting" && !linkValid)}
loading={addingAnnouncement}
disabled={
!text || (type === "voting" && !linkValid) || addingAnnouncement
}
>
Přidat oznámení
</Button>
......
......@@ -2,27 +2,36 @@ import React, { useState } from "react";
import { addPost, addProposal } from "actions/posts";
import Button from "components/Button";
import { useActionLoading } from "hooks";
const AddPostForm = ({ className }) => {
const [text, setText] = useState("");
const addingPost = useActionLoading(addPost, { content: text });
const addingProposal = useActionLoading(addPost, { content: text });
const onTextInput = (evt) => {
setText(evt.target.value);
};
const onAddPost = (evt) => {
const onAddPost = async (evt) => {
if (!!text) {
addPost.run({ content: text });
setText("");
const result = await addPost.run({ content: text });
if (!result.error) {
setText("");
}
}
};
const onAddProposal = (evt) => {
const onAddProposal = async (evt) => {
evt.stopPropagation();
if (!!text) {
addProposal.run({ content: text });
setText("");
const result = await addProposal.run({ content: text });
if (!result.error) {
setText("");
}
}
};
......@@ -54,7 +63,8 @@ const AddPostForm = ({ className }) => {
<div className="space-x-4">
<Button
onClick={onAddPost}
disabled={!text}
disabled={!text || addingPost || addingProposal}
loading={addingPost || addingProposal}
hoverActive
icon="ico--chevron-down"
iconWrapperClassName="dropdown-button"
......
......@@ -2,16 +2,20 @@ import React, { useCallback, useState } from "react";
import {
deleteAnnouncement,
loadAnnouncements,
updateAnnouncementContent,
} from "actions/announcements";
import AnnouncementEditModal from "components/annoucements/AnnouncementEditModal";
import AnnouncementList from "components/annoucements/AnnouncementList";
import { CardBody } from "components/cards";
import ErrorMessage from "components/ErrorMessage";
import ModalConfirm from "components/modals/ModalConfirm";
import { useItemActionConfirm } from "hooks";
import { useActionLoading, useItemActionConfirm } from "hooks";
import { AnnouncementStore, AuthStore } from "stores";
const AnnoucementsContainer = () => {
const [itemToEdit, setItemToEdit] = useState(null);
const { 2: loadResult } = loadAnnouncements.useWatch();
const [
itemToDelete,
......@@ -20,6 +24,11 @@ const AnnoucementsContainer = () => {
onDeleteCancel,
] = useItemActionConfirm(deleteAnnouncement);
const deletingAnnouncement = useActionLoading(
deleteAnnouncement,
itemToDelete
);
const { isAuthenticated, user } = AuthStore.useState();
const items = AnnouncementStore.useState((state) =>
state.itemIds.map((id) => state.items[id])
......@@ -51,6 +60,13 @@ const AnnoucementsContainer = () => {
return (
<>
{loadResult && loadResult.error && (
<CardBody>
<ErrorMessage>
Oznámení se nepodařilo načíst: {loadResult.message}
</ErrorMessage>
</CardBody>
)}
<AnnouncementList
items={items}
canRunActions={isAuthenticated && user.role === "chairman"}
......@@ -62,6 +78,7 @@ const AnnoucementsContainer = () => {
isOpen={!!itemToDelete}
onConfirm={onDeleteConfirm}
onCancel={onDeleteCancel}
confirming={deletingAnnouncement}
title="Opravdu smazat?"
yesActionLabel="Smazat"
>
......
import React from "react";
import React, { useCallback, useState } from "react";
import pick from "lodash/pick";
import {
acceptProposal,
announceProposal,
dislike,
edit,
hide,
like,
loadPosts,
rejectProposal,
rejectProposalByChairman,
} from "actions/posts";
import { ban } from "actions/users";
import ErrorMessage from "components/ErrorMessage";
import ModalConfirm from "components/modals/ModalConfirm";
import PostEditModal from "components/posts/PostEditModal";
import PostList from "components/posts/PostList";
import { useItemActionConfirm } from "hooks";
import { useActionLoading, useItemActionConfirm } from "hooks";
import { AuthStore, PostStore } from "stores";
const PostsContainer = ({ className }) => {
const [postToEdit, setPostToEdit] = useState(null);
const [
userToBan,
setUserToBan,
......@@ -62,6 +68,32 @@ const PostsContainer = ({ className }) => {
(state) => state.filters.flags === "archived"
);
const banningUser = useActionLoading(ban, userToBan);
const hidingPost = useActionLoading(hide, postToHide);
const announcingProposal = useActionLoading(announceProposal, postToAnnounce);
const acceptingProposal = useActionLoading(acceptProposal, postToAccept);
const rejectingProposal = useActionLoading(rejectProposal, postToReject);
const rejectingProposalByChairman = useActionLoading(
rejectProposalByChairman,
postToRejectByChairman
);
const { 2: loadResult } = loadPosts.useWatch();
const confirmEdit = useCallback(
async (newContent) => {
if (postToEdit && newContent) {
await edit.run({ post: postToEdit, newContent });
setPostToEdit(null);
}
},
[postToEdit, setPostToEdit]
);
const cancelEdit = useCallback(() => {
setPostToEdit(null);
}, [setPostToEdit]);
/**
* Ban a post's author.
* @param {CF2021.Post} post
......@@ -85,6 +117,11 @@ const PostsContainer = ({ className }) => {
return (
<>
{loadResult && loadResult.error && (
<ErrorMessage>
Příspěvky se nepodařilo načíst: {loadResult.message}
</ErrorMessage>
)}
<PostList
items={window.items
.slice(sliceStart, sliceEnd)
......@@ -98,6 +135,7 @@ const PostsContainer = ({ className }) => {
dimArchived={!showingArchivedOnly}
onHide={setPostToHide}
onBanUser={onBanUser}
onEdit={setPostToEdit}
onAnnounceProcedureProposal={setPostToAnnounce}
onAcceptProcedureProposal={setPostToAccept}
onRejectProcedureProposal={setPostToReject}
......@@ -107,6 +145,7 @@ const PostsContainer = ({ className }) => {
isOpen={!!userToBan}
onConfirm={onBanUserConfirm}
onCancel={onBanUserCancel}
confirming={banningUser}
title={`Zablokovat uživatele ${userToBan ? userToBan.name : null}?`}
yesActionLabel="Zablokovat"
>
......@@ -117,15 +156,7 @@ const PostsContainer = ({ className }) => {
isOpen={!!postToHide}
onConfirm={onPostHideConfirm}
onCancel={onPostHideCancel}
title="Skrýt příspěvek?"
yesActionLabel="Potvrdit"
>
Příspěvek se skryje a uživatelé ho neuvidí. Opravdu to chcete?
</ModalConfirm>
<ModalConfirm
isOpen={!!postToHide}
onConfirm={onPostHideConfirm}
onCancel={onPostHideCancel}
confirming={hidingPost}
title="Skrýt příspěvek?"
yesActionLabel="Potvrdit"
>
......@@ -135,6 +166,7 @@ const PostsContainer = ({ className }) => {
isOpen={!!postToAnnounce}
onConfirm={onAnnounceConfirm}
onCancel={onAnnounceCancel}
confirming={announcingProposal}
title="Vyhlásit procedurální návrh?"
yesActionLabel="Vyhlásit návrh"
>
......@@ -144,6 +176,7 @@ const PostsContainer = ({ className }) => {
isOpen={!!postToAccept}
onConfirm={onAcceptConfirm}
onCancel={onAcceptCancel}
confirming={acceptingProposal}
title="Schválit procedurální návrh?"
yesActionLabel="Schválit návrh"
>
......@@ -153,6 +186,7 @@ const PostsContainer = ({ className }) => {
isOpen={!!postToReject}
onConfirm={onRejectConfirm}
onCancel={onRejectCancel}
confirming={rejectingProposal}
title="Zamítnout procedurální návrh?"
yesActionLabel="Zamítnout návrh"
>
......@@ -162,12 +196,21 @@ const PostsContainer = ({ className }) => {
isOpen={!!postToRejectByChairman}
onConfirm={onRejectByChairmanConfirm}
onCancel={onRejectByChairmanCancel}
confirming={rejectingProposalByChairman}
title="Zamítnout procedurální návrh předsedajícícm?"
yesActionLabel="Zamítnout návrh předsedajícím"
>
Procedurální návrh bude <strong>zamítnut předsedajícím</strong>. Opravdu
to chcete?
</ModalConfirm>
{postToEdit && (
<PostEditModal
isOpen={true}
post={postToEdit}
onConfirm={confirmEdit}
onCancel={cancelEdit}
/>
)}
</>
);
};
......
......@@ -3,10 +3,13 @@ import { useCallback, useState } from "react";
export const useItemActionConfirm = (actionFn) => {
const [item, setItem] = useState(null);
const onActionConfirm = useCallback(() => {
const onActionConfirm = useCallback(async () => {
if (item) {
actionFn.run(item);
setItem(null);
const result = await actionFn.run(item);
if (!result.error) {
setItem(null);
}
}
}, [item, setItem, actionFn]);
......@@ -20,10 +23,13 @@ export const useItemActionConfirm = (actionFn) => {
export const useActionConfirm = (actionFn, actionArgs) => {
const [showConfirm, setShowConfirm] = useState(false);
const onActionConfirm = useCallback(() => {
const onActionConfirm = useCallback(async () => {
if (showConfirm) {
actionFn.run(actionArgs);
setShowConfirm(false);
const result = await actionFn.run(actionArgs);
if (!result.error) {
setShowConfirm(false);
}
}
}, [showConfirm, setShowConfirm, actionFn, actionArgs]);
......@@ -33,3 +39,8 @@ export const useActionConfirm = (actionFn, actionArgs) => {
return [showConfirm, setShowConfirm, onActionConfirm, onActionCancel];
};
export const useActionLoading = (actionFn, actionArgs) => {
const { 0: started, 1: finished } = actionFn.useWatch(actionArgs);
return started && !finished;
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment