Skip to content
Snippets Groups Projects
Commit 2084c1f5 authored by xaralis's avatar xaralis
Browse files

feat: consider auth levels, load and manage annoucements, manage program points, manage posts

parent 9b034be3
No related branches found
No related tags found
No related merge requests found
Pipeline #1847 passed
Showing
with 428 additions and 155 deletions
REACT_APP_STYLEGUIDE_URL=http://localhost:3001
REACT_APP_API_BASE_URL=https://cf2021.pirati.cz/api
REACT_APP_WS_BASE_URL=wss://cf2021.pirati.cz/ws/posts
REACT_APP_WS_BASE_URL=wss://cf2021.pirati.cz/ws
......@@ -27,11 +27,22 @@ const onKeycloakEvent = (event) => {
if (["onAuthRefreshSuccess", "onAuthSuccess"].includes(event)) {
Sentry.setUser(keycloak.tokenParsed);
AuthStore.update((state) => {
const kcRoles = keycloak.tokenParsed.roles;
let role = null;
if (kcRoles.includes("chairman")) {
role = "chairman";
} else if (kcRoles.includes("member")) {
role = "member";
} else {
role = "regp";
}
state.isAuthenticated = true;
state.user = {
name: keycloak.tokenParsed.name,
username: keycloak.tokenParsed.preferred_username,
groups: keycloak.tokenParsed.groups,
role,
accessToken: keycloak.token,
};
});
......
import findIndex from "lodash/findIndex";
import remove from "lodash/remove";
import { createAsyncAction, successResult } from "pullstate";
import keyBy from "lodash/keyBy";
import property from "lodash/property";
import { createAsyncAction, errorResult, successResult } from "pullstate";
import { fetch } from "api";
import { AnnouncementStore } from "stores";
import {
announcementTypeMappingRev,
parseRawAnnouncement,
syncAnnoucementItemIds,
} from "utils";
/**
* Add new announcement.
*/
export const addAnnouncement = createAsyncAction(
async ({ content }) => {
/** @type {CF2021.Announcement} */
const payload = {
id: "999",
datetime: new Date(),
type: "announcement",
content,
seen: true,
};
return successResult(payload);
export const loadAnnouncements = createAsyncAction(
async () => {
try {
const resp = await fetch("/announcements");
const data = await resp.json();
return successResult(data.data);
} catch (err) {
return errorResult([], err.toString());
}
},
{
postActionHook: ({ result }) => {
if (!result.error) {
const announcements = result.payload.map(parseRawAnnouncement);
AnnouncementStore.update((state) => {
state.items.push(result.payload);
state.items = keyBy(announcements, property("id"));
syncAnnoucementItemIds(state);
});
}
},
}
);
/**
* Add new announcement.
*/
export const addAnnouncement = createAsyncAction(async ({ content }) => {
try {
const body = JSON.stringify({
content,
type: announcementTypeMappingRev["announcement"],
});
const resp = await fetch("/announcements", { method: "POST", body });
const data = await resp.json();
return successResult(data.data);
} catch (err) {
return errorResult([], err.toString());
}
});
/**
* Delete existing announcement.
*/
......@@ -40,16 +60,12 @@ export const deleteAnnouncement = createAsyncAction(
* @param {CF2021.Announcement} item
*/
async (item) => {
return successResult(item);
},
{
postActionHook: ({ result }) => {
if (!result.error) {
AnnouncementStore.update((state) => {
remove(state.items, { id: result.payload.id });
});
try {
await fetch(`/announcements/${item.id}`, { method: "DELETE" });
return successResult({ item });
} catch (err) {
return errorResult([], err.toString());
}
},
}
);
......@@ -63,18 +79,14 @@ export const updateAnnouncementContent = createAsyncAction(
* @param {string} newContent
*/
async ({ item, newContent }) => {
return successResult({ item, newContent });
},
{
postActionHook: ({ result }) => {
if (!result.error) {
AnnouncementStore.update((state) => {
const itemIdx = findIndex(state.items, {
id: result.payload.item.id,
});
state.items[itemIdx].content = result.payload.newContent;
try {
const body = JSON.stringify({
content: newContent,
});
await fetch(`/announcements/${item.id}`, { method: "PUT", body });
return successResult({ item, newContent });
} catch (err) {
return errorResult([], err.toString());
}
},
}
);
import { createAsyncAction, errorResult, successResult } from "pullstate";
import fetch from "unfetch";
import { AuthStore } from "stores";
export const loadGroupMappings = createAsyncAction(
async () => {
try {
const resp = await fetch("https://iapi.pirati.cz/v1/groups");
const mappings = await resp.json();
return successResult(mappings);
} catch (err) {
return errorResult([], err.toString());
}
},
{
postActionHook: ({ result }) => {
if (!result.error) {
AuthStore.update((state) => {
state.groupMappings = result.payload;
});
}
},
}
);
......@@ -4,7 +4,12 @@ import { createAsyncAction, errorResult, successResult } from "pullstate";
import { fetch } from "api";
import { PostStore } from "stores";
import { filterPosts, parseRawPost, postsTypeMappingRev } from "utils";
import {
filterPosts,
parseRawPost,
postsStateMappingRev,
postsTypeMappingRev,
} from "utils";
export const loadPosts = createAsyncAction(
async () => {
......@@ -137,6 +142,19 @@ export const hide = createAsyncAction(
}
);
/**
*
* @param {CF2021.ProposalPost} proposal
* @param {CF2021.ProposalPostState} state
*/
const updateProposalState = async (proposal, state) => {
const body = JSON.stringify({
state: postsStateMappingRev[state],
});
await fetch(`/posts/${proposal.id}`, { method: "PUT", body });
return successResult(proposal);
};
/**
* Announce procedure proposal.
*/
......@@ -144,8 +162,8 @@ export const announceProposal = createAsyncAction(
/**
* @param {CF2021.ProposalPost} proposal
*/
async (proposal) => {
return successResult(proposal);
(proposal) => {
return updateProposalState(proposal, "announced");
},
{
shortCircuitHook: ({ args }) => {
......@@ -159,13 +177,6 @@ export const announceProposal = createAsyncAction(
return false;
},
postActionHook: ({ result }) => {
if (!result.error) {
PostStore.update((state) => {
state.items[result.payload.id].state = "announced";
});
}
},
}
);
......@@ -176,8 +187,8 @@ export const acceptProposal = createAsyncAction(
/**
* @param {CF2021.ProposalPost} proposal
*/
async (proposal) => {
return successResult(proposal);
(proposal) => {
return updateProposalState(proposal, "accepted");
},
{
shortCircuitHook: ({ args }) => {
......@@ -191,13 +202,6 @@ export const acceptProposal = createAsyncAction(
return false;
},
postActionHook: ({ result }) => {
if (!result.error) {
PostStore.update((state) => {
state.items[result.payload.id].state = "accepted";
});
}
},
}
);
......@@ -208,8 +212,8 @@ export const rejectProposal = createAsyncAction(
/**
* @param {CF2021.ProposalPost} proposal
*/
async (proposal) => {
return successResult(proposal);
(proposal) => {
return updateProposalState(proposal, "rejected");
},
{
shortCircuitHook: ({ args }) => {
......@@ -223,12 +227,30 @@ export const rejectProposal = createAsyncAction(
return false;
},
postActionHook: ({ result }) => {
if (!result.error) {
PostStore.update((state) => {
state.items[result.payload.id].state = "rejected";
});
}
);
/**
* Reject procedure proposal.
*/
export const rejectProposalByChairman = createAsyncAction(
/**
* @param {CF2021.ProposalPost} proposal
*/
(proposal) => {
return updateProposalState(proposal, "rejected-by-chairman");
},
{
shortCircuitHook: ({ args }) => {
if (args.type !== "procedure-proposal") {
return errorResult();
}
if (!["pending", "announced"].includes(args.state)) {
return errorResult();
}
return false;
},
}
);
import keyBy from "lodash/keyBy";
import pick from "lodash/pick";
import property from "lodash/property";
import { createAsyncAction, errorResult, successResult } from "pullstate";
import { fetch } from "api";
import { ProgramStore } from "stores";
import { loadPosts } from "./posts";
export const loadProgram = createAsyncAction(
async () => {
try {
......@@ -33,6 +37,7 @@ export const loadProgram = createAsyncAction(
"description",
"proposer",
]),
discussionOpened: entry.discussion_opened,
expectedStartAt: new Date(entry.expected_start_at),
expectedFinishAt: entry.expected_finish_at
? new Date(entry.expected_finish_at)
......@@ -45,15 +50,40 @@ export const loadProgram = createAsyncAction(
const currentEntry = result.payload.find((entry) => entry.is_live);
ProgramStore.update((state) => {
state.schedule = entries;
state.items = keyBy(entries, property("id"));
state.scheduleIds = entries.map((entry) => entry.id);
if (currentEntry) {
state.current = state.schedule.find(
(scheduleEntry) => scheduleEntry.id === currentEntry.id
state.currentId = currentEntry.id;
}
});
}
},
}
);
} else {
// TODO: for testing only
state.current = state.schedule[1];
/**
* Rename program point.
*/
export const renameProgramPoint = createAsyncAction(
async ({ programEntry, newTitle }) => {
try {
const body = JSON.stringify({
title: newTitle,
});
await fetch(`/program/${programEntry.id}`, { method: "PUT", body });
return successResult({ programEntry, newTitle });
} catch (err) {
return errorResult([], err.toString());
}
},
{
postActionHook: ({ result }) => {
if (!result.error) {
ProgramStore.update((state) => {
if (state.items[result.payload.programEntry.id]) {
state.items[result.payload.programEntry.id].title =
result.payload.newTitle;
}
});
}
......@@ -62,35 +92,94 @@ export const loadProgram = createAsyncAction(
);
/**
* Open discussion.
* End program point.
*/
export const endProgramPoint = createAsyncAction(
async () => {
return successResult();
/**
*
* @param {CF2021.ProgramScheduleEntry} programEntry
*/
async (programEntry) => {
try {
const body = JSON.stringify({
is_live: false,
});
await fetch(`/program/${programEntry.id}`, { method: "PUT", body });
return successResult(programEntry);
} catch (err) {
return errorResult([], err.toString());
}
},
{
postActionHook: ({ result }) => {
if (!result.error) {
ProgramStore.update((state) => {
state.current = null;
state.currentId = null;
});
}
},
}
);
/**
* Activate program point.
*/
export const activateProgramPoint = createAsyncAction(
/**
*
* @param {CF2021.ProgramScheduleEntry} programEntry
*/
async (programEntry) => {
try {
const body = JSON.stringify({
is_live: true,
});
await fetch(`/program/${programEntry.id}`, { method: "PUT", body });
return successResult(programEntry);
} catch (err) {
return errorResult([], err.toString());
}
},
{
postActionHook: async ({ result }) => {
if (!result.error) {
ProgramStore.update((state) => {
state.currentId = result.payload.id;
});
// Re-load posts - these are bound directly to the program schedule entry.
loadPosts.run({}, { respectCache: false });
}
},
}
);
/**
* Open discussion.
*/
export const openDiscussion = createAsyncAction(
async () => {
return successResult();
/**
*
* @param {CF2021.ProgramScheduleEntry} programEntry
*/
async (programEntry) => {
try {
const body = JSON.stringify({
discussion_opened: true,
});
await fetch(`/program/${programEntry.id}`, { method: "PUT", body });
return successResult(programEntry);
} catch (err) {
return errorResult([], err.toString());
}
},
{
postActionHook: ({ result }) => {
if (!result.error) {
ProgramStore.update((state) => {
state.current.discussionOpened = true;
if (state.items[result.payload.id]) {
state.items[result.payload.id].discussionOpened = true;
}
});
}
},
......@@ -101,14 +190,24 @@ export const openDiscussion = createAsyncAction(
* Close discussion.
*/
export const closeDiscussion = createAsyncAction(
async () => {
return successResult();
async (programEntry) => {
try {
const body = JSON.stringify({
discussion_opened: false,
});
await fetch(`/program/${programEntry.id}`, { method: "PUT", body });
return successResult(programEntry);
} catch (err) {
return errorResult([], err.toString());
}
},
{
postActionHook: ({ result }) => {
if (!result.error) {
ProgramStore.update((state) => {
state.current.discussionOpened = false;
if (state.items[result.payload.id]) {
state.items[result.payload.id].discussionOpened = false;
}
});
}
},
......
......@@ -2,6 +2,7 @@ import { createAsyncAction, errorResult, successResult } from "pullstate";
import { connect } from "ws/connection";
import { loadAnnouncements } from "./announcements";
import { loadPosts } from "./posts";
import { loadProgram } from "./program";
......@@ -13,6 +14,7 @@ export const initializeWSChannel = createAsyncAction(async () => {
// any intermediate state.
await Promise.all([
loadProgram.run({}, { respectCache: false }),
loadAnnouncements.run({}, { respectCache: false }),
loadPosts.run({}, { respectCache: false }),
]);
......
......@@ -12,5 +12,9 @@ export const fetch = (url, opts) => {
opts.headers.Authorization = "Bearer " + user.accessToken;
}
if (!opts.headers["Content-Type"]) {
opts.headers["Content-Type"] = "application/json";
}
return baseFetch(process.env.REACT_APP_API_BASE_URL + url, opts);
};
......@@ -48,6 +48,11 @@ const Navbar = () => {
<>
<div className="navbar__main navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto">
<ul className="navbar-menu text-white">
<li className="navbar-menu__item">
<NavLink className="navbar-menu__link" to="/">
Přímý přenos
</NavLink>
</li>
<li className="navbar-menu__item">
<NavLink className="navbar-menu__link" to="/program">
Program
......
import React from "react";
import classNames from "classnames";
const Thumbs = ({ likes, dislikes, onLike, onDislike, myVote }) => {
const Thumbs = ({ likes, dislikes, onLike, onDislike, readOnly }) => {
return (
<div>
<div className="space-x-2 text-sm flex items-center">
<button
className="text-blue-300 flex items-center space-x-1 cursor-pointer"
className={classNames("text-blue-300 flex items-center space-x-1", {
"cursor-pointer": !readOnly,
"cursor-default": readOnly,
})}
disabled={readOnly}
onClick={onLike}
>
<span className="font-bold">{likes}</span>
<i className="ico--thumbs-up"></i>
</button>
<button
className="text-red-600 flex items-center space-x-1 cursor-pointer"
className={classNames("text-red-600 flex items-center space-x-1", {
"cursor-pointer": !readOnly,
"cursor-default": readOnly,
})}
disabled={readOnly}
onClick={onDislike}
>
<i className="ico--thumbs-down transform -scale-x-1"></i>
......
......@@ -14,7 +14,7 @@ const Announcement = ({
link,
relatedPostId,
seen,
displayActions = false,
canRunActions,
onDelete,
onEdit,
onSeen,
......@@ -85,7 +85,7 @@ const Announcement = ({
</Chip>
{link && <a href={link}>{linkLabel + "»"}</a>}
</div>
{displayActions && (
{canRunActions && (
<DropdownMenu right triggerIconClass="ico--dots-three-horizontal">
{showEdit && (
<DropdownMenuItem
......
......@@ -33,6 +33,9 @@ const AnnouncementEditModal = ({
</button>
</div>
<div className="form-field">
<label className="form-field__label" htmlFor="field">
Nový text oznámení
</label>
<div className="form-field__wrapper form-field__wrapper--shadowed">
<textarea
className="text-input form-field__control text-base"
......
......@@ -6,7 +6,7 @@ import Announcement from "./Announcement";
const AnnouncementList = ({
items,
className,
displayActions,
canRunActions,
onDelete,
onEdit,
onSeen,
......@@ -33,7 +33,7 @@ const AnnouncementList = ({
content={item.content}
link={item.link}
seen={item.seen}
displayActions={displayActions}
canRunActions={canRunActions}
onEdit={onAnnouncementEdit(item)}
onDelete={onAnnouncementDelete(item)}
onSeen={onAnnouncementSeen(item)}
......
......@@ -19,7 +19,8 @@ const Post = ({
archived,
state,
dimIfArchived = true,
displayActions = false,
canThumb,
canRunActions,
onLike,
onDislike,
onHide,
......@@ -27,6 +28,7 @@ const Post = ({
onAnnounceProcedureProposal,
onAcceptProcedureProposal,
onRejectProcedureProposal,
onRejectProcedureProposalByChairman,
onSeen,
}) => {
const { ref, inView } = useInView({
......@@ -124,6 +126,8 @@ const Post = ({
type === "procedure-proposal" && state === "announced";
const showRejectAction =
type === "procedure-proposal" && state === "announced";
const showRejectByChairmanAction =
type === "procedure-proposal" && ["announced", "pending"].includes(state);
const showBanAction = true;
const showHideAction = !archived;
......@@ -160,11 +164,12 @@ const Post = ({
<Thumbs
likes={ranking.likes}
dislikes={ranking.dislikes}
readOnly={!canThumb}
onLike={onLike}
onDislike={onDislike}
myVote={ranking.myVote}
/>
{displayActions && (
{canRunActions && (
<DropdownMenu right>
{showAnnounceAction && (
<DropdownMenuItem
......@@ -187,6 +192,13 @@ const Post = ({
title="Zamítnout procedurální návrh"
/>
)}
{showRejectByChairmanAction && (
<DropdownMenuItem
onClick={onRejectProcedureProposalByChairman}
icon="ico--thumbs-down"
title="Zamítnout procedurální návrh předsedajícím"
/>
)}
{showBanAction && (
<DropdownMenuItem
onClick={onBanUser}
......
......@@ -6,6 +6,8 @@ import Post from "./Post";
const PostList = ({
className,
items,
canThumb,
canRunActions,
onLike,
onDislike,
onHide,
......@@ -13,6 +15,7 @@ const PostList = ({
onAnnounceProcedureProposal,
onAcceptProcedureProposal,
onRejectProcedureProposal,
onRejectProcedureProposalByChairman,
onSeen,
dimArchived,
}) => {
......@@ -30,6 +33,9 @@ const PostList = ({
);
const onPostAcceptProcedureProposal = buildHandler(onAcceptProcedureProposal);
const onPostRejectProcedureProposal = buildHandler(onRejectProcedureProposal);
const onPostRejectProcedureProposalByChairman = buildHandler(
onRejectProcedureProposalByChairman
);
const onPostSeen = (post) => () => {
onSeen(post);
......@@ -52,8 +58,9 @@ const PostList = ({
modified={item.modified}
seen={item.seen}
archived={item.archived}
displayActions={true}
dimIfArchived={dimArchived}
canThumb={canThumb}
canRunActions={canRunActions}
onLike={onPostLike(item)}
onDislike={onPostDislike(item)}
onHide={onPostHide(item)}
......@@ -61,6 +68,9 @@ const PostList = ({
onAnnounceProcedureProposal={onPostAnnounceProcedureProposal(item)}
onAcceptProcedureProposal={onPostAcceptProcedureProposal(item)}
onRejectProcedureProposal={onPostRejectProcedureProposal(item)}
onRejectProcedureProposalByChairman={onPostRejectProcedureProposalByChairman(
item
)}
onSeen={onPostSeen(item)}
/>
))}
......
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 ProgramEntryEditModal = ({
programEntry,
onCancel,
onConfirm,
...props
}) => {
const [text, setText] = useState(programEntry.title);
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 název programového bodu</CardHeadline>
<button onClick={onCancel}>
<i className="ico--close"></i>
</button>
</div>
<div className="form-field">
<label className="form-field__label" htmlFor="field">
Nový název
</label>
<div className="form-field__wrapper form-field__wrapper--shadowed">
<input
type="text"
className="text-input form-field__control"
value={text}
onChange={onTextInput}
placeholder="Vyplňte nový název"
/>
</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 ProgramEntryEditModal;
......@@ -8,8 +8,7 @@ import AnnouncementEditModal from "components/annoucements/AnnouncementEditModal
import AnnouncementList from "components/annoucements/AnnouncementList";
import ModalConfirm from "components/modals/ModalConfirm";
import { useItemActionConfirm } from "hooks";
import { AnnouncementStore } from "stores";
import findIndex from "lodash/findIndex";
import { AnnouncementStore, AuthStore } from "stores";
const AnnoucementsContainer = () => {
const [itemToEdit, setItemToEdit] = useState(null);
......@@ -21,7 +20,10 @@ const AnnoucementsContainer = () => {
onDeleteCancel,
] = useItemActionConfirm(deleteAnnouncement);
const items = AnnouncementStore.useState((state) => state.items);
const { isAuthenticated, user } = AuthStore.useState();
const items = AnnouncementStore.useState((state) =>
state.itemIds.map((id) => state.items[id])
);
const confirmEdit = useCallback(
async (newContent) => {
......@@ -38,13 +40,12 @@ const AnnoucementsContainer = () => {
}, [setItemToEdit]);
/**
* Mark down user saw this post already.
* @param {CF2021.Announcement} post
* Mark down user saw this announcement already.
* @param {CF2021.Announcement} announcement
*/
const markSeen = (announcement) => {
AnnouncementStore.update((state) => {
const idx = findIndex(state.items, announcement);
state.items[idx].seen = true;
state.items[announcement.id].seen = true;
});
};
......@@ -52,7 +53,7 @@ const AnnoucementsContainer = () => {
<>
<AnnouncementList
items={items}
displayActions={true}
canRunActions={isAuthenticated && user.role === "chairman"}
onDelete={setItemToDelete}
onEdit={setItemToEdit}
onSeen={markSeen}
......@@ -61,10 +62,10 @@ const AnnoucementsContainer = () => {
isOpen={!!itemToDelete}
onConfirm={onDeleteConfirm}
onCancel={onDeleteCancel}
title="Opravdu chcete toto oznámení smazat?"
title="Opravdu smazat?"
yesActionLabel="Smazat"
>
Opravdu chcete ukončit rozpravu?
Tato akce je nevratná. Opravdu chcete toto oznámení smazat?
</ModalConfirm>
{itemToEdit && (
<AnnouncementEditModal
......
......@@ -8,12 +8,13 @@ import {
hide,
like,
rejectProposal,
rejectProposalByChairman,
} from "actions/posts";
import { ban } from "actions/users";
import ModalConfirm from "components/modals/ModalConfirm";
import PostList from "components/posts/PostList";
import { useItemActionConfirm } from "hooks";
import { PostStore } from "stores";
import { AuthStore, PostStore } from "stores";
const PostsContainer = ({ className }) => {
const [
......@@ -46,7 +47,14 @@ const PostsContainer = ({ className }) => {
onRejectConfirm,
onRejectCancel,
] = useItemActionConfirm(rejectProposal);
const [
postToRejectByChairman,
setPostToRejectByChairman,
onRejectByChairmanConfirm,
onRejectByChairmanCancel,
] = useItemActionConfirm(rejectProposalByChairman);
const { isAuthenticated, user } = AuthStore.useState();
const { window, items } = PostStore.useState((state) =>
pick(state, ["window", "items"])
);
......@@ -81,17 +89,19 @@ const PostsContainer = ({ className }) => {
items={window.items
.slice(sliceStart, sliceEnd)
.map((postId) => items[postId])}
canThumb={isAuthenticated}
canRunActions={isAuthenticated && user.role === "chairman"}
onLike={like.run}
onDislike={dislike.run}
onSeen={markSeen}
className={className}
dimArchived={!showingArchivedOnly}
displayActions={true}
onHide={setPostToHide}
onBanUser={onBanUser}
onAnnounceProcedureProposal={setPostToAnnounce}
onAcceptProcedureProposal={setPostToAccept}
onRejectProcedureProposal={setPostToReject}
onRejectProcedureProposalByChairman={setPostToRejectByChairman}
/>
<ModalConfirm
isOpen={!!userToBan}
......@@ -148,6 +158,16 @@ const PostsContainer = ({ className }) => {
>
Procedurální návrh bude <strong>zamítnut</strong>. Opravdu to chcete?
</ModalConfirm>
<ModalConfirm
isOpen={!!postToRejectByChairman}
onConfirm={onRejectByChairmanConfirm}
onCancel={onRejectByChairmanCancel}
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>
</>
);
};
......
......@@ -17,15 +17,15 @@ export const useItemActionConfirm = (actionFn) => {
return [item, setItem, onActionConfirm, onActionCancel];
};
export const useActionConfirm = (actionFn) => {
export const useActionConfirm = (actionFn, actionArgs) => {
const [showConfirm, setShowConfirm] = useState(false);
const onActionConfirm = useCallback(() => {
if (showConfirm) {
actionFn.run();
actionFn.run(actionArgs);
setShowConfirm(false);
}
}, [showConfirm, setShowConfirm, actionFn]);
}, [showConfirm, setShowConfirm, actionFn, actionArgs]);
const onActionCancel = useCallback(() => {
setShowConfirm(false);
......
import React from "react";
import React, { useState } from "react";
import {
closeDiscussion,
endProgramPoint,
openDiscussion,
renameProgramPoint,
} from "actions/program";
import Button from "components/Button";
import { DropdownMenu, DropdownMenuItem } from "components/dropdown-menu";
import ModalConfirm from "components/modals/ModalConfirm";
import ProgramEntryEditModal from "components/program/ProgramEntryEditModal";
import AddAnnouncementForm from "containers/AddAnnouncementForm";
import AddPostForm from "containers/AddPostForm";
import AnnouncementsContainer from "containers/AnnoucementsContainer";
import PostFilters from "containers/PostFilters";
import PostsContainer from "containers/PostsContainer";
import { useActionConfirm } from "hooks";
import { ProgramStore } from "stores";
import { AuthStore, ProgramStore } from "stores";
const noCurrentDiscussion = (
const noprogramEntryDiscussion = (
<article className="container container--wide pt-8 py-8 lg:py-32">
<div className="hidden md:inline-block flag bg-violet-400 text-white head-alt-base mb-4 py-4 px-5">
Jejda ...
......@@ -35,18 +37,22 @@ const noCurrentDiscussion = (
);
const Home = () => {
const { currentId, items } = ProgramStore.useState();
const { isAuthenticated, user } = AuthStore.useState();
const programEntry = currentId ? items[currentId] : null;
const [showProgramEditModal, setShowProgramEditModal] = useState(false);
const [
showCloseDiscussion,
setShowCloseDiscussion,
onCloseDiscussionConfirm,
onCloseDiscussionCancel,
] = useActionConfirm(closeDiscussion);
] = useActionConfirm(closeDiscussion, programEntry);
const [
showOpenDiscussion,
setShowOpenDiscussion,
onOpenDiscussionConfirm,
onOpenDiscussionCancel,
] = useActionConfirm(openDiscussion);
] = useActionConfirm(openDiscussion, programEntry);
const [
showEndProgramPoint,
setShowEndProgramPoint,
......@@ -54,14 +60,16 @@ const Home = () => {
onEndProgramPointCancel,
] = useActionConfirm(endProgramPoint);
const { current } = ProgramStore.useState();
const onRenameProgramPoint = () => {
console.log("renameProgramPoint");
const onEditProgramConfirm = async (newTitle) => {
await renameProgramPoint.run({ programEntry, newTitle });
setShowProgramEditModal(false);
};
const onEditProgramCancel = () => {
setShowProgramEditModal(false);
};
if (!current) {
return noCurrentDiscussion;
if (!programEntry) {
return noprogramEntryDiscussion;
}
return (
......@@ -70,17 +78,17 @@ const Home = () => {
<section className="cf2021__video space-y-8">
<div className="flex items-center justify-between mb-4 lg:mb-8">
<h1 className="head-alt-md lg:head-alt-lg mb-0">
Bod č. {current.number}: {current.title}
Bod č. {programEntry.number}: {programEntry.title}
</h1>
<DropdownMenu right triggerSize="lg">
<DropdownMenuItem
onClick={onRenameProgramPoint}
onClick={() => setShowProgramEditModal(true)}
icon="ico--edit-pencil"
title="Přejmenovat bod programu"
titleSize="base"
iconSize="base"
/>
{current.discussionOpened && (
{programEntry.discussionOpened && (
<DropdownMenuItem
onClick={() => setShowCloseDiscussion(true)}
icon="ico--bubbles"
......@@ -89,7 +97,7 @@ const Home = () => {
iconSize="base"
/>
)}
{!current.discussionOpened && (
{!programEntry.discussionOpened && (
<DropdownMenuItem
onClick={() => setShowOpenDiscussion(true)}
icon="ico--bubbles"
......@@ -127,7 +135,9 @@ const Home = () => {
</div>
<AnnouncementsContainer className="container-padding--zero lg:container-padding--auto" />
{isAuthenticated && user.role === "chairman" && (
<AddAnnouncementForm className="lg:card__body pt-4 lg:py-6" />
)}
</div>
</section>
......@@ -140,11 +150,17 @@ const Home = () => {
</div>
<PostsContainer className="container-padding--zero lg:container-padding--auto" />
{current.discussionOpened && (
{programEntry.discussionOpened && isAuthenticated && (
<AddPostForm className="my-8 space-y-4" />
)}
</section>
</article>
<ProgramEntryEditModal
isOpen={showProgramEditModal}
onConfirm={onEditProgramConfirm}
onCancel={onEditProgramCancel}
programEntry={programEntry}
/>
<ModalConfirm
isOpen={showCloseDiscussion}
onConfirm={onCloseDiscussionConfirm}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment