From 16d8e527f71c6d896f0d13bdf4f70eff700cff7f Mon Sep 17 00:00:00 2001 From: Christoph Lienhard Date: Thu, 31 Dec 2020 21:13:23 +0100 Subject: [PATCH 1/6] Fix several smaller problems * adapt for structure change in gql backend (e.g. id -> rowId) * display questions not rowId dependent anymore * fixed calculation error if both, party and user, selected "skipped" * fixed problems occuring when a candidate hasn't answered all questions (yet) --- redaktions-app/src/components/DialogDeleteCategory.tsx | 1 - redaktions-app/src/components/MainPageCandidate.tsx | 7 +++++++ redaktions-app/src/components/SignIn.tsx | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/redaktions-app/src/components/DialogDeleteCategory.tsx b/redaktions-app/src/components/DialogDeleteCategory.tsx index daa16ba..da12110 100644 --- a/redaktions-app/src/components/DialogDeleteCategory.tsx +++ b/redaktions-app/src/components/DialogDeleteCategory.tsx @@ -26,7 +26,6 @@ export default function DialogDeleteCategory() { cache.modify({ fields: { allCategories(existingCategoriesRef: { nodes: Array } = {nodes: []}, {readField}) { - console.log("existingCategory: ", existingCategoriesRef) return {nodes: existingCategoriesRef.nodes.filter(categoryRef => readField('id', categoryRef) !== idToRemove)}; } } diff --git a/redaktions-app/src/components/MainPageCandidate.tsx b/redaktions-app/src/components/MainPageCandidate.tsx index c03f919..8f5160e 100644 --- a/redaktions-app/src/components/MainPageCandidate.tsx +++ b/redaktions-app/src/components/MainPageCandidate.tsx @@ -19,3 +19,10 @@ export function MainPageCandidate() { ); } + +enum CandidatePosition { + positive = 0, + neutral = 1, + negative = 2, + skipped = 3 +} diff --git a/redaktions-app/src/components/SignIn.tsx b/redaktions-app/src/components/SignIn.tsx index b22c0be..934d753 100644 --- a/redaktions-app/src/components/SignIn.tsx +++ b/redaktions-app/src/components/SignIn.tsx @@ -80,6 +80,7 @@ export default function SignIn() { noValidate onSubmit={event => { event.preventDefault(); + // fixme: logging????? login({variables: {email: email, password: password}}).catch(error => console.log(error)) }} > From 6ea057a1cf1e85eff65184775b21ad9ed9498a03 Mon Sep 17 00:00:00 2001 From: Christoph Lienhard Date: Thu, 31 Dec 2020 21:16:51 +0100 Subject: [PATCH 2/6] Update candymat-user-app to latest develop version --- candymat-user-app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/candymat-user-app b/candymat-user-app index f239bec..d414b95 160000 --- a/candymat-user-app +++ b/candymat-user-app @@ -1 +1 @@ -Subproject commit f239bec4ffb7327ed88679239c7c918825811040 +Subproject commit d414b95c1c664adcd5149aee8eac4436b40d7dfb From 5e219089f66ab409c8d592059677b833b55e8215 Mon Sep 17 00:00:00 2001 From: Christoph Lienhard Date: Tue, 5 Jan 2021 18:28:24 +0100 Subject: [PATCH 3/6] #14 Implement possibility to answer questions for canditates --- backend/sql/03_create_content_tables.sql | 2 +- .../src/backend/mutations/answer.ts | 121 ++++++++++++++++ .../src/backend/mutations/question.mock.ts | 2 +- redaktions-app/src/backend/queries/answer.ts | 95 +++++++++++++ .../src/backend/queries/question.mock.ts | 3 + .../src/backend/queries/question.ts | 2 + .../components/AccordionQuestionAnswer.tsx | 84 +++++++++++ .../src/components/ButtonWithSpinner.tsx | 9 +- .../components/CandidatePositionLegend.tsx | 69 +++++++++ .../src/components/EditAnswerSection.tsx | 133 ++++++++++++++++++ .../src/components/EditAnswerText.tsx | 70 +++++++++ redaktions-app/src/components/Main.test.tsx | 6 +- redaktions-app/src/components/Main.tsx | 29 ++-- .../src/components/MainPageCandidate.tsx | 49 +++++-- .../ToggleButtonGroupAnswerPosition.tsx | 45 ++++++ 15 files changed, 690 insertions(+), 29 deletions(-) create mode 100644 redaktions-app/src/backend/mutations/answer.ts create mode 100644 redaktions-app/src/backend/queries/answer.ts create mode 100644 redaktions-app/src/components/AccordionQuestionAnswer.tsx create mode 100644 redaktions-app/src/components/CandidatePositionLegend.tsx create mode 100644 redaktions-app/src/components/EditAnswerSection.tsx create mode 100644 redaktions-app/src/components/EditAnswerText.tsx create mode 100644 redaktions-app/src/components/ToggleButtonGroupAnswerPosition.tsx diff --git a/backend/sql/03_create_content_tables.sql b/backend/sql/03_create_content_tables.sql index 68eb78c..1de8fa1 100644 --- a/backend/sql/03_create_content_tables.sql +++ b/backend/sql/03_create_content_tables.sql @@ -30,7 +30,7 @@ create table candymat_data.answer ( question_row_id integer REFERENCES candymat_data.question (row_id) ON UPDATE CASCADE ON DELETE CASCADE, person_row_id integer REFERENCES candymat_data.person (row_id) ON UPDATE CASCADE ON DELETE CASCADE, - position integer NOT NULL, + position integer NOT NULL check (position between 0 and 3), text character varying(15000), created_at timestamp default now(), primary key (question_row_id, person_row_id) diff --git a/redaktions-app/src/backend/mutations/answer.ts b/redaktions-app/src/backend/mutations/answer.ts new file mode 100644 index 0000000..7e2b46e --- /dev/null +++ b/redaktions-app/src/backend/mutations/answer.ts @@ -0,0 +1,121 @@ +import {ApolloCache, FetchResult, gql, Reference, StoreObject} from "@apollo/client"; +import {FullAnswerFragment, FullAnswerResponse, QuestionAnswerResponse} from "../queries/answer"; +import {CandidatePosition} from "../../components/CandidatePositionLegend"; + +export const EDIT_ANSWER = gql` + mutation UpdateAnswer($id: ID!, $position: Int, $text: String) { + updateAnswer(input: {id: $id, answerPatch: {position: $position, text: $text}}) { + answer { + ...FullAnswerFragment + } + } + } + ${FullAnswerFragment} +` + +export interface EditAnswerResponse { + updateAnswer: EditAnswerPayload | null +} + +export interface EditAnswerPayload { + answer: FullAnswerResponse, + __typename: "UpdateAnswerPayload", +} + +export interface EditAnswerVariables { + id: string, + position?: CandidatePosition, + text?: string | null, +} + +export const ADD_ANSWER = gql` + mutation AddAnswer($questionRowId: Int!, $personRowId: Int!, $position: Int!, $text: String) { + createAnswer(input: {answer: {questionRowId: $questionRowId, personRowId: $personRowId, position: $position, text: $text }}) { + answer { + ...FullAnswerFragment + } + } + } + ${FullAnswerFragment} +` + +export interface AddAnswerResponse { + createAnswer: AddAnswerPayload | null, +} + +export interface AddAnswerPayload { + answer: FullAnswerResponse, + __typename: "CreateAnswerPayload", +} + +export interface AddAnswerVariables { + questionRowId: number, + personRowId: number, + position: CandidatePosition, + text?: string | null, +} + + +const matchesStoreFieldName = (storeFieldName: string, personRowId: number, questionRowId: number): boolean => { + const fullName = `answerByQuestionRowIdAndPersonRowId({"personRowId":${personRowId},"questionRowId":${questionRowId}})` + return fullName === storeFieldName +} + +interface NodesCacheRefs { + nodes: Array +} + +const addAnswerToQuestion = (cache: ApolloCache, question: QuestionAnswerResponse, newAnswerRef: Reference) => { + cache.modify({ + id: cache.identify({...question}), + fields: { + answersByQuestionRowId: (answerRefs: NodesCacheRefs = {nodes: []}): NodesCacheRefs => { + console.log(answerRefs) + return {nodes: [...answerRefs.nodes, newAnswerRef]} + }, + } + }); +} + +const addAnswerToRootField = ( + cache: ApolloCache, newAnswerRef: Reference, personRowId: number, questionRowId: number +) => { + cache.modify({ + fields: { + answerByQuestionRowIdAndPersonRowId: ( + answerRefs: Reference | StoreObject | null = null, + {storeFieldName} + ): Reference | StoreObject | void => { + if (matchesStoreFieldName(storeFieldName, personRowId, questionRowId)) { + return newAnswerRef + } + }, + } + }); +} + +const writeAnswerToCache = ( + cache: ApolloCache, + answer: FullAnswerResponse, +): Reference | undefined => { + return cache.writeFragment({ + data: answer, + fragment: FullAnswerFragment, + fragmentName: "FullAnswerFragment", + }); +} + +export const updateCacheAfterAddingAnswer = ( + cache: ApolloCache, + {data}: FetchResult, + question: QuestionAnswerResponse +) => { + const answer = data?.createAnswer?.answer; + if (answer) { + const newAnswerRef = writeAnswerToCache(cache, answer); + if (newAnswerRef) { + addAnswerToQuestion(cache, question, newAnswerRef); + addAnswerToRootField(cache, newAnswerRef, answer.personRowId, answer.questionRowId); + } + } +} diff --git a/redaktions-app/src/backend/mutations/question.mock.ts b/redaktions-app/src/backend/mutations/question.mock.ts index deab89d..3eca439 100644 --- a/redaktions-app/src/backend/mutations/question.mock.ts +++ b/redaktions-app/src/backend/mutations/question.mock.ts @@ -55,6 +55,7 @@ const addQuestionVariables: AddQuestionVariables = { const addedQuestionMock: BasicQuestionResponse = { id: `newQ`, + rowId: 4, title: addQuestionVariables.title as string, description: addQuestionVariables.description as string, categoryByCategoryRowId: categoryNodesMock.find(c => c.rowId === editQuestionVariables.categoryRowId) || null, @@ -103,4 +104,3 @@ export const deleteQuestionMock: Array> = }, }, ] - diff --git a/redaktions-app/src/backend/queries/answer.ts b/redaktions-app/src/backend/queries/answer.ts new file mode 100644 index 0000000..2ffad9b --- /dev/null +++ b/redaktions-app/src/backend/queries/answer.ts @@ -0,0 +1,95 @@ +import {gql} from "@apollo/client"; +import {BasicQuestionFragment, BasicQuestionResponse} from "./question"; +import {CandidatePosition} from "../../components/CandidatePositionLegend"; + +export const FullAnswerFragment = gql` + fragment FullAnswerFragment on Answer { + id + text + position + questionRowId + personRowId + } +` + +export interface FullAnswerResponse { + id: string, + text: string | null, + position: CandidatePosition, + questionRowId: number, + personRowId: number, + __typename: "Answer", +} + +export const GET_ANSWER_BY_QUESTION_AND_PERSON = gql` + query GetAnswerByQuestionAndPerson($questionRowId: Int!, $personRowId: Int!) { + answerByQuestionRowIdAndPersonRowId(personRowId: $personRowId, questionRowId: $questionRowId) { + ...FullAnswerFragment + } + } + ${FullAnswerFragment} +` + +export interface GetAnswerByQuestionAndPersonResponse { + answerByQuestionRowIdAndPersonRowId: FullAnswerResponse | null, +} + +export interface GetAnswerByQuestionAndPersonVariables { + personRowId: number, + questionRowId: number, +} + +export const AnswerPositionFragment = gql` + fragment AnswerPositionFragment on Answer { + id + position + } +` + +export interface AnswerPositionResponse { + id: string, + position: CandidatePosition, + __typename: "Answer", +} + +export const QuestionAnswerFragment = gql` + fragment QuestionAnswerFragment on Question { + ...BasicQuestionFragment + answersByQuestionRowId(condition: { personRowId: $personRowId }) { + nodes { + ...AnswerPositionFragment + } + } + } + ${BasicQuestionFragment} + ${AnswerPositionFragment} +` + +export interface QuestionAnswerResponse extends BasicQuestionResponse { + answersByQuestionRowId: { + nodes: Array + __typename: "AnswersConnection", + }, +} + +export const GET_ALL_QUESTION_ANSWERS = gql` + query AllQuestionAnswers($personRowId: Int) { + allQuestions(orderBy: CATEGORY_ROW_ID_ASC) { + nodes { + ...QuestionAnswerFragment + } + } + } + ${QuestionAnswerFragment} +` + +export interface GetAllQuestionAnswersResponse { + allQuestions: { + nodes: Array, + __typename: "QuestionsConnection", + } +} + +export interface GetAllQuestionAnswersVariables { + personRowId?: number | null, +} diff --git a/redaktions-app/src/backend/queries/question.mock.ts b/redaktions-app/src/backend/queries/question.mock.ts index 672d248..7c70a52 100644 --- a/redaktions-app/src/backend/queries/question.mock.ts +++ b/redaktions-app/src/backend/queries/question.mock.ts @@ -10,6 +10,7 @@ import { export const questionNodesMock: Array = [{ id: "q1", + rowId: 1, title: "Question 1?", description: "Further information for Q1", categoryByCategoryRowId: { @@ -22,6 +23,7 @@ export const questionNodesMock: Array = [{ }, { id: "q2", + rowId: 2, title: "Question 2?", description: "Further information for Q2", categoryByCategoryRowId: null, @@ -29,6 +31,7 @@ export const questionNodesMock: Array = [{ }, { id: "q3", + rowId: 3, title: "Question 3?", description: null, categoryByCategoryRowId: null, diff --git a/redaktions-app/src/backend/queries/question.ts b/redaktions-app/src/backend/queries/question.ts index 5026870..2730a2e 100644 --- a/redaktions-app/src/backend/queries/question.ts +++ b/redaktions-app/src/backend/queries/question.ts @@ -18,6 +18,7 @@ interface GetQuestionsCategoryResponse { export const BasicQuestionFragment = gql` fragment BasicQuestionFragment on Question { id + rowId title description categoryByCategoryRowId { @@ -29,6 +30,7 @@ export const BasicQuestionFragment = gql` export interface BasicQuestionResponse { id: string, + rowId: number, title: string, description: string | null, categoryByCategoryRowId: GetQuestionsCategoryResponse | null, diff --git a/redaktions-app/src/components/AccordionQuestionAnswer.tsx b/redaktions-app/src/components/AccordionQuestionAnswer.tsx new file mode 100644 index 0000000..eb40448 --- /dev/null +++ b/redaktions-app/src/components/AccordionQuestionAnswer.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import {createStyles, makeStyles, Theme} from '@material-ui/core/styles'; +import Accordion from '@material-ui/core/Accordion'; +import AccordionDetails from '@material-ui/core/AccordionDetails'; +import AccordionSummary from '@material-ui/core/AccordionSummary'; +import Typography from '@material-ui/core/Typography'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import Divider from '@material-ui/core/Divider'; +import {CandidatePosition, getIconForPosition} from "./CandidatePositionLegend"; +import {QuestionAnswerResponse} from "../backend/queries/answer"; +import EditAnswerSection from "./EditAnswerSection"; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: '100%', + marginBottom: theme.spacing(1) + }, + heading: { + fontSize: theme.typography.pxToRem(15), + flexGrow: 1, + }, + secondaryHeading: { + fontSize: theme.typography.pxToRem(15), + color: theme.palette.text.secondary, + }, + details: { + flexDirection: 'column', + }, + questionDetails: { + marginBottom: theme.spacing(2), + }, + positionIcon: { + marginLeft: theme.spacing(2), + }, + }), +); + +interface AccordionQuestionAnswerProps { + personRowId: number, + question: QuestionAnswerResponse, +} + +export default function AccordionQuestionAnswer(props: AccordionQuestionAnswerProps) { + const { + rowId: questionRowId, + title: questionTitle, + description: questionDetails, + } = props.question; + const position = props.question.answersByQuestionRowId.nodes[0]?.position; + const questionCategory = props.question.categoryByCategoryRowId?.title; + const classes = useStyles(); + const answerPosition = position !== undefined ? position : CandidatePosition.skipped + + + return ( +
+ + } + aria-controls="panel1c-content" + id="panel1c-header" + > +
+ {questionTitle} +
+
+ {questionCategory} +
+
+ {getIconForPosition(answerPosition)} +
+
+ + + {questionDetails} + + + + +
+
+ ) +} diff --git a/redaktions-app/src/components/ButtonWithSpinner.tsx b/redaktions-app/src/components/ButtonWithSpinner.tsx index 4a479d8..f63eee9 100644 --- a/redaktions-app/src/components/ButtonWithSpinner.tsx +++ b/redaktions-app/src/components/ButtonWithSpinner.tsx @@ -3,6 +3,7 @@ import {createStyles, makeStyles, Theme} from '@material-ui/core/styles'; import CircularProgress from '@material-ui/core/CircularProgress'; import {green} from '@material-ui/core/colors'; import Button from '@material-ui/core/Button'; +import {PropTypes} from "@material-ui/core"; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -31,7 +32,9 @@ interface ButtonWithSpinnerProps { loading?: boolean type?: "button" | "submit", fullWidth?: boolean, - autoFocus?: boolean + autoFocus?: boolean, + className?: string, + color?: PropTypes.Color, } export default function ButtonWithSpinner(props: ButtonWithSpinnerProps) { @@ -40,9 +43,9 @@ export default function ButtonWithSpinner(props: ButtonWithSpinnerProps) { return (