Skip to content
Snippets Groups Projects
Commit 1480c500 authored by xaralis's avatar xaralis
Browse files

feat: more error handling, better form UX

parent 64645cae
Branches
No related tags found
No related merge requests found
Pipeline #1859 passed
Showing
with 377 additions and 61 deletions
...@@ -11347,6 +11347,11 @@ ...@@ -11347,6 +11347,11 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
}, },
"react-mde": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/react-mde/-/react-mde-11.0.0.tgz",
"integrity": "sha512-U3k/ITPXklEjXkKhR7rgI3Y7ii5V62slSmG+/rYDQaCAabNwX+5dULKpIxWWSyqi+PvsuRVEYx6vV4sECMMbCw=="
},
"react-modal": { "react-modal": {
"version": "3.12.1", "version": "3.12.1",
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.12.1.tgz", "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.12.1.tgz",
...@@ -12596,6 +12601,63 @@ ...@@ -12596,6 +12601,63 @@
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww=="
}, },
"showdown": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/showdown/-/showdown-1.9.1.tgz",
"integrity": "sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA==",
"requires": {
"yargs": "^14.2"
},
"dependencies": {
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"yargs": {
"version": "14.2.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
"integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
"requires": {
"cliui": "^5.0.0",
"decamelize": "^1.2.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^15.0.1"
}
},
"yargs-parser": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz",
"integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"side-channel": { "side-channel": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz",
......
...@@ -45,7 +45,11 @@ export const addAnnouncement = createAsyncAction( ...@@ -45,7 +45,11 @@ export const addAnnouncement = createAsyncAction(
link, link,
type: announcementTypeMappingRev[type], type: announcementTypeMappingRev[type],
}); });
const resp = await fetch("/announcements", { method: "POST", body }); const resp = await fetch("/announcements", {
method: "POST",
body,
expectedStatus: 201,
});
const data = await resp.json(); const data = await resp.json();
return successResult(data.data); return successResult(data.data);
} catch (err) { } catch (err) {
...@@ -64,7 +68,10 @@ export const deleteAnnouncement = createAsyncAction( ...@@ -64,7 +68,10 @@ export const deleteAnnouncement = createAsyncAction(
*/ */
async (item) => { async (item) => {
try { try {
await fetch(`/announcements/${item.id}`, { method: "DELETE" }); await fetch(`/announcements/${item.id}`, {
method: "DELETE",
expectedStatus: 204,
});
return successResult({ item }); return successResult({ item });
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -86,7 +93,11 @@ export const updateAnnouncementContent = createAsyncAction( ...@@ -86,7 +93,11 @@ export const updateAnnouncementContent = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
content: newContent, content: newContent,
}); });
await fetch(`/announcements/${item.id}`, { method: "PUT", body }); await fetch(`/announcements/${item.id}`, {
method: "PUT",
body,
expectedStatus: 204,
});
return successResult({ item, newContent }); return successResult({ item, newContent });
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
......
...@@ -14,7 +14,7 @@ import { ...@@ -14,7 +14,7 @@ import {
export const loadPosts = createAsyncAction( export const loadPosts = createAsyncAction(
async () => { async () => {
try { try {
const resp = await fetch("/posts"); const resp = await fetch("/posts", { expectedStatus: 200 });
const data = await resp.json(); const data = await resp.json();
return successResult(data.data); return successResult(data.data);
} catch (err) { } catch (err) {
...@@ -23,7 +23,7 @@ export const loadPosts = createAsyncAction( ...@@ -23,7 +23,7 @@ export const loadPosts = createAsyncAction(
}, },
{ {
postActionHook: ({ result }) => { postActionHook: ({ result }) => {
if (!result.error) { if (!result.error && result.payload) {
const posts = result.payload.map(parseRawPost); const posts = result.payload.map(parseRawPost);
PostStore.update((state) => { PostStore.update((state) => {
...@@ -49,7 +49,10 @@ export const like = createAsyncAction( ...@@ -49,7 +49,10 @@ export const like = createAsyncAction(
*/ */
async (post) => { async (post) => {
try { try {
await fetch(`/posts/${post.id}/like`, { method: "PATCH" }); await fetch(`/posts/${post.id}/like`, {
method: "PATCH",
expectedStatus: 204,
});
return successResult(post); return successResult(post);
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -72,7 +75,10 @@ export const dislike = createAsyncAction( ...@@ -72,7 +75,10 @@ export const dislike = createAsyncAction(
*/ */
async (post) => { async (post) => {
try { try {
await fetch(`/posts/${post.id}/dislike`, { method: "PATCH" }); await fetch(`/posts/${post.id}/dislike`, {
method: "PATCH",
expectedStatus: 204,
});
return successResult(post); return successResult(post);
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -98,7 +104,7 @@ export const addPost = createAsyncAction(async ({ content }) => { ...@@ -98,7 +104,7 @@ export const addPost = createAsyncAction(async ({ content }) => {
content, content,
type: postsTypeMappingRev["post"], type: postsTypeMappingRev["post"],
}); });
await fetch(`/posts`, { method: "POST", body }); await fetch(`/posts`, { method: "POST", body, expectedStatus: 201 });
return successResult(); return successResult();
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -114,7 +120,7 @@ export const addProposal = createAsyncAction(async ({ content }) => { ...@@ -114,7 +120,7 @@ export const addProposal = createAsyncAction(async ({ content }) => {
content, content,
type: postsTypeMappingRev["procedure-proposal"], type: postsTypeMappingRev["procedure-proposal"],
}); });
await fetch(`/posts`, { method: "POST", body }); await fetch(`/posts`, { method: "POST", body, expectedStatus: 201 });
return successResult(); return successResult();
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -154,7 +160,35 @@ export const edit = createAsyncAction( ...@@ -154,7 +160,35 @@ export const edit = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
content: newContent, content: newContent,
}); });
await fetch(`/posts/${post.id}`, { method: "PUT", body }); await fetch(`/posts/${post.id}`, {
method: "PUT",
body,
expectedStatus: 204,
});
return successResult();
} catch (err) {
return errorResult([], err.toString());
}
}
);
/**
* Archive post.
*/
export const archive = createAsyncAction(
/**
* @param {CF2021.Post} post
*/
async (post) => {
try {
const body = JSON.stringify({
is_archived: true,
});
await fetch(`/posts/${post.id}`, {
method: "PUT",
body,
expectedStatus: 204,
});
return successResult(); return successResult();
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -171,7 +205,11 @@ const updateProposalState = async (proposal, state) => { ...@@ -171,7 +205,11 @@ const updateProposalState = async (proposal, state) => {
const body = JSON.stringify({ const body = JSON.stringify({
state: postsStateMappingRev[state], state: postsStateMappingRev[state],
}); });
await fetch(`/posts/${proposal.id}`, { method: "PUT", body }); await fetch(`/posts/${proposal.id}`, {
method: "PUT",
body,
expectedStatus: 204,
});
return successResult(proposal); return successResult(proposal);
}; };
......
...@@ -71,7 +71,11 @@ export const renameProgramPoint = createAsyncAction( ...@@ -71,7 +71,11 @@ export const renameProgramPoint = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
title: newTitle, title: newTitle,
}); });
await fetch(`/program/${programEntry.id}`, { method: "PUT", body }); await fetch(`/program/${programEntry.id}`, {
method: "PUT",
body,
expectedStatus: 204,
});
return successResult({ programEntry, newTitle }); return successResult({ programEntry, newTitle });
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -104,7 +108,11 @@ export const endProgramPoint = createAsyncAction( ...@@ -104,7 +108,11 @@ export const endProgramPoint = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
is_live: false, is_live: false,
}); });
await fetch(`/program/${programEntry.id}`, { method: "PUT", body }); await fetch(`/program/${programEntry.id}`, {
method: "PUT",
body,
expectedStatus: 204,
});
return successResult(programEntry); return successResult(programEntry);
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -134,7 +142,11 @@ export const activateProgramPoint = createAsyncAction( ...@@ -134,7 +142,11 @@ export const activateProgramPoint = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
is_live: true, is_live: true,
}); });
await fetch(`/program/${programEntry.id}`, { method: "PUT", body }); await fetch(`/program/${programEntry.id}`, {
method: "PUT",
body,
expectedStatus: 204,
});
return successResult(programEntry); return successResult(programEntry);
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -167,7 +179,11 @@ export const openDiscussion = createAsyncAction( ...@@ -167,7 +179,11 @@ export const openDiscussion = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
discussion_opened: true, discussion_opened: true,
}); });
await fetch(`/program/${programEntry.id}`, { method: "PUT", body }); await fetch(`/program/${programEntry.id}`, {
method: "PUT",
body,
expectedStatus: 204,
});
return successResult(programEntry); return successResult(programEntry);
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -195,7 +211,11 @@ export const closeDiscussion = createAsyncAction( ...@@ -195,7 +211,11 @@ export const closeDiscussion = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
discussion_opened: false, discussion_opened: false,
}); });
await fetch(`/program/${programEntry.id}`, { method: "PUT", body }); await fetch(`/program/${programEntry.id}`, {
method: "PUT",
body,
expectedStatus: 204,
});
return successResult(programEntry); return successResult(programEntry);
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
......
...@@ -2,19 +2,29 @@ import baseFetch from "unfetch"; ...@@ -2,19 +2,29 @@ import baseFetch from "unfetch";
import { AuthStore } from "./stores"; import { AuthStore } from "./stores";
export const fetch = (url, opts) => { export const fetch = async (
url,
{ headers = {}, expectedStatus = 200, method = "GET", body = null } = {}
) => {
const { isAuthenticated, user } = AuthStore.getRawState(); const { isAuthenticated, user } = AuthStore.getRawState();
opts = opts || {};
opts.headers = opts.headers || {};
if (isAuthenticated) { if (isAuthenticated) {
opts.headers.Authorization = "Bearer " + user.accessToken; headers.Authorization = "Bearer " + user.accessToken;
}
if (!headers["Content-Type"]) {
headers["Content-Type"] = "application/json";
} }
if (!opts.headers["Content-Type"]) { const response = await baseFetch(process.env.REACT_APP_API_BASE_URL + url, {
opts.headers["Content-Type"] = "application/json"; body,
method,
headers,
});
if (!!expectedStatus && response.status !== expectedStatus) {
throw new Error(`Unexpected status code ${response.status}`);
} }
return baseFetch(process.env.REACT_APP_API_BASE_URL + url, opts); return response;
}; };
import React from "react";
import ReactMde from "react-mde";
import Showdown from "showdown";
const converter = new Showdown.Converter({
tables: true,
simplifiedAutoLink: true,
strikethrough: true,
tasklists: true,
});
const MarkdownEditor = ({ value, onChange }) => {
return (
<ReactMde
value={value}
onChange={onChange}
// selectedTab={selectedTab}
// onTabChange={setSelectedTab}
generateMarkdownPreview={(markdown) =>
Promise.resolve(converter.makeHtml(markdown))
}
/>
);
};
export default MarkdownEditor;
...@@ -2,12 +2,15 @@ import React, { useState } from "react"; ...@@ -2,12 +2,15 @@ import React, { useState } from "react";
import Button from "components/Button"; import Button from "components/Button";
import { Card, CardActions, CardBody, CardHeadline } from "components/cards"; import { Card, CardActions, CardBody, CardHeadline } from "components/cards";
import ErrorMessage from "components/ErrorMessage";
import Modal from "components/modals/Modal"; import Modal from "components/modals/Modal";
const AnnouncementEditModal = ({ const AnnouncementEditModal = ({
announcement, announcement,
onCancel, onCancel,
onConfirm, onConfirm,
confirming,
error,
...props ...props
}) => { }) => {
const [text, setText] = useState(announcement.content); const [text, setText] = useState(announcement.content);
...@@ -46,12 +49,18 @@ const AnnouncementEditModal = ({ ...@@ -46,12 +49,18 @@ const AnnouncementEditModal = ({
></textarea> ></textarea>
</div> </div>
</div> </div>
{error && (
<ErrorMessage className="mt-2">
Při editaci došlo k problému: {error}
</ErrorMessage>
)}
</CardBody> </CardBody>
<CardActions right className="space-x-1"> <CardActions right className="space-x-1">
<Button <Button
hoverActive hoverActive
color="blue-300" color="blue-300"
className="text-sm" className="text-sm"
loading={confirming}
onClick={confirm} onClick={confirm}
> >
Uložit Uložit
......
...@@ -36,7 +36,7 @@ const ModalConfirm = ({ ...@@ -36,7 +36,7 @@ const ModalConfirm = ({
<CardBodyText>{children}</CardBodyText> <CardBodyText>{children}</CardBodyText>
{error && ( {error && (
<ErrorMessage className="mt-2"> <ErrorMessage className="mt-2">
Při provádění akce došlo k problému: error Při provádění akce došlo k problému: {error}
</ErrorMessage> </ErrorMessage>
)} )}
</CardBody> </CardBody>
...@@ -64,4 +64,4 @@ const ModalConfirm = ({ ...@@ -64,4 +64,4 @@ const ModalConfirm = ({
); );
}; };
export default ModalConfirm; export default React.memo(ModalConfirm);
...@@ -30,6 +30,7 @@ const Post = ({ ...@@ -30,6 +30,7 @@ const Post = ({
onRejectProcedureProposal, onRejectProcedureProposal,
onRejectProcedureProposalByChairman, onRejectProcedureProposalByChairman,
onEdit, onEdit,
onArchive,
onSeen, onSeen,
}) => { }) => {
const { ref, inView } = useInView({ const { ref, inView } = useInView({
...@@ -132,6 +133,7 @@ const Post = ({ ...@@ -132,6 +133,7 @@ const Post = ({
const showEditAction = true; const showEditAction = true;
const showBanAction = true; const showBanAction = true;
const showHideAction = !archived; const showHideAction = !archived;
const showArchiveAction = !archived;
return ( return (
<div className={wrapperClassName} ref={ref}> <div className={wrapperClassName} ref={ref}>
...@@ -222,6 +224,13 @@ const Post = ({ ...@@ -222,6 +224,13 @@ const Post = ({
title="Skrýt příspěvek" title="Skrýt příspěvek"
/> />
)} )}
{showArchiveAction && (
<DropdownMenuItem
onClick={onArchive}
icon="ico--drawer"
title="Archivovat příspěvek"
/>
)}
</DropdownMenu> </DropdownMenu>
)} )}
</div> </div>
......
...@@ -2,9 +2,17 @@ import React, { useState } from "react"; ...@@ -2,9 +2,17 @@ import React, { useState } from "react";
import Button from "components/Button"; import Button from "components/Button";
import { Card, CardActions, CardBody, CardHeadline } from "components/cards"; import { Card, CardActions, CardBody, CardHeadline } from "components/cards";
import ErrorMessage from "components/ErrorMessage";
import Modal from "components/modals/Modal"; import Modal from "components/modals/Modal";
const PostEditModal = ({ post, onCancel, onConfirm, ...props }) => { const PostEditModal = ({
post,
onCancel,
onConfirm,
confirming,
error,
...props
}) => {
const [text, setText] = useState(post.content); const [text, setText] = useState(post.content);
const onTextInput = (evt) => { const onTextInput = (evt) => {
...@@ -41,12 +49,18 @@ const PostEditModal = ({ post, onCancel, onConfirm, ...props }) => { ...@@ -41,12 +49,18 @@ const PostEditModal = ({ post, onCancel, onConfirm, ...props }) => {
></textarea> ></textarea>
</div> </div>
</div> </div>
{error && (
<ErrorMessage className="mt-2">
Při editaci došlo k problému: {error}
</ErrorMessage>
)}
</CardBody> </CardBody>
<CardActions right className="space-x-1"> <CardActions right className="space-x-1">
<Button <Button
hoverActive hoverActive
color="blue-300" color="blue-300"
className="text-sm" className="text-sm"
loading={confirming}
onClick={confirm} onClick={confirm}
> >
Uložit Uložit
......
...@@ -17,6 +17,7 @@ const PostList = ({ ...@@ -17,6 +17,7 @@ const PostList = ({
onRejectProcedureProposal, onRejectProcedureProposal,
onRejectProcedureProposalByChairman, onRejectProcedureProposalByChairman,
onEdit, onEdit,
onArchive,
onSeen, onSeen,
dimArchived, dimArchived,
}) => { }) => {
...@@ -30,6 +31,7 @@ const PostList = ({ ...@@ -30,6 +31,7 @@ const PostList = ({
const onPostEdit = buildHandler(onEdit); const onPostEdit = buildHandler(onEdit);
const onPostHide = buildHandler(onHide); const onPostHide = buildHandler(onHide);
const onPostBanUser = buildHandler(onBanUser); const onPostBanUser = buildHandler(onBanUser);
const onPostArchive = buildHandler(onArchive);
const onPostAnnounceProcedureProposal = buildHandler( const onPostAnnounceProcedureProposal = buildHandler(
onAnnounceProcedureProposal onAnnounceProcedureProposal
); );
...@@ -74,6 +76,7 @@ const PostList = ({ ...@@ -74,6 +76,7 @@ const PostList = ({
item item
)} )}
onEdit={onPostEdit(item)} onEdit={onPostEdit(item)}
onArchive={onPostArchive(item)}
onSeen={onPostSeen(item)} onSeen={onPostSeen(item)}
/> />
))} ))}
......
...@@ -11,7 +11,8 @@ const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()] ...@@ -11,7 +11,8 @@ const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]
const AddAnnouncementForm = ({ className }) => { const AddAnnouncementForm = ({ className }) => {
const [text, setText] = useState(""); const [text, setText] = useState("");
const [link, setLink] = useState(""); const [link, setLink] = useState("");
const [linkValid, setLinkValid] = useState(false); const [linkValid, setLinkValid] = useState(null);
const [noTextError, setNoTextError] = useState(false);
const [type, setType] = useState("announcement"); const [type, setType] = useState("announcement");
const [adding, addingError] = useActionState(addAnnouncement, { const [adding, addingError] = useActionState(addAnnouncement, {
...@@ -22,24 +23,36 @@ const AddAnnouncementForm = ({ className }) => { ...@@ -22,24 +23,36 @@ const AddAnnouncementForm = ({ className }) => {
const onTextInput = (evt) => { const onTextInput = (evt) => {
setText(evt.target.value); setText(evt.target.value);
if (evt.target.value !== "") {
setNoTextError(false);
}
}; };
const onLinkInput = (evt) => { const onLinkInput = (evt) => {
setLink(evt.target.value); setLink(evt.target.value);
if (!!evt.target.value) { if (!!evt.target.value) {
setLinkValid(evt.target.value.match(urlRegex)); setLinkValid(!!evt.target.value.match(urlRegex));
} }
}; };
const onAdd = async (evt) => { const onAdd = async (evt) => {
if (!link) {
setLinkValid(false);
}
if (!!text) { if (!!text) {
const result = await addAnnouncement.run({ content: text, link, type }); if (type === "voting" && link) {
const result = await addAnnouncement.run({ content: text, link, type });
if (!result.error) { if (!result.error) {
setText(""); setText("");
setLink(""); setLink("");
}
} }
} else {
setNoTextError(true);
} }
}; };
...@@ -77,7 +90,11 @@ const AddAnnouncementForm = ({ className }) => { ...@@ -77,7 +90,11 @@ const AddAnnouncementForm = ({ className }) => {
</div> </div>
</div> </div>
<div className="form-field"> <div
className={classNames("form-field", {
"form-field--error": noTextError,
})}
>
<div className="form-field__wrapper form-field__wrapper--shadowed"> <div className="form-field__wrapper form-field__wrapper--shadowed">
<textarea <textarea
className="text-input text-sm form-field__control " className="text-input text-sm form-field__control "
...@@ -88,12 +105,17 @@ const AddAnnouncementForm = ({ className }) => { ...@@ -88,12 +105,17 @@ const AddAnnouncementForm = ({ className }) => {
onChange={onTextInput} onChange={onTextInput}
></textarea> ></textarea>
</div> </div>
{noTextError && (
<div className="form-field__error">
Před přidáním oznámení nezapomeňte vyplnit jeho obsah.
</div>
)}
</div> </div>
<div <div
className={classNames("form-field", { className={classNames("form-field", {
hidden: type !== "voting", hidden: type !== "voting",
"form-field--error": !!link && !linkValid, "form-field--error": linkValid === false,
})} })}
> >
<div className="form-field__wrapper form-field__wrapper--shadowed"> <div className="form-field__wrapper form-field__wrapper--shadowed">
...@@ -108,7 +130,7 @@ const AddAnnouncementForm = ({ className }) => { ...@@ -108,7 +130,7 @@ const AddAnnouncementForm = ({ className }) => {
<i className="ico--link1"></i> <i className="ico--link1"></i>
</div> </div>
</div> </div>
{!!link && !linkValid && ( {linkValid === false && (
<div className="form-field__error">Zadejte platnou URL.</div> <div className="form-field__error">Zadejte platnou URL.</div>
)} )}
</div> </div>
...@@ -119,7 +141,7 @@ const AddAnnouncementForm = ({ className }) => { ...@@ -119,7 +141,7 @@ const AddAnnouncementForm = ({ className }) => {
className="text-sm mt-4" className="text-sm mt-4"
hoverActive hoverActive
loading={adding} loading={adding}
disabled={!text || (type === "voting" && !linkValid) || adding} disabled={adding}
> >
Přidat oznámení Přidat oznámení
</Button> </Button>
......
import React, { useState } from "react"; import React, { useState } from "react";
import classNames from "classnames";
import { addPost, addProposal } from "actions/posts"; import { addPost, addProposal } from "actions/posts";
import Button from "components/Button"; import Button from "components/Button";
...@@ -7,6 +8,8 @@ import { useActionState } from "hooks"; ...@@ -7,6 +8,8 @@ import { useActionState } from "hooks";
const AddPostForm = ({ className }) => { const AddPostForm = ({ className }) => {
const [text, setText] = useState(""); const [text, setText] = useState("");
const [type, setType] = useState("post");
const [noTextError, setNoTextError] = useState(false);
const [addingPost, addingPostError] = useActionState(addPost, { const [addingPost, addingPostError] = useActionState(addPost, {
content: text, content: text,
}); });
...@@ -16,37 +19,55 @@ const AddPostForm = ({ className }) => { ...@@ -16,37 +19,55 @@ const AddPostForm = ({ className }) => {
const onTextInput = (evt) => { const onTextInput = (evt) => {
setText(evt.target.value); setText(evt.target.value);
if (evt.target.value !== "") {
setNoTextError(false);
}
}; };
const onAddPost = async (evt) => { const onAdd = async (evt) => {
if (!!text) { if (!!text) {
const result = await addPost.run({ content: text }); const result = await (type === "post" ? addPost : addProposal).run({
content: text,
});
if (!result.error) { if (!result.error) {
setText(""); setText("");
} }
} else {
setNoTextError(true);
} }
}; };
const onAddProposal = async (evt) => { const setTypePost = (evt) => {
evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
setType("post");
if (!!text) { };
const result = await addProposal.run({ content: text }); const setTypeProposal = (evt) => {
evt.preventDefault();
if (!result.error) { evt.stopPropagation();
setText(""); setType("procedure-proposal");
}
}
}; };
const buttonDropdownActionList = ( const buttonDropdownActionList = (
<ul className="dropdown-button__choices bg-white text-black whitespace-no-wrap"> <ul className="dropdown-button__choices bg-white text-black whitespace-no-wrap">
<li className="dropdown-button__choice hover:bg-grey-125"> {type === "post" && (
<span className="block px-4 py-3" onClick={onAddProposal}> <li
Navrhnout postup className="dropdown-button__choice hover:bg-grey-125"
</span> onClick={setTypeProposal}
</li> >
<span className="block px-4 py-3">Navrhnout postup</span>
</li>
)}
{type === "procedure-proposal" && (
<li
className="dropdown-button__choice hover:bg-grey-125"
onClick={setTypePost}
>
<span className="block px-4 py-3">Přidat příspěvek</span>
</li>
)}
</ul> </ul>
); );
...@@ -62,7 +83,11 @@ const AddPostForm = ({ className }) => { ...@@ -62,7 +83,11 @@ const AddPostForm = ({ className }) => {
Při přidávání příspěvku došlo k problému: {addingProposalError}. Při přidávání příspěvku došlo k problému: {addingProposalError}.
</ErrorMessage> </ErrorMessage>
)} )}
<div className="form-field"> <div
className={classNames("form-field", {
"form-field--error": noTextError,
})}
>
<div className="form-field__wrapper form-field__wrapper--shadowed"> <div className="form-field__wrapper form-field__wrapper--shadowed">
<textarea <textarea
className="text-input form-field__control " className="text-input form-field__control "
...@@ -73,19 +98,25 @@ const AddPostForm = ({ className }) => { ...@@ -73,19 +98,25 @@ const AddPostForm = ({ className }) => {
onChange={onTextInput} onChange={onTextInput}
></textarea> ></textarea>
</div> </div>
{noTextError && (
<div className="form-field__error">
Před přidáním příspěvku nezapomeňte vyplnit jeho obsah.
</div>
)}
</div> </div>
<div className="space-x-4"> <div className="space-x-4">
<Button <Button
onClick={onAddPost} onClick={onAdd}
disabled={!text || addingPost || addingProposal} disabled={addingPost || addingProposal}
loading={addingPost || addingProposal} loading={addingPost || addingProposal}
hoverActive hoverActive
icon="ico--chevron-down" icon="ico--chevron-down"
iconWrapperClassName="dropdown-button" iconWrapperClassName="dropdown-button"
iconChildren={buttonDropdownActionList} iconChildren={buttonDropdownActionList}
> >
Přidat příspěvek {type === "post" && "Přidat příspěvek"}
{type === "procedure-proposal" && "Navrhnout postup"}
</Button> </Button>
<span className="text-sm text-grey-200 hidden lg:inline"> <span className="text-sm text-grey-200 hidden lg:inline">
......
...@@ -15,6 +15,8 @@ import { AnnouncementStore, AuthStore } from "stores"; ...@@ -15,6 +15,8 @@ import { AnnouncementStore, AuthStore } from "stores";
const AnnoucementsContainer = () => { const AnnoucementsContainer = () => {
const [itemToEdit, setItemToEdit] = useState(null); const [itemToEdit, setItemToEdit] = useState(null);
const [confirmingEdit, setConfirmingEdit] = useState(false);
const [editError, setEditError] = useState(null);
const { 2: loadResult } = loadAnnouncements.useWatch(); const { 2: loadResult } = loadAnnouncements.useWatch();
const [ const [
...@@ -37,8 +39,21 @@ const AnnoucementsContainer = () => { ...@@ -37,8 +39,21 @@ const AnnoucementsContainer = () => {
const confirmEdit = useCallback( const confirmEdit = useCallback(
async (newContent) => { async (newContent) => {
if (itemToEdit && newContent) { if (itemToEdit && newContent) {
await updateAnnouncementContent.run({ item: itemToEdit, newContent }); setConfirmingEdit(true);
setItemToEdit(null);
const result = await updateAnnouncementContent.run({
item: itemToEdit,
newContent,
});
if (!result.error) {
setItemToEdit(null);
setEditError(null);
} else {
setEditError(result.message);
}
setConfirmingEdit(false);
} }
}, },
[itemToEdit, setItemToEdit] [itemToEdit, setItemToEdit]
...@@ -91,6 +106,8 @@ const AnnoucementsContainer = () => { ...@@ -91,6 +106,8 @@ const AnnoucementsContainer = () => {
announcement={itemToEdit} announcement={itemToEdit}
onConfirm={confirmEdit} onConfirm={confirmEdit}
onCancel={cancelEdit} onCancel={cancelEdit}
confirming={confirmingEdit}
error={editError}
/> />
)} )}
</> </>
......
...@@ -4,6 +4,7 @@ import pick from "lodash/pick"; ...@@ -4,6 +4,7 @@ import pick from "lodash/pick";
import { import {
acceptProposal, acceptProposal,
announceProposal, announceProposal,
archive,
dislike, dislike,
edit, edit,
hide, hide,
...@@ -22,6 +23,8 @@ import { AuthStore, PostStore } from "stores"; ...@@ -22,6 +23,8 @@ import { AuthStore, PostStore } from "stores";
const PostsContainer = ({ className }) => { const PostsContainer = ({ className }) => {
const [postToEdit, setPostToEdit] = useState(null); const [postToEdit, setPostToEdit] = useState(null);
const [confirmingEdit, setConfirmingEdit] = useState(false);
const [editError, setEditError] = useState(null);
const [ const [
userToBan, userToBan,
...@@ -35,6 +38,12 @@ const PostsContainer = ({ className }) => { ...@@ -35,6 +38,12 @@ const PostsContainer = ({ className }) => {
onPostHideConfirm, onPostHideConfirm,
onPostHideCancel, onPostHideCancel,
] = useItemActionConfirm(hide); ] = useItemActionConfirm(hide);
const [
postToArchive,
setPostToArchive,
onPostArchiveConfirm,
onPostArchiveCancel,
] = useItemActionConfirm(archive);
const [ const [
postToAnnounce, postToAnnounce,
setPostToAnnounce, setPostToAnnounce,
...@@ -70,6 +79,10 @@ const PostsContainer = ({ className }) => { ...@@ -70,6 +79,10 @@ const PostsContainer = ({ className }) => {
const [banningUser, banningUserError] = useActionState(ban, userToBan); const [banningUser, banningUserError] = useActionState(ban, userToBan);
const [hidingPost, hidingPostError] = useActionState(hide, postToHide); const [hidingPost, hidingPostError] = useActionState(hide, postToHide);
const [archivingPost, archivingPostError] = useActionState(
archive,
postToArchive
);
const [announcingProposal, announcingProposalError] = useActionState( const [announcingProposal, announcingProposalError] = useActionState(
announceProposal, announceProposal,
postToAnnounce postToAnnounce
...@@ -92,8 +105,18 @@ const PostsContainer = ({ className }) => { ...@@ -92,8 +105,18 @@ const PostsContainer = ({ className }) => {
const confirmEdit = useCallback( const confirmEdit = useCallback(
async (newContent) => { async (newContent) => {
if (postToEdit && newContent) { if (postToEdit && newContent) {
await edit.run({ post: postToEdit, newContent }); setConfirmingEdit(true);
setPostToEdit(null);
const result = await edit.run({ post: postToEdit, newContent });
if (!result.error) {
setPostToEdit(null);
setEditError(null);
} else {
setEditError(result.message);
}
setConfirmingEdit(false);
} }
}, },
[postToEdit, setPostToEdit] [postToEdit, setPostToEdit]
...@@ -145,6 +168,7 @@ const PostsContainer = ({ className }) => { ...@@ -145,6 +168,7 @@ const PostsContainer = ({ className }) => {
onHide={setPostToHide} onHide={setPostToHide}
onBanUser={onBanUser} onBanUser={onBanUser}
onEdit={setPostToEdit} onEdit={setPostToEdit}
onArchive={setPostToArchive}
onAnnounceProcedureProposal={setPostToAnnounce} onAnnounceProcedureProposal={setPostToAnnounce}
onAcceptProcedureProposal={setPostToAccept} onAcceptProcedureProposal={setPostToAccept}
onRejectProcedureProposal={setPostToReject} onRejectProcedureProposal={setPostToReject}
...@@ -173,6 +197,18 @@ const PostsContainer = ({ className }) => { ...@@ -173,6 +197,18 @@ const PostsContainer = ({ className }) => {
> >
Příspěvek se skryje a uživatelé ho neuvidí. Opravdu to chcete? Příspěvek se skryje a uživatelé ho neuvidí. Opravdu to chcete?
</ModalConfirm> </ModalConfirm>
<ModalConfirm
isOpen={!!postToArchive}
onConfirm={onPostArchiveConfirm}
onCancel={onPostArchiveCancel}
confirming={archivingPost}
error={archivingPostError}
title="Archivovat příspěvek?"
yesActionLabel="Potvrdit"
>
Příspěvek bude archivován a bude ve výpisu vizuálně odlišen. Opravdu to
chcete?
</ModalConfirm>
<ModalConfirm <ModalConfirm
isOpen={!!postToAnnounce} isOpen={!!postToAnnounce}
onConfirm={onAnnounceConfirm} onConfirm={onAnnounceConfirm}
...@@ -224,6 +260,8 @@ const PostsContainer = ({ className }) => { ...@@ -224,6 +260,8 @@ const PostsContainer = ({ className }) => {
post={postToEdit} post={postToEdit}
onConfirm={confirmEdit} onConfirm={confirmEdit}
onCancel={cancelEdit} onCancel={cancelEdit}
confirming={confirmingEdit}
error={editError}
/> />
)} )}
</> </>
......
...@@ -30,6 +30,10 @@ export const handlePostChanged = (payload) => { ...@@ -30,6 +30,10 @@ export const handlePostChanged = (payload) => {
if (has(payload, "state")) { if (has(payload, "state")) {
state.items[payload.id].state = postsStateMapping[payload.state]; state.items[payload.id].state = postsStateMapping[payload.state];
} }
if (has(payload, "is_archived")) {
state.items[payload.id].archived = payload.is_archived;
}
} }
}); });
}; };
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment