diff --git a/package-lock.json b/package-lock.json index db6b3447ef3420d528ca3246732c68f2713105ed..ae8c27a2b00569114e537719e3a120124ff84acb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11347,6 +11347,11 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "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": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.12.1.tgz", @@ -12596,6 +12601,63 @@ "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", diff --git a/package.json b/package.json index 3b0975013e5815096adb5a70b33f59519b168ef4..fe1ec7035b92d15b03b5b46660db6d8b40fcbd27 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,11 @@ "react-device-detect": "^1.13.1", "react-dom": "^16.13.1", "react-intersection-observer": "^8.31.0", + "react-mde": "^11.0.0", "react-modal": "^3.12.1", "react-router-dom": "^5.2.0", "react-scripts": "3.4.3", + "showdown": "^1.9.1", "unfetch": "^4.2.0", "wait-queue": "^1.1.4" }, diff --git a/src/actions/announcements.js b/src/actions/announcements.js index 738ef521f3b6f448fa05c13dbffbd46ce1b0d3fd..cb99ea52220d8b0d8460ed11e041849f0ab2c7fa 100644 --- a/src/actions/announcements.js +++ b/src/actions/announcements.js @@ -45,7 +45,11 @@ export const addAnnouncement = createAsyncAction( link, 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(); return successResult(data.data); } catch (err) { @@ -64,7 +68,10 @@ export const deleteAnnouncement = createAsyncAction( */ async (item) => { try { - await fetch(`/announcements/${item.id}`, { method: "DELETE" }); + await fetch(`/announcements/${item.id}`, { + method: "DELETE", + expectedStatus: 204, + }); return successResult({ item }); } catch (err) { return errorResult([], err.toString()); @@ -86,7 +93,11 @@ export const updateAnnouncementContent = createAsyncAction( const body = JSON.stringify({ content: newContent, }); - await fetch(`/announcements/${item.id}`, { method: "PUT", body }); + await fetch(`/announcements/${item.id}`, { + method: "PUT", + body, + expectedStatus: 204, + }); return successResult({ item, newContent }); } catch (err) { return errorResult([], err.toString()); diff --git a/src/actions/posts.js b/src/actions/posts.js index f2ca0a308d0785d0dd2eb103858a5a13b13ce20d..2e6d123a0692abb207466f051abb7b87bd3336ea 100644 --- a/src/actions/posts.js +++ b/src/actions/posts.js @@ -14,7 +14,7 @@ import { export const loadPosts = createAsyncAction( async () => { try { - const resp = await fetch("/posts"); + const resp = await fetch("/posts", { expectedStatus: 200 }); const data = await resp.json(); return successResult(data.data); } catch (err) { @@ -23,7 +23,7 @@ export const loadPosts = createAsyncAction( }, { postActionHook: ({ result }) => { - if (!result.error) { + if (!result.error && result.payload) { const posts = result.payload.map(parseRawPost); PostStore.update((state) => { @@ -49,7 +49,10 @@ export const like = createAsyncAction( */ async (post) => { try { - await fetch(`/posts/${post.id}/like`, { method: "PATCH" }); + await fetch(`/posts/${post.id}/like`, { + method: "PATCH", + expectedStatus: 204, + }); return successResult(post); } catch (err) { return errorResult([], err.toString()); @@ -72,7 +75,10 @@ export const dislike = createAsyncAction( */ async (post) => { try { - await fetch(`/posts/${post.id}/dislike`, { method: "PATCH" }); + await fetch(`/posts/${post.id}/dislike`, { + method: "PATCH", + expectedStatus: 204, + }); return successResult(post); } catch (err) { return errorResult([], err.toString()); @@ -98,7 +104,7 @@ export const addPost = createAsyncAction(async ({ content }) => { content, type: postsTypeMappingRev["post"], }); - await fetch(`/posts`, { method: "POST", body }); + await fetch(`/posts`, { method: "POST", body, expectedStatus: 201 }); return successResult(); } catch (err) { return errorResult([], err.toString()); @@ -114,7 +120,7 @@ export const addProposal = createAsyncAction(async ({ content }) => { content, type: postsTypeMappingRev["procedure-proposal"], }); - await fetch(`/posts`, { method: "POST", body }); + await fetch(`/posts`, { method: "POST", body, expectedStatus: 201 }); return successResult(); } catch (err) { return errorResult([], err.toString()); @@ -154,7 +160,35 @@ export const edit = createAsyncAction( const body = JSON.stringify({ 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(); } catch (err) { return errorResult([], err.toString()); @@ -171,7 +205,11 @@ const updateProposalState = async (proposal, state) => { const body = JSON.stringify({ state: postsStateMappingRev[state], }); - await fetch(`/posts/${proposal.id}`, { method: "PUT", body }); + await fetch(`/posts/${proposal.id}`, { + method: "PUT", + body, + expectedStatus: 204, + }); return successResult(proposal); }; diff --git a/src/actions/program.js b/src/actions/program.js index be1612589476f7c3da615978fdd04e14acf9bd42..56a3f18073c62c48eef338cb6590a1d40b42e275 100644 --- a/src/actions/program.js +++ b/src/actions/program.js @@ -71,7 +71,11 @@ export const renameProgramPoint = createAsyncAction( const body = JSON.stringify({ title: newTitle, }); - await fetch(`/program/${programEntry.id}`, { method: "PUT", body }); + await fetch(`/program/${programEntry.id}`, { + method: "PUT", + body, + expectedStatus: 204, + }); return successResult({ programEntry, newTitle }); } catch (err) { return errorResult([], err.toString()); @@ -104,7 +108,11 @@ export const endProgramPoint = createAsyncAction( const body = JSON.stringify({ is_live: false, }); - await fetch(`/program/${programEntry.id}`, { method: "PUT", body }); + await fetch(`/program/${programEntry.id}`, { + method: "PUT", + body, + expectedStatus: 204, + }); return successResult(programEntry); } catch (err) { return errorResult([], err.toString()); @@ -134,7 +142,11 @@ export const activateProgramPoint = createAsyncAction( const body = JSON.stringify({ is_live: true, }); - await fetch(`/program/${programEntry.id}`, { method: "PUT", body }); + await fetch(`/program/${programEntry.id}`, { + method: "PUT", + body, + expectedStatus: 204, + }); return successResult(programEntry); } catch (err) { return errorResult([], err.toString()); @@ -167,7 +179,11 @@ export const openDiscussion = createAsyncAction( const body = JSON.stringify({ discussion_opened: true, }); - await fetch(`/program/${programEntry.id}`, { method: "PUT", body }); + await fetch(`/program/${programEntry.id}`, { + method: "PUT", + body, + expectedStatus: 204, + }); return successResult(programEntry); } catch (err) { return errorResult([], err.toString()); @@ -195,7 +211,11 @@ export const closeDiscussion = createAsyncAction( const body = JSON.stringify({ discussion_opened: false, }); - await fetch(`/program/${programEntry.id}`, { method: "PUT", body }); + await fetch(`/program/${programEntry.id}`, { + method: "PUT", + body, + expectedStatus: 204, + }); return successResult(programEntry); } catch (err) { return errorResult([], err.toString()); diff --git a/src/api.js b/src/api.js index c7d5efd34840b6f275fe913ab5c692ccf527a548..8bfc444b5d16023621fa24725168fe293fbc7628 100644 --- a/src/api.js +++ b/src/api.js @@ -2,19 +2,29 @@ import baseFetch from "unfetch"; 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(); - opts = opts || {}; - opts.headers = opts.headers || {}; - 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"]) { - opts.headers["Content-Type"] = "application/json"; + const response = await baseFetch(process.env.REACT_APP_API_BASE_URL + url, { + 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; }; diff --git a/src/components/MarkdownEditor.jsx b/src/components/MarkdownEditor.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1379fe5a176ad1317c57299f502a7ab0c1bd708a --- /dev/null +++ b/src/components/MarkdownEditor.jsx @@ -0,0 +1,26 @@ +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; diff --git a/src/components/annoucements/AnnouncementEditModal.jsx b/src/components/annoucements/AnnouncementEditModal.jsx index df5d583f03c86f6f4c33377138355f9a5a36f281..c0bed97aeb03f8044cdaa4ea0e78dc39de345527 100644 --- a/src/components/annoucements/AnnouncementEditModal.jsx +++ b/src/components/annoucements/AnnouncementEditModal.jsx @@ -2,12 +2,15 @@ import React, { useState } from "react"; import Button from "components/Button"; import { Card, CardActions, CardBody, CardHeadline } from "components/cards"; +import ErrorMessage from "components/ErrorMessage"; import Modal from "components/modals/Modal"; const AnnouncementEditModal = ({ announcement, onCancel, onConfirm, + confirming, + error, ...props }) => { const [text, setText] = useState(announcement.content); @@ -46,12 +49,18 @@ const AnnouncementEditModal = ({ ></textarea> </div> </div> + {error && ( + <ErrorMessage className="mt-2"> + Při editaci došlo k problému: {error} + </ErrorMessage> + )} </CardBody> <CardActions right className="space-x-1"> <Button hoverActive color="blue-300" className="text-sm" + loading={confirming} onClick={confirm} > Uložit diff --git a/src/components/modals/ModalConfirm.jsx b/src/components/modals/ModalConfirm.jsx index f0cfe1e8c6e6e6b150a6011d6b707f76cb8db89d..8efbbeca96411f6b520072813b5e1eaf8d936be1 100644 --- a/src/components/modals/ModalConfirm.jsx +++ b/src/components/modals/ModalConfirm.jsx @@ -36,7 +36,7 @@ const ModalConfirm = ({ <CardBodyText>{children}</CardBodyText> {error && ( <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> )} </CardBody> @@ -64,4 +64,4 @@ const ModalConfirm = ({ ); }; -export default ModalConfirm; +export default React.memo(ModalConfirm); diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx index cd67cbf999a40e781da354b101b85165c7934223..72ed9925703251ea0ab18032730d8a93f37db9a6 100644 --- a/src/components/posts/Post.jsx +++ b/src/components/posts/Post.jsx @@ -30,6 +30,7 @@ const Post = ({ onRejectProcedureProposal, onRejectProcedureProposalByChairman, onEdit, + onArchive, onSeen, }) => { const { ref, inView } = useInView({ @@ -132,6 +133,7 @@ const Post = ({ const showEditAction = true; const showBanAction = true; const showHideAction = !archived; + const showArchiveAction = !archived; return ( <div className={wrapperClassName} ref={ref}> @@ -222,6 +224,13 @@ const Post = ({ title="Skrýt příspěvek" /> )} + {showArchiveAction && ( + <DropdownMenuItem + onClick={onArchive} + icon="ico--drawer" + title="Archivovat příspěvek" + /> + )} </DropdownMenu> )} </div> diff --git a/src/components/posts/PostEditModal.jsx b/src/components/posts/PostEditModal.jsx index b801099744e01f08b6de40275ce1b012ffd4b3dc..72b5c0c3cdc09da0ff1c415857196c87f75977a1 100644 --- a/src/components/posts/PostEditModal.jsx +++ b/src/components/posts/PostEditModal.jsx @@ -2,9 +2,17 @@ import React, { useState } from "react"; import Button from "components/Button"; import { Card, CardActions, CardBody, CardHeadline } from "components/cards"; +import ErrorMessage from "components/ErrorMessage"; 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 onTextInput = (evt) => { @@ -41,12 +49,18 @@ const PostEditModal = ({ post, onCancel, onConfirm, ...props }) => { ></textarea> </div> </div> + {error && ( + <ErrorMessage className="mt-2"> + Při editaci došlo k problému: {error} + </ErrorMessage> + )} </CardBody> <CardActions right className="space-x-1"> <Button hoverActive color="blue-300" className="text-sm" + loading={confirming} onClick={confirm} > Uložit diff --git a/src/components/posts/PostList.jsx b/src/components/posts/PostList.jsx index 6faf9a2750c77a6481bb501a95c5b282b4e2c977..c8143ece2ba6ffa9a911990b0ab0289ee7f93482 100644 --- a/src/components/posts/PostList.jsx +++ b/src/components/posts/PostList.jsx @@ -17,6 +17,7 @@ const PostList = ({ onRejectProcedureProposal, onRejectProcedureProposalByChairman, onEdit, + onArchive, onSeen, dimArchived, }) => { @@ -30,6 +31,7 @@ const PostList = ({ const onPostEdit = buildHandler(onEdit); const onPostHide = buildHandler(onHide); const onPostBanUser = buildHandler(onBanUser); + const onPostArchive = buildHandler(onArchive); const onPostAnnounceProcedureProposal = buildHandler( onAnnounceProcedureProposal ); @@ -74,6 +76,7 @@ const PostList = ({ item )} onEdit={onPostEdit(item)} + onArchive={onPostArchive(item)} onSeen={onPostSeen(item)} /> ))} diff --git a/src/containers/AddAnnouncementForm.jsx b/src/containers/AddAnnouncementForm.jsx index fdc732ecf6dae2a446eb04a0f0a766bc33507242..b83dfdd9948c2ec8a8c60a3c340e21b036091f0e 100644 --- a/src/containers/AddAnnouncementForm.jsx +++ b/src/containers/AddAnnouncementForm.jsx @@ -11,7 +11,8 @@ const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()] const AddAnnouncementForm = ({ className }) => { const [text, setText] = 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 [adding, addingError] = useActionState(addAnnouncement, { @@ -22,24 +23,36 @@ const AddAnnouncementForm = ({ className }) => { const onTextInput = (evt) => { setText(evt.target.value); + + if (evt.target.value !== "") { + setNoTextError(false); + } }; const onLinkInput = (evt) => { setLink(evt.target.value); if (!!evt.target.value) { - setLinkValid(evt.target.value.match(urlRegex)); + setLinkValid(!!evt.target.value.match(urlRegex)); } }; const onAdd = async (evt) => { + if (!link) { + setLinkValid(false); + } + 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) { - setText(""); - setLink(""); + if (!result.error) { + setText(""); + setLink(""); + } } + } else { + setNoTextError(true); } }; @@ -77,7 +90,11 @@ const AddAnnouncementForm = ({ className }) => { </div> </div> - <div className="form-field"> + <div + className={classNames("form-field", { + "form-field--error": noTextError, + })} + > <div className="form-field__wrapper form-field__wrapper--shadowed"> <textarea className="text-input text-sm form-field__control " @@ -88,12 +105,17 @@ const AddAnnouncementForm = ({ className }) => { onChange={onTextInput} ></textarea> </div> + {noTextError && ( + <div className="form-field__error"> + Před přidáním oznámení nezapomeňte vyplnit jeho obsah. + </div> + )} </div> <div className={classNames("form-field", { hidden: type !== "voting", - "form-field--error": !!link && !linkValid, + "form-field--error": linkValid === false, })} > <div className="form-field__wrapper form-field__wrapper--shadowed"> @@ -108,7 +130,7 @@ const AddAnnouncementForm = ({ className }) => { <i className="ico--link1"></i> </div> </div> - {!!link && !linkValid && ( + {linkValid === false && ( <div className="form-field__error">Zadejte platnou URL.</div> )} </div> @@ -119,7 +141,7 @@ const AddAnnouncementForm = ({ className }) => { className="text-sm mt-4" hoverActive loading={adding} - disabled={!text || (type === "voting" && !linkValid) || adding} + disabled={adding} > Přidat oznámení </Button> diff --git a/src/containers/AddPostForm.jsx b/src/containers/AddPostForm.jsx index dc2d90b6acd0f569323e8af8b2a9ab2e38ed290c..667278372f7f3d9b2292f89f66097261c54c4b77 100644 --- a/src/containers/AddPostForm.jsx +++ b/src/containers/AddPostForm.jsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import classNames from "classnames"; import { addPost, addProposal } from "actions/posts"; import Button from "components/Button"; @@ -7,6 +8,8 @@ import { useActionState } from "hooks"; const AddPostForm = ({ className }) => { const [text, setText] = useState(""); + const [type, setType] = useState("post"); + const [noTextError, setNoTextError] = useState(false); const [addingPost, addingPostError] = useActionState(addPost, { content: text, }); @@ -16,37 +19,55 @@ const AddPostForm = ({ className }) => { const onTextInput = (evt) => { setText(evt.target.value); + + if (evt.target.value !== "") { + setNoTextError(false); + } }; - const onAddPost = async (evt) => { + const onAdd = async (evt) => { if (!!text) { - const result = await addPost.run({ content: text }); + const result = await (type === "post" ? addPost : addProposal).run({ + content: text, + }); if (!result.error) { setText(""); } + } else { + setNoTextError(true); } }; - const onAddProposal = async (evt) => { + const setTypePost = (evt) => { + evt.preventDefault(); evt.stopPropagation(); - - if (!!text) { - const result = await addProposal.run({ content: text }); - - if (!result.error) { - setText(""); - } - } + setType("post"); + }; + const setTypeProposal = (evt) => { + evt.preventDefault(); + evt.stopPropagation(); + setType("procedure-proposal"); }; const buttonDropdownActionList = ( <ul className="dropdown-button__choices bg-white text-black whitespace-no-wrap"> - <li className="dropdown-button__choice hover:bg-grey-125"> - <span className="block px-4 py-3" onClick={onAddProposal}> - Navrhnout postup - </span> - </li> + {type === "post" && ( + <li + className="dropdown-button__choice hover:bg-grey-125" + onClick={setTypeProposal} + > + <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> ); @@ -62,7 +83,11 @@ const AddPostForm = ({ className }) => { Při přidávání příspěvku došlo k problému: {addingProposalError}. </ErrorMessage> )} - <div className="form-field"> + <div + className={classNames("form-field", { + "form-field--error": noTextError, + })} + > <div className="form-field__wrapper form-field__wrapper--shadowed"> <textarea className="text-input form-field__control " @@ -73,19 +98,25 @@ const AddPostForm = ({ className }) => { onChange={onTextInput} ></textarea> </div> + {noTextError && ( + <div className="form-field__error"> + Před přidáním příspěvku nezapomeňte vyplnit jeho obsah. + </div> + )} </div> <div className="space-x-4"> <Button - onClick={onAddPost} - disabled={!text || addingPost || addingProposal} + onClick={onAdd} + disabled={addingPost || addingProposal} loading={addingPost || addingProposal} hoverActive icon="ico--chevron-down" iconWrapperClassName="dropdown-button" iconChildren={buttonDropdownActionList} > - Přidat příspěvek + {type === "post" && "Přidat příspěvek"} + {type === "procedure-proposal" && "Navrhnout postup"} </Button> <span className="text-sm text-grey-200 hidden lg:inline"> diff --git a/src/containers/AnnoucementsContainer.jsx b/src/containers/AnnoucementsContainer.jsx index e8d6c04fe61cd8532f0e07eba521e1e6b5874bd2..cd8da9c91daf71ef321b31b5ad6761d5aa8fa692 100644 --- a/src/containers/AnnoucementsContainer.jsx +++ b/src/containers/AnnoucementsContainer.jsx @@ -15,6 +15,8 @@ import { AnnouncementStore, AuthStore } from "stores"; const AnnoucementsContainer = () => { const [itemToEdit, setItemToEdit] = useState(null); + const [confirmingEdit, setConfirmingEdit] = useState(false); + const [editError, setEditError] = useState(null); const { 2: loadResult } = loadAnnouncements.useWatch(); const [ @@ -37,8 +39,21 @@ const AnnoucementsContainer = () => { const confirmEdit = useCallback( async (newContent) => { if (itemToEdit && newContent) { - await updateAnnouncementContent.run({ item: itemToEdit, newContent }); - setItemToEdit(null); + setConfirmingEdit(true); + + const result = await updateAnnouncementContent.run({ + item: itemToEdit, + newContent, + }); + + if (!result.error) { + setItemToEdit(null); + setEditError(null); + } else { + setEditError(result.message); + } + + setConfirmingEdit(false); } }, [itemToEdit, setItemToEdit] @@ -91,6 +106,8 @@ const AnnoucementsContainer = () => { announcement={itemToEdit} onConfirm={confirmEdit} onCancel={cancelEdit} + confirming={confirmingEdit} + error={editError} /> )} </> diff --git a/src/containers/PostsContainer.jsx b/src/containers/PostsContainer.jsx index 936a3c6137aec03b21999f5682ae0c613baf9ddb..0efdc0c551a718013579cedbddf6daff544357df 100644 --- a/src/containers/PostsContainer.jsx +++ b/src/containers/PostsContainer.jsx @@ -4,6 +4,7 @@ import pick from "lodash/pick"; import { acceptProposal, announceProposal, + archive, dislike, edit, hide, @@ -22,6 +23,8 @@ import { AuthStore, PostStore } from "stores"; const PostsContainer = ({ className }) => { const [postToEdit, setPostToEdit] = useState(null); + const [confirmingEdit, setConfirmingEdit] = useState(false); + const [editError, setEditError] = useState(null); const [ userToBan, @@ -35,6 +38,12 @@ const PostsContainer = ({ className }) => { onPostHideConfirm, onPostHideCancel, ] = useItemActionConfirm(hide); + const [ + postToArchive, + setPostToArchive, + onPostArchiveConfirm, + onPostArchiveCancel, + ] = useItemActionConfirm(archive); const [ postToAnnounce, setPostToAnnounce, @@ -70,6 +79,10 @@ const PostsContainer = ({ className }) => { const [banningUser, banningUserError] = useActionState(ban, userToBan); const [hidingPost, hidingPostError] = useActionState(hide, postToHide); + const [archivingPost, archivingPostError] = useActionState( + archive, + postToArchive + ); const [announcingProposal, announcingProposalError] = useActionState( announceProposal, postToAnnounce @@ -92,8 +105,18 @@ const PostsContainer = ({ className }) => { const confirmEdit = useCallback( async (newContent) => { if (postToEdit && newContent) { - await edit.run({ post: postToEdit, newContent }); - setPostToEdit(null); + setConfirmingEdit(true); + + const result = await edit.run({ post: postToEdit, newContent }); + + if (!result.error) { + setPostToEdit(null); + setEditError(null); + } else { + setEditError(result.message); + } + + setConfirmingEdit(false); } }, [postToEdit, setPostToEdit] @@ -145,6 +168,7 @@ const PostsContainer = ({ className }) => { onHide={setPostToHide} onBanUser={onBanUser} onEdit={setPostToEdit} + onArchive={setPostToArchive} onAnnounceProcedureProposal={setPostToAnnounce} onAcceptProcedureProposal={setPostToAccept} onRejectProcedureProposal={setPostToReject} @@ -173,6 +197,18 @@ const PostsContainer = ({ className }) => { > Příspěvek se skryje a uživatelé ho neuvidí. Opravdu to chcete? </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 isOpen={!!postToAnnounce} onConfirm={onAnnounceConfirm} @@ -224,6 +260,8 @@ const PostsContainer = ({ className }) => { post={postToEdit} onConfirm={confirmEdit} onCancel={cancelEdit} + confirming={confirmingEdit} + error={editError} /> )} </> diff --git a/src/ws/handlers/posts.js b/src/ws/handlers/posts.js index a277ed8a0843072a0407132aa444614dbf49df97..3c07ed0677243efddaa841762afffaf82d9c8fcd 100644 --- a/src/ws/handlers/posts.js +++ b/src/ws/handlers/posts.js @@ -30,6 +30,10 @@ export const handlePostChanged = (payload) => { if (has(payload, "state")) { state.items[payload.id].state = postsStateMapping[payload.state]; } + + if (has(payload, "is_archived")) { + state.items[payload.id].archived = payload.is_archived; + } } }); };