Skip to content

Commit

Permalink
feat: link rubric to assessment point with autocomplete field
Browse files Browse the repository at this point in the history
- adjusted rubric/assessment point linking UX

- Autocomplete hook adjustments, allowing implementation in curriculum and rubrics search

- created `Rubrics.search_rubrics/2` and `LantternWeb.RubricsLive.RubricSearchInputComponent`

- added `:assessment_points_rubric_id_fkey` fk constraint to `AssessmentPoint` schema preventing raise in case of rubric/assessment point scale mismatch

- removed `RubricsOverlayComponent`'s `:new_rubric_linked` action notification

- added `RubricsLive.RubricSearchInputComponent` to `RubricsOverlayComponent`

- added rubric criteria gin index migration
  • Loading branch information
endoooo committed Nov 9, 2023
1 parent 069205f commit 60b0f38
Show file tree
Hide file tree
Showing 11 changed files with 413 additions and 206 deletions.
70 changes: 37 additions & 33 deletions assets/js/autocomplete-hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ const pushSelect = (hook, input, selected) => {
input.value = selected.name;

// force hidden input value change and trigger phx-change event
const hiddenInput = document.getElementById(
input.getAttribute("data-hidden-input-id")
);
hiddenInput.value = selected.id;
hiddenInput.dispatchEvent(new Event("input", { bubbles: true }));
if (input.getAttribute("data-hidden-input-id")) {
const hiddenInput = document.getElementById(
input.getAttribute("data-hidden-input-id")
);
hiddenInput.value = selected.id;
hiddenInput.dispatchEvent(new Event("input", { bubbles: true }));
}

// send event to liveview server
hook.pushEventTo(input, "autocomplete_result_select", selected);
Expand All @@ -49,19 +51,19 @@ const setActive = (activeId, input, controls) => {
};

function autocompleteSearchResults(event) {
let input = this.el;
let controlId = input.getAttribute("aria-controls");
let controls = document.getElementById(controlId);
const input = this.el;
const controlId = input.getAttribute("aria-controls");
const controls = document.getElementById(controlId);

// on li mouseenter
let activateLi = (event) => {
const activateLi = (event) => {
setActive(event.target.id, input, controls);
};

// on li click
let selectLi = (event) => {
let targetParentLi = event.target.closest("li");
let selected = {
const selectLi = (event) => {
const targetParentLi = event.target.closest("li");
const selected = {
id: targetParentLi.getAttribute("data-result-id"),
name: targetParentLi.getAttribute("data-result-name"),
};
Expand All @@ -84,22 +86,24 @@ function autocompleteSearchResults(event) {
}
}

function removeCurriculumItem(event) {
let input = this.el;
function clearSelectedItem(event) {
const input = this.el;

// force hidden input value change and trigger phx-change event
const hiddenInput = document.getElementById(
input.getAttribute("data-hidden-input-id")
);
hiddenInput.value = "";
hiddenInput.dispatchEvent(new Event("input", { bubbles: true }));
if (input.getAttribute("data-hidden-input-id")) {
const hiddenInput = document.getElementById(
input.getAttribute("data-hidden-input-id")
);
hiddenInput.value = "";
hiddenInput.dispatchEvent(new Event("input", { bubbles: true }));
}
}

// on click away
function clickAwayHandler(event) {
let input = this.el;
let controlId = input.getAttribute("aria-controls");
let controls = document.getElementById(controlId);
const input = this.el;
const controlId = input.getAttribute("aria-controls");
const controls = document.getElementById(controlId);

if (
event.target.id !== input.id &&
Expand All @@ -111,11 +115,11 @@ function clickAwayHandler(event) {

// on keydown
function keydownHandler(event) {
let input = this.el;
let controlId = input.getAttribute("aria-controls");
let controls = document.getElementById(controlId);
let list = controls.querySelectorAll("li");
let isShowing = input.getAttribute("aria-expanded") === "true";
const input = this.el;
const controlId = input.getAttribute("aria-controls");
const controls = document.getElementById(controlId);
const list = controls.querySelectorAll("li");
const isShowing = input.getAttribute("aria-expanded") === "true";
let activeDescendantId = input.getAttribute("aria-activedescendant");

// handle Escape
Expand All @@ -129,8 +133,8 @@ function keydownHandler(event) {
event.preventDefault();
// if controls are visible and there's a active descendant, select it
if (isShowing && activeDescendantId) {
let active = document.getElementById(activeDescendantId);
let selected = {
const active = document.getElementById(activeDescendantId);
const selected = {
id: active.getAttribute("data-result-id"),
name: active.getAttribute("data-result-name"),
};
Expand Down Expand Up @@ -167,14 +171,14 @@ function keydownHandler(event) {
event.keyCode === 40 &&
indexOfActive < list.length - 1
) {
let newActiveDescendantId = list[indexOfActive + 1].id;
const newActiveDescendantId = list[indexOfActive + 1].id;
setActive(newActiveDescendantId, input, controls);
} else if (
indexOfActive !== -1 &&
event.keyCode === 38 &&
indexOfActive > 0
) {
let newActiveDescendantId = list[indexOfActive - 1].id;
const newActiveDescendantId = list[indexOfActive - 1].id;
setActive(newActiveDescendantId, input, controls);
}
}
Expand All @@ -191,8 +195,8 @@ const autocompleteHook = {
);

window.addEventListener(
"phx:remove_curriculum_item",
removeCurriculumItem.bind(this),
"phx:clear_selected_item",
clearSelectedItem.bind(this),
{ signal: hookAbortControllerMap[this.el.id].signal }
);

Expand Down
6 changes: 6 additions & 0 deletions lib/lanttern/assessments/assessment_point.ex
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ defmodule Lanttern.Assessments.AssessmentPoint do
|> validate_required([:name, :curriculum_item_id, :scale_id])
|> validate_and_build_datetime()
|> put_classes()
|> foreign_key_constraint(
:rubric_id,
name: :assessment_points_rubric_id_fkey,
message:
"Error linking rubric. Check if it exists and uses the same scale used in the assessment point."
)
end

defp validate_and_build_datetime(changeset) do
Expand Down
96 changes: 70 additions & 26 deletions lib/lanttern/rubrics.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,6 @@ defmodule Lanttern.Rubrics do
|> maybe_preload(opts)
end

defp maybe_filter_by_differentiation_flag(rubrics_query, opts) do
case Keyword.get(opts, :is_differentiation) do
nil ->
rubrics_query

is_differentiation ->
from(
r in rubrics_query,
where: r.is_differentiation == ^is_differentiation
)
end
end

defp maybe_filter_by_scale(rubrics_query, opts) do
case Keyword.get(opts, :scale_id) do
nil ->
rubrics_query

scale_id ->
from(
r in rubrics_query,
where: r.scale_id == ^scale_id
)
end
end

@doc """
Returns the list of rubrics with scale, descriptors, and descriptors ordinal values preloaded.
Expand All @@ -77,6 +51,48 @@ defmodule Lanttern.Rubrics do
|> Enum.map(&sort_rubric_descriptors/1)
end

@doc """
Search rubrics by criteria.
User can search by id by adding `#` before the id `#123`.
### Options:
`:is_differentiation` – filter results by differentiation flag
## Examples
iex> search_rubrics("understanding")
[%Rubric{}, ...]
"""
def search_rubrics(search_term, opts \\ [])

def search_rubrics("#" <> search_term, opts) do
if search_term =~ ~r/[0-9]+\z/ do
from(
r in Rubric,
where: r.id == ^search_term
)
|> maybe_filter_by_differentiation_flag(opts)
|> Repo.all()
else
search_rubrics(search_term, opts)
end
end

def search_rubrics(search_term, opts) do
ilike_search_term = "%#{search_term}%"

from(
r in Rubric,
where: ilike(r.criteria, ^ilike_search_term),
order_by: {:asc, fragment("? <<-> ?", ^search_term, r.criteria)}
)
|> maybe_filter_by_differentiation_flag(opts)
|> Repo.all()
end

@doc """
Gets a single rubric.
Expand Down Expand Up @@ -386,4 +402,32 @@ defmodule Lanttern.Rubrics do
def change_rubric_descriptor(%RubricDescriptor{} = rubric_descriptor, attrs \\ %{}) do
RubricDescriptor.changeset(rubric_descriptor, attrs)
end

# helpers

defp maybe_filter_by_differentiation_flag(rubrics_query, opts) do
case Keyword.get(opts, :is_differentiation) do
nil ->
rubrics_query

is_differentiation ->
from(
r in rubrics_query,
where: r.is_differentiation == ^is_differentiation
)
end
end

defp maybe_filter_by_scale(rubrics_query, opts) do
case Keyword.get(opts, :scale_id) do
nil ->
rubrics_query

scale_id ->
from(
r in rubrics_query,
where: r.scale_id == ^scale_id
)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ defmodule LantternWeb.AssessmetPointLive.CurriculumItemSearchInputComponent do
socket =
socket
|> assign(:selected, nil)
|> push_event("remove_curriculum_item", %{})
|> push_event("clear_selected_item", %{})

{:noreply, socket}
end
Expand Down
7 changes: 0 additions & 7 deletions lib/lanttern_web/live/assessment_point_live/details.ex
Original file line number Diff line number Diff line change
Expand Up @@ -418,13 +418,6 @@ defmodule LantternWeb.AssessmentPointLive.Details do
{:noreply, update(socket, :assessment_point, &Map.put(&1, :rubric_id, rubric_id))}
end

def handle_info({RubricsOverlayComponent, {:new_rubric_linked, _rubric_id}}, socket) do
{:noreply,
push_navigate(socket,
to: ~p"/assessment_points/#{socket.assigns.assessment_point.id}/rubrics"
)}
end

def handle_info({RubricsOverlayComponent, {:error, error_msg}}, socket),
do: {:noreply, put_flash(socket, :error, error_msg)}

Expand Down
Loading

0 comments on commit 60b0f38

Please sign in to comment.