Changeset 7899209


Ignore:
Timestamp:
10/27/21 19:32:04 (3 years ago)
Author:
Стојков Марко <mst@…>
Branches:
dev
Children:
6b0fbbe, 79ae621
Parents:
466d1ac (diff), a3b5f34 (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 feature/search-questions into dev

Location:
src
Files:
5 added
18 edited

Legend:

Unmodified
Added
Removed
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-facade.service.ts

    r466d1ac r7899209  
    55import { catchError, filter, map } from 'rxjs/operators';
    66
    7 import { PreviewQuestionsOrderEnum, PreviewQuestionViewModel, QuestionStateViewModel } from 'src/app/shared-app/models';
     7import {
     8  PreviewQuestionsOrderEnum,
     9  PreviewQuestionViewModel,
     10  QuestionStateViewModel,
     11  SearchQuestionsQueryViewModel
     12} from 'src/app/shared-app/models';
    813import {
    914  EffectStartedWorking,
    1015  GetPreviewQuestionsLatest,
    1116  GetPreviewQuestionsPopular,
    12   GetQuestionState
     17  GetQuestionState,
     18  GetSearchQuestions
    1319} from './question-state/question.actions';
    1420import { questionStateQuery } from './question-state/question.selectors';
     
    4349  }
    4450
     51  public getSearchQuestions(): Observable<PreviewQuestionViewModel[]> {
     52    return this.store.select(questionStateQuery.getSearchQuestions);
     53  }
     54
     55  public getSearchQuestionsQuery(): Observable<SearchQuestionsQueryViewModel> {
     56    return this.store
     57      .select(questionStateQuery.getSearchQuestionsQuery)
     58      .pipe(filter((x: SearchQuestionsQueryViewModel | null): x is SearchQuestionsQueryViewModel => x !== null));
     59  }
     60
    4561  public getPreviewQuestionsLatest(): Observable<PreviewQuestionViewModel[]> {
    4662    return this.store.select(questionStateQuery.getPreviewQuestionsLatest);
     
    6379  }
    6480
     81  public searchQuestions(searchText: string, categories: string[]): void {
     82    this.dispatchEffect(new GetSearchQuestions(searchText, categories));
     83  }
     84
    6585  private fetchPreviewQuestionsLatest(): void {
    6686    this.dispatchEffect(new GetPreviewQuestionsLatest());
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.actions.ts

    r466d1ac r7899209  
    22import { Action } from '@ngrx/store';
    33
    4 import { PreviewQuestionViewModel, QuestionStateViewModel } from 'src/app/shared-app/models';
    5 import { PreviewQuestionResponse } from './question-state.models';
     4import { PreviewQuestionViewModel, QuestionStateViewModel, SearchQuestionsQueryViewModel } from 'src/app/shared-app/models';
    65
    76export enum QuestionActionTypes {
     
    1211  GetPreviewQuestionsPopular = '[Question] Get preview questions Popular',
    1312  GetPreviewQuestionsPopularSuccess = '[Question] Get preview questions Popular Success',
     13  GetSearchQuestions = '[Question] Get search questions',
     14  GetSearchQuestionsSuccess = '[Question] Get search questions Success',
    1415  EffectStartedWorking = '[Question] Effect Started Working',
    1516  EffectFinishedWorking = '[Question] Effect Finished Working',
     
    5354}
    5455
     56export class GetSearchQuestions implements Action {
     57  readonly type = QuestionActionTypes.GetSearchQuestions;
     58
     59  constructor(public searchText: string, public categories: string[]) {}
     60}
     61
     62export class GetSearchQuestionsSuccess implements Action {
     63  readonly type = QuestionActionTypes.GetSearchQuestionsSuccess;
     64
     65  constructor(public payload: PreviewQuestionViewModel[], public query: SearchQuestionsQueryViewModel) {}
     66}
     67
    5568export class EffectStartedWorking implements Action {
    5669  readonly type = QuestionActionTypes.EffectStartedWorking;
     
    7588  | GetPreviewQuestionsLatestSuccess
    7689  | GetPreviewQuestionsPopularSuccess
     90  | GetSearchQuestionsSuccess
    7791  | EffectStartedWorking
    7892  | EffectFinishedWorking
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.effects.ts

    r466d1ac r7899209  
    11import { Injectable } from '@angular/core';
    2 import { Actions, createEffect, ofType } from '@ngrx/effects';
    3 import { catchError, filter, switchMap, withLatestFrom } from 'rxjs/operators';
    4 import { PreviewQuestionsOrderEnum } from 'src/app/shared-app/models';
     2import { act, Actions, createEffect, ofType } from '@ngrx/effects';
     3import { catchError, filter, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators';
     4import { PreviewQuestionsOrderEnum, SearchQuestionsQueryViewModel } from 'src/app/shared-app/models';
    55import { TranslateFromJsonService } from 'src/app/shared-app/services';
    66
     
    1111  EffectFinishedWorking,
    1212  EffectFinishedWorkingError,
     13  GetPreviewQuestionsLatest,
    1314  GetPreviewQuestionsLatestSuccess,
     15  GetPreviewQuestionsPopular,
    1416  GetPreviewQuestionsPopularSuccess,
    1517  GetQuestionState,
    1618  GetQuestionStateSuccess,
     19  GetSearchQuestions,
     20  GetSearchQuestionsSuccess,
    1721  QuestionActionTypes
    1822} from './question.actions';
     
    4448  getPreviewQuestionsLatest$ = createEffect(() => {
    4549    return this.actions$.pipe(
    46       ofType<GetQuestionState>(QuestionActionTypes.GetPreviewQuestionsLatest),
     50      ofType<GetPreviewQuestionsLatest>(QuestionActionTypes.GetPreviewQuestionsLatest),
    4751      withLatestFrom(this.facade.getPreviewQuestionsLatest()),
    4852      filter(([action, questions]) => questions.length === 0),
     
    6165  getPreviewQuestionsPopular$ = createEffect(() => {
    6266    return this.actions$.pipe(
    63       ofType<GetQuestionState>(QuestionActionTypes.GetPreviewQuestionsPopular),
     67      ofType<GetPreviewQuestionsPopular>(QuestionActionTypes.GetPreviewQuestionsPopular),
    6468      withLatestFrom(this.facade.getPreviewQuestionsPopular()),
    6569      filter(([action, questions]) => questions.length === 0),
     
    7579    );
    7680  });
     81
     82  getSearchQuestions$ = createEffect(() => {
     83    return this.actions$.pipe(
     84      ofType<GetSearchQuestions>(QuestionActionTypes.GetSearchQuestions),
     85      mergeMap((action) => {
     86        const categoriesAsString = action.categories !== null ? action.categories.join(',') : '';
     87        return this.api
     88          .get<PreviewQuestionResponse[]>(`v1/questions/search?searchText=${action.searchText}&categories=${categoriesAsString}`)
     89          .pipe(
     90            switchMap((state) => [
     91              new GetSearchQuestionsSuccess(
     92                QuestionMapper.ToPreviwQuestionsViewModel(state, this.translate),
     93                new SearchQuestionsQueryViewModel(action.searchText)
     94              ),
     95              new EffectFinishedWorking()
     96            ]),
     97            catchError((err) => [new EffectFinishedWorkingError(err)])
     98          );
     99      })
     100    );
     101  });
    77102}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.reducers.ts

    r466d1ac r7899209  
    1818        ...state,
    1919        previewQuestionsPopular: action.payload
     20      };
     21    case QuestionActionTypes.GetSearchQuestionsSuccess:
     22      return {
     23        ...state,
     24        searchQuestions: action.payload,
     25        searchQuestionsQuery: action.query
    2026      };
    2127    case QuestionActionTypes.EffectStartedWorking: {
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.selectors.ts

    r466d1ac r7899209  
    77const getPreviewQuestionsLatest = createSelector(getQuestionState, (state) => state.previewQuestionsLatest);
    88const getPreviewQuestionsPopular = createSelector(getQuestionState, (state) => state.previewQuestionsPopular);
     9const getSearchQuestions = createSelector(getQuestionState, (state) => state.searchQuestions);
     10const getSearchQuestionsQuery = createSelector(getQuestionState, (state) => state.searchQuestionsQuery);
    911const effectWorking = createSelector(getQuestionState, (state) => state.effectWorking);
    1012
     
    1315  getQuestion,
    1416  getPreviewQuestionsLatest,
    15   getPreviewQuestionsPopular
     17  getPreviewQuestionsPopular,
     18  getSearchQuestions,
     19  getSearchQuestionsQuery
    1620};
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.state.ts

    r466d1ac r7899209  
    11import { HttpErrorResponse } from '@angular/common/http';
    2 import { PreviewQuestionViewModel, QuestionStateViewModel } from 'src/app/shared-app/models';
     2import { PreviewQuestionViewModel, QuestionStateViewModel, SearchQuestionsQueryViewModel } from 'src/app/shared-app/models';
    33
    44export const questionStateKey = 'question';
     
    99  previewQuestionsPopular: PreviewQuestionViewModel[];
    1010  effectWorking: boolean | HttpErrorResponse;
     11  searchQuestions: PreviewQuestionViewModel[];
     12  searchQuestionsQuery: SearchQuestionsQueryViewModel | null;
    1113}
    1214
     
    1517  previewQuestionsLatest: [],
    1618  previewQuestionsPopular: [],
    17   effectWorking: false
     19  searchQuestions: [],
     20  effectWorking: false,
     21  searchQuestionsQuery: null
    1822};
  • src/Clients/Angular/finki-chattery/src/app/modules/questioning/components/questioning-components.ts

    r466d1ac r7899209  
    22import { QuestioningGeneralComponent } from './questioning-general/questioning-general.component';
    33import { QuestionsPreviewGeneralComponent } from './questions-preview-general/questions-preview-general.component';
     4import { QuestionsSearchComponent } from './questions-search/questions-search.component';
    45
    56export const QUESTIONING_COMPONENTS: any[] = [
    67  QuestionPreviewGeneralComponent,
    78  QuestionsPreviewGeneralComponent,
    8   QuestioningGeneralComponent
     9  QuestioningGeneralComponent,
     10  QuestionsSearchComponent
    911];
  • src/Clients/Angular/finki-chattery/src/app/modules/questioning/components/questions-preview-general/questions-preview-general.component.html

    r466d1ac r7899209  
    1 <app-search-question></app-search-question>
     1<app-search-question (searched)="routeToSearch()"></app-search-question>
    22<div class="margin-x-lg">
    33  <h1 class="mat-headline">{{ 'questions-preview' | translate }}</h1>
  • src/Clients/Angular/finki-chattery/src/app/modules/questioning/components/questions-preview-general/questions-preview-general.component.ts

    r466d1ac r7899209  
    3636    this.router.navigateByUrl(`questioning/${uid}`);
    3737  }
     38
     39  routeToSearch(): void {
     40    this.router.navigateByUrl(`questioning/search`);
     41  }
    3842}
  • src/Clients/Angular/finki-chattery/src/app/modules/questioning/questioning.routes.ts

    r466d1ac r7899209  
    44import { QuestioningGeneralComponent } from './components/questioning-general/questioning-general.component';
    55import { QuestionsPreviewGeneralComponent } from './components/questions-preview-general/questions-preview-general.component';
     6import { QuestionsSearchComponent } from './components/questions-search/questions-search.component';
    67
    78const routes: Routes = [
     
    1415        pathMatch: 'full',
    1516        component: QuestionsPreviewGeneralComponent
     17      },
     18      {
     19        path: 'search',
     20        pathMatch: 'full',
     21        component: QuestionsSearchComponent
    1622      },
    1723      {
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/question/search-question/search-question.component.html

    r466d1ac r7899209  
    1111      <mat-icon matSuffix>search</mat-icon>
    1212    </mat-form-field>
    13     <mat-form-field appearance="fill">
     13    <mat-form-field class="full-width" appearance="fill">
    1414      <mat-select
    1515        [formControl]="questionCategoriesFormContor"
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/question/search-question/search-question.component.ts

    r466d1ac r7899209  
    1 import { Component, OnInit } from '@angular/core';
     1import { Component, EventEmitter, OnInit, Output } from '@angular/core';
    22import { FormControl, Validators } from '@angular/forms';
     3
    34import { CategoryFacadeService } from 'src/app/core/state/category-facade.service';
    4 import { CategoryStateViewModel } from 'src/app/shared-app/models';
     5import { QuestionFacadeService } from 'src/app/core/state/question-facade.service';
    56import { ButtonType } from '../../generic/button/button.models';
    67
     
    1112})
    1213export class SearchQuestionComponent implements OnInit {
     14  @Output() searched = new EventEmitter();
     15
    1316  ButtonType = ButtonType;
    1417  questionSearchFormContor = new FormControl('', [Validators.required, Validators.maxLength(250)]);
     
    1619  categories$ = this.categoriesFacade.getCategories();
    1720
    18   constructor(private categoriesFacade: CategoryFacadeService) {}
     21  constructor(private categoriesFacade: CategoryFacadeService, private questionsFacade: QuestionFacadeService) {}
    1922
    2023  ngOnInit(): void {}
     
    2730
    2831  public searchQuestions(): void {
    29     alert('SEARCH');
     32    this.questionsFacade.searchQuestions(this.questionSearchFormContor.value, this.questionCategoriesFormContor.value);
     33    this.searched.emit();
    3034  }
    3135}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/models/question-state-view-models.models.ts

    r466d1ac r7899209  
    6969  constructor(public uid: string, public text: string, public nameTranslated: string) {}
    7070}
     71
     72export class SearchQuestionsQueryViewModel {
     73  constructor(public text: string) {}
     74}
  • src/Clients/Angular/finki-chattery/src/assets/translations/en.json

    r466d1ac r7899209  
    4343  "questions-preview-question-subtitle": "Asked on: {{date}}",
    4444  "questions-preview-question-answers": "Answers",
    45   "questions-preview-question-views": "Views"
     45  "questions-preview-question-views": "Views",
     46  "questions-search-title": "Search results for: {{searchQuery}}"
    4647}
  • src/FinkiChattery/FinkiChattery.Api/Controllers/v1/QuestionsController.cs

    r466d1ac r7899209  
    3535        [HttpGet("{questionUid:Guid}")]
    3636        [Authorize]
    37         public async Task<IActionResult> GetQuestionState([FromRoute]Guid questionUid)
     37        public async Task<IActionResult> GetQuestionState([FromRoute] Guid questionUid)
    3838        {
    3939            var questionDto = await MediatorService.SendQueryAsync(new GetQuestionStateQuery(questionUid));
     
    4848            return Ok(questions.ToPreviewQuestionsResponse());
    4949        }
     50
     51        [HttpGet("search")]
     52        [Authorize(AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme, Policy = AuthenticationPolicy.Student)]
     53        public async Task<IActionResult> SearchQuestions([FromQuery] string searchText, [FromQuery] string categories)
     54        {
     55            var questions = await MediatorService.SendQueryAsync(new SearchQuestionsQuery(searchText, categories));
     56            return Ok(questions.ToPreviewQuestionsResponse());
     57        }
    5058    }
    5159}
  • src/FinkiChattery/FinkiChattery.Database/FullTextSearch/FullTextIndexQuestion.sql

    r466d1ac r7899209  
    11CREATE FULLTEXT INDEX ON [dbo].[Question] ([Search])
    22KEY INDEX [PK_Question] ON [QuestionFullTextCatalog]
    3 WITH (CHANGE_TRACKING AUTO)
     3WITH (CHANGE_TRACKING AUTO, STOPLIST OFF)
  • src/FinkiChattery/FinkiChattery.Persistence/Repositories/Contracts/IQuestionRepo.cs

    r466d1ac r7899209  
    99    public interface IQuestionRepo : IRepository<Question>
    1010    {
     11        Task<List<QuestionPreviewDto>> SearchQuestions(string searchText, IEnumerable<Guid> categories);
     12
    1113        Task<QuestionStateDto> GetQuestionState(Guid questionUid);
    1214
  • src/FinkiChattery/FinkiChattery.Persistence/Repositories/Implementations/QuestionRepo.cs

    r466d1ac r7899209  
    22using FinkiChattery.Persistence.Models;
    33using FinkiChattery.Persistence.Repositories.Contracts;
     4using Microsoft.Data.SqlClient;
    45using Microsoft.EntityFrameworkCore;
    56using System;
     
    78using System.Linq;
    89using System.Linq.Expressions;
     10using System.Text.RegularExpressions;
    911using System.Threading.Tasks;
    1012
     
    110112            return questionDto;
    111113        }
     114
     115        public async Task<List<QuestionPreviewDto>> SearchQuestions(string searchText, IEnumerable<Guid> categories)
     116        {
     117            var search = Regex.Replace(searchText, "[\\\\/:*?\"<>\\]\\[|&'`~^=%,(){}_\\-]", " ")
     118                                     .Split(" ".ToArray(), StringSplitOptions.RemoveEmptyEntries)
     119                                     .Select(c => $"\"{c}*\"");
     120
     121            var searchString = string.Join(" AND ", search);
     122
     123            var rawQuery = (IQueryable<Question>)
     124                DbSet.FromSqlRaw(@"
     125                        SELECT [q].[Id], [q].[Uid], [q].[Title], [q].[Views], [q].[AnswersCount], [q].[CreatedOn]
     126                        FROM [dbo].[Question] AS [q]
     127                        INNER JOIN CONTAINSTABLE(Question, Search, @searchString, 30) ccontains ON [q].[Id] = ccontains.[KEY]",
     128                        new SqlParameter("searchString", searchString))
     129                .Include(x => x.QuestionCategories).ThenInclude(x => x.Category);
     130
     131            if (categories.Any())
     132            {
     133                rawQuery = rawQuery.Where(x => x.QuestionCategories.Any(y => categories.Contains(y.Category.Uid)));
     134            }
     135
     136            return await rawQuery
     137                .Select(x => new QuestionPreviewDto(x.Id,
     138                                    x.Uid,
     139                                    x.Title,
     140                                    x.Views,
     141                                    x.AnswersCount,
     142                                    x.CreatedOn,
     143                                    x.QuestionCategories.Select(y => new QuestionPreviewCategoryDto(y.Id, y.Uid, y.Category.Name))))
     144                .ToListAsync();
     145        }
    112146    }
    113147}
Note: See TracChangeset for help on using the changeset viewer.