Ignore:
Timestamp:
11/04/21 17:01:30 (3 years ago)
Author:
Стојков Марко <mst@…>
Branches:
dev
Children:
caaf82d
Parents:
1e0d869 (diff), b9d7ae5 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merged dev

Location:
src/Clients/Angular/finki-chattery/src
Files:
1 added
16 edited
1 moved

Legend:

Unmodified
Added
Removed
  • src/Clients/Angular/finki-chattery/src/app/core/services/auth.service.ts

    r1e0d869 r6901f8b  
    2424  public user: ApplicationUser | null = null;
    2525  public oidcUser: User | null = null;
     26  public selfUser: SelfUserResponse | null = null;
    2627
    2728  constructor(private baseApi: BaseApiService) {
     
    3536        user?.profile.isVerified
    3637      );
     38
     39      this.selfUserDto().subscribe((selfUser) => {
     40        if (selfUser) {
     41          this.selfUser = selfUser;
     42        }
     43      });
    3744    });
    3845  }
     
    8693        user.profile.isVerified
    8794      );
     95
     96      if (!this.selfUser) {
     97        this.selfUserDto().subscribe((selfUser) => {
     98          if (selfUser) {
     99            this.selfUser = selfUser;
     100          }
     101        });
     102      }
    88103    });
    89104  }
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-facade.service.ts

    r1e0d869 r6901f8b  
    22import { Injectable } from '@angular/core';
    33import { Action, Store } from '@ngrx/store';
    4 import { Observable, Subject, throwError } from 'rxjs';
    5 import { catchError, filter, map } from 'rxjs/operators';
     4import { Observable, Subject } from 'rxjs';
     5import { filter, map } from 'rxjs/operators';
    66
    77import {
     
    99  PreviewQuestionViewModel,
    1010  QuestionStateViewModel,
    11   SearchQuestionsQueryViewModel
     11  SearchQuestionsQueryViewModel,
     12  VoteType
    1213} from 'src/app/shared-app/models';
     14import { AuthService } from '../services';
    1315import {
    1416  EffectStartedWorking,
     
    1618  GetPreviewQuestionsPopular,
    1719  GetQuestionState,
    18   GetSearchQuestions
     20  GetSearchQuestions,
     21  SetCorrectAnswer,
     22  VoteAnswer
    1923} from './question-state/question.actions';
    2024import { questionStateQuery } from './question-state/question.selectors';
     
    2933  effectWorking$: Observable<boolean | HttpErrorResponse>;
    3034
    31   constructor(private store: Store<QuestionState>) {
    32     this.effectWorking$ = this.store.select(questionStateQuery.effectWorking).pipe(
    33       filter((effect) => effect !== null),
    34       map((effect) => {
    35         if (effect instanceof HttpErrorResponse) {
    36           throw effect;
    37         } else {
    38           return effect;
    39         }
    40       }),
    41       catchError((err) => {
    42         return throwError(err);
    43       })
    44     );
     35  constructor(private store: Store<QuestionState>, private auth: AuthService) {
     36    this.effectWorking$ = this.store.select(questionStateQuery.effectWorking).pipe(filter((effect) => effect !== null));
     37  }
     38
     39  public currentQuestionOwnedByCurrentUser(): Observable<boolean> {
     40    return this.getQuestion().pipe(map((question) => this.auth.selfUser?.student?.uid === question.student.uid));
    4541  }
    4642
     
    8177  }
    8278
     79  public setCorrectAnswer(questionUid: string, answerUid: string): void {
     80    this.dispatchEffect(new SetCorrectAnswer(questionUid, answerUid));
     81  }
     82
     83  public voteAnswer(answerUid: string, questionUid: string, voteType: VoteType): void {
     84    this.dispatchEffect(new VoteAnswer(questionUid, answerUid, voteType));
     85  }
     86
    8387  public fetchQuestion(questionUid: string): void {
    8488    this.dispatchEffect(new GetQuestionState(questionUid));
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question-state-response.models.ts

    r1e0d869 r6901f8b  
     1import { VoteType } from 'src/app/shared-app/models';
     2
    13export class QuestionStateResponse {
    24  public uid!: string;
     
    3436  public correctAnswer!: boolean;
    3537  public createdOn!: moment.Moment;
    36   public upvotesCount!: number;
     38  public votesCount!: number;
    3739  public studentResponse!: AnswerStudentQuestionStateResponse;
    3840  public answerResponsesResponse!: AnswerResponseQuestionStateResponse[];
     
    7375  public text!: string;
    7476}
     77
     78export class VoteAnswerResponse {
     79  public answerUid!: string;
     80  public voteUid!: string;
     81  public voteType!: VoteType;
     82}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.actions.ts

    r1e0d869 r6901f8b  
    22import { Action } from '@ngrx/store';
    33
    4 import { PreviewQuestionViewModel, QuestionStateViewModel, SearchQuestionsQueryViewModel } from 'src/app/shared-app/models';
     4import {
     5  PreviewQuestionViewModel,
     6  QuestionStateViewModel,
     7  SearchQuestionsQueryViewModel,
     8  VoteAnswerViewModel,
     9  VoteType
     10} from 'src/app/shared-app/models';
    511
    612export enum QuestionActionTypes {
    713  GetQuestionState = '[Question] Get state',
    814  GetQuestionStateSuccess = '[Question] Get state success',
     15  SetCorrectAnswer = '[Question] Set Correct Answer',
     16  SetCorrectAnswerSuccess = '[Question] Set Correct Answer success',
    917  GetPreviewQuestionsLatest = '[Question] Get preview questions Latest',
    1018  GetPreviewQuestionsLatestSuccess = '[Question] Get preview questions Latest Success',
     
    1321  GetSearchQuestions = '[Question] Get search questions',
    1422  GetSearchQuestionsSuccess = '[Question] Get search questions Success',
     23  VoteAnswer = '[Question] Vote answer',
     24  VoteAnswerSuccess = '[Question] Vote answer Success',
    1525  EffectStartedWorking = '[Question] Effect Started Working',
    1626  EffectFinishedWorking = '[Question] Effect Finished Working',
     
    2838
    2939  constructor(public payload: QuestionStateViewModel) {}
     40}
     41
     42export class SetCorrectAnswer implements Action {
     43  readonly type = QuestionActionTypes.SetCorrectAnswer;
     44
     45  constructor(public questionUid: string, public answerUid: string) {}
     46}
     47
     48export class SetCorrectAnswerSuccess implements Action {
     49  readonly type = QuestionActionTypes.SetCorrectAnswerSuccess;
     50
     51  constructor(public payload: string) {}
    3052}
    3153
     
    6688}
    6789
     90export class VoteAnswer implements Action {
     91  readonly type = QuestionActionTypes.VoteAnswer;
     92
     93  constructor(public questionUid: string, public answerUid: string, public voteType: VoteType) {}
     94}
     95
     96export class VoteAnswerSuccess implements Action {
     97  readonly type = QuestionActionTypes.VoteAnswerSuccess;
     98
     99  constructor(public payload: VoteAnswerViewModel) {}
     100}
     101
    68102export class EffectStartedWorking implements Action {
    69103  readonly type = QuestionActionTypes.EffectStartedWorking;
     
    89123  | GetPreviewQuestionsPopularSuccess
    90124  | GetSearchQuestionsSuccess
     125  | VoteAnswerSuccess
     126  | SetCorrectAnswerSuccess
    91127  | EffectStartedWorking
    92128  | EffectFinishedWorking
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.effects.ts

    r1e0d869 r6901f8b  
    11import { Injectable } from '@angular/core';
    22import { act, Actions, createEffect, ofType } from '@ngrx/effects';
    3 import { catchError, filter, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators';
     3import { catchError, filter, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators';
    44import { PreviewQuestionsOrderEnum, SearchQuestionsQueryViewModel } from 'src/app/shared-app/models';
    55import { TranslateFromJsonService } from 'src/app/shared-app/services';
    66
    77import { BaseApiService } from 'src/app/shared-app/services/base-api.service';
     8import { NotificationService } from '../../services/notification.service';
    89import { QuestionFacadeService } from '../question-facade.service';
    9 import { PreviewQuestionResponse, QuestionStateResponse } from './question-state.models';
     10import { VoteAnswerRequest } from './question-state-request.models';
     11import { PreviewQuestionResponse, QuestionStateResponse, VoteAnswerResponse } from './question-state-response.models';
    1012import {
    1113  EffectFinishedWorking,
     
    1921  GetSearchQuestions,
    2022  GetSearchQuestionsSuccess,
    21   QuestionActionTypes
     23  QuestionActionTypes,
     24  SetCorrectAnswer,
     25  SetCorrectAnswerSuccess,
     26  VoteAnswer,
     27  VoteAnswerSuccess
    2228} from './question.actions';
    2329import { QuestionMapper } from './question.mapper';
     
    3137    private api: BaseApiService,
    3238    private translate: TranslateFromJsonService,
    33     private facade: QuestionFacadeService
     39    private facade: QuestionFacadeService,
     40    private notification: NotificationService
    3441  ) {}
    3542
     
    103110    );
    104111  });
     112
     113  voteAnswer$ = createEffect(() => {
     114    return this.actions$.pipe(
     115      ofType<VoteAnswer>(QuestionActionTypes.VoteAnswer),
     116      mergeMap((action) => {
     117        const body = new VoteAnswerRequest(action.voteType);
     118        return this.api.post<VoteAnswerResponse>(`v1/questions/${action.questionUid}/answers/${action.answerUid}/votes`, body).pipe(
     119          tap((state) => this.notification.successNotification('sucess-vote')),
     120          switchMap((state) => [new VoteAnswerSuccess(QuestionMapper.ToVoteAnswerViewModel(state)), new EffectFinishedWorking()]),
     121          catchError((err) => [new EffectFinishedWorkingError(err)])
     122        );
     123      })
     124    );
     125  });
     126
     127  setCorrectAnswer$ = createEffect(() => {
     128    return this.actions$.pipe(
     129      ofType<SetCorrectAnswer>(QuestionActionTypes.SetCorrectAnswer),
     130      mergeMap((action) => {
     131        return this.api.put<string>(`v1/questions/${action.questionUid}/answers/${action.answerUid}/correct`).pipe(
     132          tap((state) => this.notification.successNotification('success-correct-answer')),
     133          switchMap((state) => [new SetCorrectAnswerSuccess(state), new EffectFinishedWorking()]),
     134          catchError((err) => [new EffectFinishedWorkingError(err)])
     135        );
     136      })
     137    );
     138  });
    105139}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.mapper.ts

    r1e0d869 r6901f8b  
    1010  QuestionStateViewModel,
    1111  StudentQuestionStateViewModel,
    12   TeamQuestionStateViewModel
     12  TeamQuestionStateViewModel,
     13  VoteAnswerViewModel
    1314} from 'src/app/shared-app/models';
    1415import { TranslateFromJsonService } from 'src/app/shared-app/services';
    15 import { PreviewQuestionResponse, QuestionStateResponse } from './question-state.models';
     16import { PreviewQuestionResponse, QuestionStateResponse, VoteAnswerResponse } from './question-state-response.models';
    1617
    1718export class QuestionMapper {
     
    5152          x.correctAnswer,
    5253          moment(x.createdOn),
    53           x.upvotesCount,
     54          x.votesCount,
    5455          answerStudent,
    5556          answerResponses
     
    113114    return questions;
    114115  }
     116
     117  public static ToVoteAnswerViewModel(response: VoteAnswerResponse): VoteAnswerViewModel {
     118    return new VoteAnswerViewModel(response.answerUid, response.voteUid, response.voteType);
     119  }
    115120}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.reducers.ts

    r1e0d869 r6901f8b  
     1import { AnswerQuestionStateViewModel, VoteType } from 'src/app/shared-app/models';
    12import { QuestionAction, QuestionActionTypes } from './question.actions';
    23import { initialState, QuestionState } from './question.state';
     
    2526        searchQuestionsQuery: action.query
    2627      };
     28    case QuestionActionTypes.VoteAnswerSuccess: {
     29      if (state.question) {
     30        return {
     31          ...state,
     32          question: {
     33            ...state.question,
     34            answers: state.question.answers.map((x) => {
     35              if (x.uid === action.payload.answerUid) {
     36                let votesCountNew = x.votesCount;
     37
     38                switch (action.payload.voteType) {
     39                  case VoteType.Upvote:
     40                    votesCountNew++;
     41                    break;
     42                  case VoteType.Downvote:
     43                    votesCountNew--;
     44                    break;
     45                }
     46
     47                return {
     48                  ...x,
     49                  votesCount: votesCountNew
     50                };
     51              }
     52
     53              return x;
     54            })
     55          }
     56        };
     57      }
     58
     59      return {
     60        ...state
     61      };
     62    }
     63    case QuestionActionTypes.SetCorrectAnswerSuccess: {
     64      if (state.question) {
     65        return {
     66          ...state,
     67          question: {
     68            ...state.question,
     69            answers: state.question.answers.map((x) => {
     70              if (x.correctAnswer) {
     71                return {
     72                  ...x,
     73                  correctAnswer: false
     74                };
     75              }
     76              if (x.uid === action.payload) {
     77                return {
     78                  ...x,
     79                  correctAnswer: true
     80                };
     81              }
     82              return x;
     83            })
     84          }
     85        };
     86      }
     87
     88      return {
     89        ...state
     90      };
     91    }
    2792    case QuestionActionTypes.EffectStartedWorking: {
    2893      return {
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/generic/vote/vote.component.html

    r1e0d869 r6901f8b  
    1 <div>
     1<div class="main-div">
    22  <div class="vote-icons" fxLayout="column" fxLayoutAlign="center center">
    33    <mat-icon class="vote-icon" (click)="voted(VoteType.Upvote)" [inline]="true">arrow_drop_up</mat-icon>
     
    77      >check</mat-icon
    88    >
     9    <mat-icon
     10      *ngIf="canSetCorrectAnswer && !correct"
     11      [inline]="true"
     12      class="text-center text-bold green set-correct-answer"
     13      (click)="setCorrectAnswer.emit()"
     14      >check</mat-icon
     15    >
    916  </div>
    1017</div>
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/generic/vote/vote.component.scss

    r1e0d869 r6901f8b  
    1414  cursor: pointer;
    1515}
     16
     17.show-on-hover {
     18  display: none;
     19}
     20
     21.set-correct-answer {
     22  opacity: 0.3;
     23  visibility: hidden;
     24}
     25
     26.main-div:hover > .vote-icons > .set-correct-answer {
     27  visibility: visible;
     28}
     29
     30.main-div {
     31  height: 100%;
     32}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/generic/vote/vote.component.ts

    r1e0d869 r6901f8b  
    11import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
     2import { VoteType } from 'src/app/shared-app/models';
    23import { ButtonType } from '../button/button.models';
    3 
    4 export enum VoteType {
    5   Upvote,
    6   Downvote
    7 }
    84
    95@Component({
     
    1410export class VoteComponent implements OnInit {
    1511  @Input() voteCount: number | undefined;
     12  @Input() canSetCorrectAnswer: boolean | null = false;
    1613  @Input() correct = false;
    1714  @Output() voteClicked = new EventEmitter<VoteType>();
     15  @Output() setCorrectAnswer = new EventEmitter<void>();
    1816
    1917  VoteType = VoteType;
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/question/question-preview/question-preview.component.html

    r1e0d869 r6901f8b  
    1414      <mat-card-content>
    1515        <div fxLayout="row wrap" fxLayoutAlign="space-around none">
    16           <app-vote [voteCount]="answer.upvotesCount" [correct]="answer.correctAnswer" fxFlex="6%"></app-vote>
     16          <app-vote
     17            [canSetCorrectAnswer]="canSetCorrectAnswer | async"
     18            [voteCount]="answer.votesCount"
     19            [correct]="answer.correctAnswer"
     20            (voteClicked)="answerVoted($event, answer.uid, question.uid)"
     21            (setCorrectAnswer)="setCorrectAnswer(question.uid, answer.uid)"
     22            fxFlex="6%"
     23          ></app-vote>
    1724          <div fxFlex="92%">
    1825            <app-text-editor
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/question/question-preview/question-preview.component.ts

    r1e0d869 r6901f8b  
     1import { HttpErrorResponse } from '@angular/common/http';
    12import { Component, OnInit } from '@angular/core';
     3import { NotificationService } from 'src/app/core/services/notification.service';
    24import { QuestionFacadeService } from 'src/app/core/state/question-facade.service';
    35
    4 import { QuestionStateViewModel } from 'src/app/shared-app/models';
     6import { QuestionStateViewModel, VoteType } from 'src/app/shared-app/models';
    57import { ButtonType } from '../../generic/button/button.models';
    68
     
    1113})
    1214export class QuestionPreviewComponent implements OnInit {
     15  canSetCorrectAnswer = this.questionFacade.currentQuestionOwnedByCurrentUser();
    1316  question!: QuestionStateViewModel;
    1417  working = true;
    1518  ButtonType = ButtonType;
    16   constructor(private questionFacade: QuestionFacadeService) {}
     19  constructor(private questionFacade: QuestionFacadeService, private notification: NotificationService) {}
    1720
    1821  ngOnInit(): void {
     
    2124      this.working = false;
    2225    });
     26
     27    this.questionFacade.effectWorking$.subscribe((effect) => {
     28      if (effect instanceof HttpErrorResponse) {
     29        this.notification.handleErrorsNotification(effect.error);
     30      }
     31    });
     32  }
     33
     34  answerVoted(voteType: VoteType, answerUid: string, questionUid: string): void {
     35    this.questionFacade.voteAnswer(answerUid, questionUid, voteType);
     36  }
     37
     38  setCorrectAnswer(questionUid: string, answerUid: string): void {
     39    this.questionFacade.setCorrectAnswer(questionUid, answerUid);
    2340  }
    2441}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/models/question-state-enums.models.ts

    r1e0d869 r6901f8b  
    33  Popular
    44}
     5
     6export enum VoteType {
     7  Upvote,
     8  Downvote
     9}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/models/question-state-view-models.models.ts

    r1e0d869 r6901f8b  
     1import { VoteType } from '.';
     2
    13export class QuestionStateViewModel {
    24  constructor(
     
    3234    public correctAnswer: boolean,
    3335    public createdOn: moment.Moment,
    34     public upvotesCount: number,
     36    public votesCount: number,
    3537    public student: AnswerStudentQuestionStateViewModel,
    3638    public answerResponses: AnswerResponseQuestionStateViewModel[]
     
    7375  constructor(public text: string) {}
    7476}
     77
     78export class VoteAnswerViewModel {
     79  constructor(public answerUid: string, public voteUid: string, public voteType: VoteType) {}
     80}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/models/user.models.ts

    r1e0d869 r6901f8b  
    99}
    1010
    11 export class SelfUserResponse {
    12   uid!: string;
    13 }
    14 
    1511export enum ApplicationUserType {
    1612  Student = 'Student',
     
    1915  Guest = 'Guest'
    2016}
     17
     18export class SelfUserResponse {
     19  public student?: StudentSelfResponse | null;
     20  public teacher?: TeacherSelfResponse | null;
     21  public moderator?: ModeratorSelfResponse | null;
     22}
     23
     24export class StudentSelfResponse {
     25  public uid!: string;
     26  public applicationUserId!: number;
     27  public index!: string;
     28  public reputation!: number;
     29  public imageUrl!: string;
     30  public questions!: StudentQuestionResponse[];
     31  public teams!: StudentTeamResponse[];
     32}
     33
     34export class StudentQuestionResponse {
     35  public questionUid!: string;
     36  public title!: string;
     37}
     38
     39export class StudentTeamResponse {
     40  public teamUid!: string;
     41  public name!: string;
     42}
     43
     44export class ModeratorSelfResponse {
     45  public uid!: string;
     46  public applicationUserId!: number;
     47}
     48
     49export class TeacherSelfResponse {
     50  public uid!: string;
     51  public applicationUserId!: number;
     52  public teams!: TeacherTeamResponse[];
     53}
     54
     55export class TeacherTeamResponse {
     56  public teamUid!: string;
     57  public name!: string;
     58}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/services/base-api.service.ts

    r1e0d869 r6901f8b  
    1919
    2020  public getSelfUser(): Observable<SelfUserResponse> {
    21     return this.get<SelfUserResponse>('self');
     21    return this.get<SelfUserResponse>('v1/self');
    2222  }
    2323
  • src/Clients/Angular/finki-chattery/src/assets/translations/en.json

    r1e0d869 r6901f8b  
    5353  "ask-question-stepper-ask": "Ask question",
    5454  "ask-question-ask-button-back": "Edit question",
    55   "ask-question-ask-button": "Ask question"
     55  "sucess-vote": "Successfully voted answer",
     56  "success-correct-answer": "Successfully set correct answer",
     57  "ask-question-ask-button": "Ask question",
     58  "AnswerAlreadyUpvoted": "You have already upvoted this answer",
     59  "AnswerAlreadyDownvoted": "You have already downvoted this answer",
     60  "StudentHasBadReputation": "You have bad reputation and can not vote"
    5661}
Note: See TracChangeset for help on using the changeset viewer.