From a6959687dbd5239c615c1670c28bed5aafecc2f2 Mon Sep 17 00:00:00 2001 From: Jannik Pulfer <109959802+RandomTannenbaum@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:17:13 +0200 Subject: [PATCH] Feature/652-readd-cv-search (#664) * Modify old search controller to not send json and add view with non-functional searchbar & routes for this new cv search controller * Add message if there are no results and make searchbar functional * Style search results to make them look like in the old version of skills * Make search results display name and found in value and change oninput to onchange * Reload serach-results via turboframe and change onchange to oninput * Create stimulus controller to add a short timeout before submitting, use this controller in the cv-search search-field and update stimulus manifest * Add translations for cv search and implement translation of results in view * Filter results from cv-search that have nil values as found_in and convert translation keys in cv search index view to snake case * Change translation key from camel to snake case in de.yml * Add link to cv search to header * Make clicking on search result preserve query params and implement highlighting of search results * Remove unnecessary stimulus controller action and write same functionality directly in view * Shorten filter method that filters out nil values for found_in in people_search controller * Make cv search form only submit if value length > 3 * Write cv search specs, add new missing translations and make search result link not trigger turbo-frame * Move logic that excludes results for searches with length < 3 to view * Add spec that asserts that results are only displayed with a search text length > 3 * Replace hardcoded route with path helper in header entry of cv search * Instead of not showing results for queries with length < 3 just return empty array in controller * Nest hover styles into normal styles * Move logic that decides wether or not to do cv search from controller to concern * Move logic that translates the found_in value and the one that creates a person link with search query from cv search view to a new helper * Reduce timeout in cv search from 400ms to 100ms * Populate value from query-params in cv search field on reload * Resolve rubocop offence * Move should search method from concern to controller --- app/assets/stylesheets/styles.scss | 23 ++++++- app/controllers/cv_search_controller.rb | 21 ++++++ app/domain/people_search.rb | 1 + app/helpers/cv_search_helper.rb | 11 ++++ .../controllers/highlight_controller.js | 10 +++ app/javascript/controllers/index.js | 28 ++++---- .../controllers/search_controller.js | 20 ++++++ app/views/cv_search/index.html.haml | 16 +++++ app/views/layouts/application.html.haml | 4 +- app/views/people/show.html.haml | 2 +- config/locales/de.yml | 11 ++++ config/routes.rb | 2 + spec/features/cv_search_spec.rb | 64 +++++++++++++++++++ 13 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 app/controllers/cv_search_controller.rb create mode 100644 app/helpers/cv_search_helper.rb create mode 100644 app/javascript/controllers/highlight_controller.js create mode 100644 app/javascript/controllers/search_controller.js create mode 100644 app/views/cv_search/index.html.haml create mode 100644 spec/features/cv_search_spec.rb diff --git a/app/assets/stylesheets/styles.scss b/app/assets/stylesheets/styles.scss index 727a09018..efcff8157 100644 --- a/app/assets/stylesheets/styles.scss +++ b/app/assets/stylesheets/styles.scss @@ -1,6 +1,9 @@ $skills-blue: #3b7bbe; $skills-text-gray: #8e8e8e; $skills-gray: #f5f5f5; +$skills-green: #69b978; +$skills-green-hover: #4e903c; +$skills-search-result-blue: #238bca; $skills-banner-text-blue: #1e5a96; @import "bootstrap"; @@ -83,14 +86,30 @@ pzsh-topbar { .bg-skills-blue { background-color: $skills-blue; + + &:hover { + background-color: #3268a1; + } } .bg-skills-gray { background-color: $skills-gray; } -.bg-skills-blue:hover { - background-color: #3268a1; +.bg-skills-green { + background-color: $skills-green; + + &:hover { + background-color: $skills-green-hover; + } +} + +.bg-skills-search-result-blue { + background-color: $skills-search-result-blue; + + &:hover { + background-color: $skills-green-hover; + } } .fixed-table { diff --git a/app/controllers/cv_search_controller.rb b/app/controllers/cv_search_controller.rb new file mode 100644 index 000000000..2c14be9d2 --- /dev/null +++ b/app/controllers/cv_search_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class CvSearchController < ApplicationController + def index + @cv_search_results = should_search ? [] : search_results + end + + private + + def search_results + PeopleSearch.new(query).entries + end + + def query + params[:q] + end + + def should_search + query.nil? || query.length < 3 + end +end diff --git a/app/domain/people_search.rb b/app/domain/people_search.rb index 026626f18..3857640f2 100644 --- a/app/domain/people_search.rb +++ b/app/domain/people_search.rb @@ -8,6 +8,7 @@ class PeopleSearch def initialize(search_term) @search_term = search_term @entries = search_result + @entries = @entries.filter { |entry| entry[:found_in] } end private diff --git a/app/helpers/cv_search_helper.rb b/app/helpers/cv_search_helper.rb new file mode 100644 index 000000000..df0162afc --- /dev/null +++ b/app/helpers/cv_search_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module CvSearchHelper + def translate_found_in(result) + I18n.t("cv_search.#{result[:found_in].split('#')[0].underscore}") + end + + def person_path_with_query(result) + "#{person_path(result[:person][:id])}?q=#{params[:q]}" + end +end diff --git a/app/javascript/controllers/highlight_controller.js b/app/javascript/controllers/highlight_controller.js new file mode 100644 index 000000000..770408957 --- /dev/null +++ b/app/javascript/controllers/highlight_controller.js @@ -0,0 +1,10 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + const params = new URLSearchParams(window.location.search); + if(params.has("q")) { + window.find(params.get("q")) + } + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index d08858d9b..bb22fcf93 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -4,12 +4,18 @@ import { application } from "./application" +import DatePickerController from "./date_picker_controller" +application.register("date-picker", DatePickerController) + import DropdownController from "./dropdown_controller" application.register("dropdown", DropdownController) import DynamicFieldsController from "./dynamic_fields_controller" application.register("dynamic-fields", DynamicFieldsController) +import HighlightController from "./highlight_controller" +application.register("highlight", HighlightController) + import ImageUploadController from "./image_upload_controller" application.register("image-upload", ImageUploadController) @@ -19,21 +25,17 @@ application.register("lang-selection", LangSelectionController) import NationalityTwoController from "./nationality_two_controller" application.register("nationality-two", NationalityTwoController) +import ProfileTabController from "./profile_tab_controller" +application.register("profile-tab", ProfileTabController) + import RemoteModalController from "./remote_modal_controller" application.register("remote-modal", RemoteModalController) -import SkillsFilterController from "./skills_filter_controller" -application.register("skills-filter", SkillsFilterController) - -import DatePickerController from "./date_picker_controller" -application.register("date-picker", DatePickerController) +import SearchController from "./search_controller" +application.register("search", SearchController) -import DropdownLinksController from "./dropdown_controller" -application.register("dropdown", DropdownLinksController) - -import ProfileTabController from "./profile_tab_controller" -application.register("profile-tab", ProfileTabController) - -import SkillsLevelController from "./skill_level_controller" -application.register("skills-level", SkillsLevelController) +import SkillLevelController from "./skill_level_controller" +application.register("skill-level", SkillLevelController) +import SkillsFilterController from "./skills_filter_controller" +application.register("skills-filter", SkillsFilterController) diff --git a/app/javascript/controllers/search_controller.js b/app/javascript/controllers/search_controller.js new file mode 100644 index 000000000..c18d09499 --- /dev/null +++ b/app/javascript/controllers/search_controller.js @@ -0,0 +1,20 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + + connect() { + const params = new URLSearchParams(window.location.search); + if(params.has("q")) { + document.getElementById("cv_search_field").value = params.get("q"); + } + } + + timeout; + submitWithTimeout(e) { + const form = e.target.parentElement; + clearTimeout(this.timeout); + this.timeout = setTimeout(() => { + form.requestSubmit(); + }, 100) + } +} \ No newline at end of file diff --git a/app/views/cv_search/index.html.haml b/app/views/cv_search/index.html.haml new file mode 100644 index 000000000..ee741b862 --- /dev/null +++ b/app/views/cv_search/index.html.haml @@ -0,0 +1,16 @@ +%div.mt-2 + %form{data: {"turbo-frame": "search-results", "turbo-action": "advance"}, "data-controller": "search"} + %input{class: 'form-control w-75', placeholder: 'CVs durchsuchen...', name: 'q', "data-action": "search#submitWithTimeout", id: "cv_search_field"} + %div.profile-header.mw-100.border-bottom.mt-2.mb-2 + Suchresultate + %turbo-frame{id: "search-results"} + - if @cv_search_results.blank? + Keine Resultate + - else + - @cv_search_results.each do |result| + %div.w-50.d-flex + = link_to result[:person][:name], person_path(result[:person][:id]), {class: "bg-skills-green w-50 text-decoration-none text-white ps-1 p-2 rounded-1", "data-turbo": "false"} + %div.w-50.d-flex.justify-content-end.align-items-center + %div.me-1 gefunden in: + = link_to translate_found_in(result), person_path_with_query(result), {class: "bg-skills-search-result-blue w-50 text-decoration-none text-white ps-1 p-2 rounded-1 text-center", "data-turbo": "false"} + %br \ No newline at end of file diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index e1a8e4992..3f13487ff 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -34,7 +34,7 @@ - if auth_user_signed_in? =link_to "Logout", destroy_auth_user_session_path, data: { "turbo-method": :delete} - elsif devise_mapping.omniauthable? - =button_to "Login", omniauth_authorize_path(resource_name, resource_class.omniauth_providers.first), {data: { "turbo": false}, class: "btn btn-link"} + =button_to "Login", omniauth_authorize_path(resource_name, resource_class.omniauth_providers.first), {data: { "turbo": false }, class: "btn btn-link"} %div.puzzle-header %div.d-flex.h-100 %ul.navbar.h-100 @@ -43,7 +43,7 @@ %li.bg-skills-blue.h-100.d-flex.align-items-center %a.nav-link.cursor-pointer.ps-2.pe-2{href: people_skills_path} Skill Suche %li.bg-skills-blue.h-100.d-flex.align-items-center - %a.nav-link.cursor-pointer.ps-2.pe-2= "CV Suche" + %a.nav-link.cursor-pointer.ps-2.pe-2{href: cv_search_index_path} CV Suche %li.bg-skills-blue.h-100.d-flex.align-items-center %a.nav-link.cursor-pointer.ps-2.pe-2{href: skills_path} Skillset %div.container-fluid diff --git a/app/views/people/show.html.haml b/app/views/people/show.html.haml index 1ca2f9da3..c807f1d02 100644 --- a/app/views/people/show.html.haml +++ b/app/views/people/show.html.haml @@ -1,6 +1,6 @@ %div.mt-2 =render partial:"people/search", :locals => {person: @person} -%div{"data-controller": "profile-tab"} +%div{"data-controller": "profile-tab highlight"} %ul.nav.nav-tabs.d-flex.flex-row.mt-2 %div.w-50.d-flex.flex-row =link_to person_path(@person), class: "btn d-flex align-items-center justify-content-center text-primary p-0", diff --git a/config/locales/de.yml b/config/locales/de.yml index 21411ac75..7639e3283 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -83,3 +83,14 @@ de: till_today: Bis Heute with_enddate: Mit Enddatum + cv_search: + advanced_trainings: Weiterbildungen + educations: Ausbildungen + activities: Stationen + projects: Projekte + name: Name + location: Wohnort + title: Abschluss + competence_notes: Kompetenzen + roles: Funktionen + department: Organisationseinheit \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index c945c9aab..c40ce79a5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,6 +19,8 @@ resources :people_skills + resources :cv_search + resources :people do resources :advanced_trainings resources :educations diff --git a/spec/features/cv_search_spec.rb b/spec/features/cv_search_spec.rb new file mode 100644 index 000000000..f5df444e1 --- /dev/null +++ b/spec/features/cv_search_spec.rb @@ -0,0 +1,64 @@ +require 'rails_helper' + + +describe 'Advanced Trainings', type: :feature, js:true do + let(:person) { people(:bob) } + + before(:each) do + sign_in auth_users(:admin) + visit("/cv_search") + end + + describe 'Search' do + it 'should find correct results' do + fill_in 'cv_search_field', with: person.name + check_search_results(I18n.t("cv_search.name")) + fill_in 'cv_search_field', with: person.projects.first.technology + check_search_results(I18n.t("cv_search.projects")) + fill_in 'cv_search_field', with: person.title + check_search_results(I18n.t("cv_search.title")) + fill_in 'cv_search_field', with: person.roles.first.name + check_search_results(I18n.t("cv_search.roles")) + fill_in 'cv_search_field', with: person.department.name + check_search_results(I18n.t("cv_search.department")) + fill_in 'cv_search_field', with: person.competence_notes.split.first + check_search_results(I18n.t("cv_search.competence_notes")) + fill_in 'cv_search_field', with: person.advanced_trainings.first.description + check_search_results(I18n.t("cv_search.advanced_trainings")) + fill_in 'cv_search_field', with: person.educations.first.location + check_search_results(I18n.t("cv_search.educations")) + fill_in 'cv_search_field', with: person.activities.first.description + check_search_results(I18n.t("cv_search.activities")) + fill_in 'cv_search_field', with: person.projects.first.description + check_search_results(I18n.t("cv_search.projects")) + end + + it 'should open person when clicking result' do + fill_in 'cv_search_field', with: person.projects.first.technology + check_search_results(I18n.t("cv_search.projects")) + click_link(person.name) + expect(page).to have_current_path(person_path(person)) + + visit("/cv_search") + education_location = person.educations.first.location + fill_in 'cv_search_field', with: education_location + check_search_results(I18n.t("cv_search.educations")) + click_link(I18n.t("cv_search.educations")) + expect(page).to have_current_path("#{person_path(person)}?q=#{education_location}") + end + + it 'should only display results when length of search-text is > 3' do + fill_in 'cv_search_field', with: person.name.slice(0, 2) + expect(page).not_to have_content(person.name) + fill_in 'cv_search_field', with: person.name.slice(0, 3) + expect(page).to have_content(person.name) + end + end +end + +def check_search_results(field_name) + within('turbo-frame#search-results') { + expect(page).to have_content(person.name) + expect(page).to have_content(field_name) + } +end \ No newline at end of file