Skip to content

Commit

Permalink
Case system improvements (#2054)
Browse files Browse the repository at this point in the history
* Mass assign

* Add case replies

* Thumbnails

* Lint

* Fix issue with mass update

* replies -> templates
  • Loading branch information
Cory McDonald authored Jul 19, 2019
1 parent 580f997 commit 24b5f1b
Show file tree
Hide file tree
Showing 17 changed files with 426 additions and 27 deletions.
67 changes: 67 additions & 0 deletions app/assets/stylesheets/admin/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -446,3 +446,70 @@ a.logo span {
left: 35px;
position: absolute;
}

.checkbox_1 {
height: 70px;
display: inline-flex;
align-items: center;
opacity: 0.9;
cursor: pointer;
position: relative;
}

.checkbox_1 > input {
width: 21px;
height: 21px;

appearance: none;

/* custom styling */
background-color: #fff;
border: 2px solid $braveGray-8;
border-radius: 5px;
cursor: pointer;
outline: none;

transition-duration: 0.1s;
}

.checkbox_1 > input:checked {
border: 3px solid #4c54d2;
background-color: #f3f3f6;
}

/* style checkmark symbol */
.checkbox_1 > input:checked + span::before {
color: #343546;
content: "\2713";

text-align: center;

display: block;
position: absolute;
left: 16px;
top: 22px;
}

.checkbox_1 > input:active {
border: 2px solid #7e47a8;
}

.checkbox_1 > input:focus + label::before {
outline: #fb542b solid 1px;
box-shadow: 0 0px 8px #7e47a8;
}

.reply:hover {
background: $braveBrand-Light;
cursor: pointer;
* {
color: $braveGray-10 !important;
}
}

#replySection {
left: 300px;
top: 0;
position: absolute;
z-index: 999999999;
}
4 changes: 3 additions & 1 deletion app/controllers/admin/case_notes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ def create
end

def update
CaseNote.find(params[:id]).update(public: false)
note = CaseNote.find(params[:id])
note.update(public: false)
redirect_to admin_case_path(note.case)
end

private
Expand Down
38 changes: 38 additions & 0 deletions app/controllers/admin/case_replies_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module Admin
class CaseRepliesController < AdminController
def index
@open_cases = Case.where(status: Case::OPEN)
@assigned_cases = Case.where(assignee: current_user, status: Case::IN_PROGRESS)

@replies = CaseReply.all
end

def create
CaseReply.create(reply_params)
redirect_to admin_case_replies_path, flash: { notice: "Your saved reply was created successfully."}
end

def edit
@reply = CaseReply.find(params[:id])
@open_cases = Case.where(status: Case::OPEN)
@assigned_cases = Case.where(assignee: current_user, status: Case::IN_PROGRESS)
end

def update
CaseReply.find(params[:id]).update(reply_params)
redirect_to admin_case_replies_path, flash: { notice: "Your saved reply was updated successfully."}
end

def destroy
CaseReply.find(params[:id]).destroy
redirect_to admin_case_replies_path, flash: { notice: "Deleted the reply"}
end


private

def reply_params
params.require(:case_reply).permit(:id, :title, :body)
end
end
end
2 changes: 2 additions & 0 deletions app/controllers/admin/cases_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ def show

last_note = @notes.where(public: true).first
@answered = last_note&.created_by&.admin?

@replies = CaseReply.all
end

def assign
Expand Down
90 changes: 90 additions & 0 deletions app/javascript/packs/admin_case.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
import Rails from "rails-ujs";

const shiftClick = () => {
// get array of items
var list = document.querySelector(".dynamic-table");
var items = list.querySelectorAll(".gradeX");

// create vars for tracking clicked items
var firstItem, lastItem;

// method for ticking all items between first and last
function tick(first, last) {
// items is a nodeList, so we do some prototype trickery
Array.prototype.forEach.call(items, function(el, i) {
// find each checkbox
var checkbox = el.getElementsByTagName("input")[0];
// tick all within first to last range
if ((i >= first && i <= last) || (i <= first && i >= last)) {
checkbox.checked = true;
}
});
}

// method for unticking all items except current item
function untickAllExcept(first) {
Array.prototype.forEach.call(items, function(el, i) {
var cb = el.querySelectorAll("input[type='checkbox']");
if (i !== first) {
cb[0].checked = false;
}
});
}

// click listener on list
list.addEventListener("click", function(e) {
if (e.target.type === "checkbox" || e.target.nodeName === "SPAN") {
var item = e.target.parentNode.parentNode;
if (e.target.nodeName === "SPAN") {
const checked = e.target.parentNode.firstChild.checked;
e.target.parentNode.firstChild.checked = !checked;
}

if (e.shiftKey) {
// store as last item clicked
lastItem = Array.prototype.indexOf.call(items, item);
} else {
// store as first item clicked
firstItem = Array.prototype.indexOf.call(items, item);
// unset last item
lastItem = null;
}

// do magic
if (lastItem != null) {
tick(firstItem, lastItem);
} else {
untickAllExcept(firstItem);
}
}
});
};

function selected(e) {
console.log(
"Original event that triggered text replacement:",
Expand All @@ -23,6 +83,8 @@ function selected(e) {
e.detail.item.original.key
}</div>`;

assignCheckboxes(e, event.target, assignedHTML);

const parent = event.target.closest("div");
if (parent.id) {
parent.classList.toggle("w-100");
Expand All @@ -39,6 +101,32 @@ function selected(e) {
}
}

function assignCheckboxes(e, target, assignedHTML) {
const checkbox = target
.closest("tr")
.querySelectorAll("input[type='checkbox']");

if (checkbox && checkbox[0].checked) {
const inputChecked = target
.closest("table")
.querySelectorAll("input[type='checkbox']:checked");

inputChecked.forEach(checked => {
const checkedForm = checked.closest("tr").querySelector("form");

checkedForm.querySelector(".assignee-input").value =
e.detail.item.original.value;
Rails.fire(checkedForm, "submit");

let parentDiv = checkedForm.closest("div");
if (parentDiv.id) {
parentDiv.classList.toggle("w-100");
parentDiv.closest("td").innerHTML = assignedHTML;
}
});
}
}

function toggleForm(event) {
const form = event.target.parentElement.parentElement.querySelector("form");
form.classList.toggle("d-none");
Expand All @@ -58,6 +146,8 @@ document.addEventListener("DOMContentLoaded", function() {
};
}

shiftClick();

document
.querySelectorAll(".filter")
.forEach(element => element.addEventListener("click", toggleForm));
Expand Down
113 changes: 113 additions & 0 deletions app/javascript/packs/admin_case_replies.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from "react";
import * as ReactDOM from "react-dom";

export default class CaseReply extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: "",
isVisible: false
};
}

setText = e => {
this.setState({ filterText: e.target.value });
};

getReplies = () => {
if (this.state.filterText === "") {
return this.props.replies;
}

return this.props.replies.filter(reply => {
let query = this.state.filterText.toLowerCase();
let title = reply.title.toLowerCase();
let body = reply.body.toLowerCase();

return title.includes(query) || body.includes(query);
});
};

renderDropdown = () => {
this.setState(prevState => ({
isVisible: !prevState.isVisible
}));
};

clickReply = body => {
const textarea = document.getElementsByName("case_note[note]")[0];

textarea.value += body + "\n";

this.setState({ isVisible: false });
};

render() {
const button = (
<div className="btn btn-light ml-2 mb-2" onClick={this.renderDropdown}>
<i className="fa fa-comment-o" />
<i
className="small ml-0 mr-2 fa fa-caret-down"
style={{ top: "5px", position: "relative" }}
/>
Saved templates
</div>
);
let replies = (
<div className="text-muted">
<i className="fa fa-frown-o mx-2" />
<span>No replies</span>
</div>
);

const savedReplies = this.getReplies();
if (savedReplies.length > 0) {
replies = savedReplies.map(reply => (
<div
className="reply p-2 rounded"
key={reply.id}
onClick={() => this.clickReply(reply.body)}
>
<div className="font-weight-bold text-dark text-truncate">
{reply.title}
</div>
<div className="text-muted text-truncate">{reply.body}</div>
</div>
));
}

return (
<div>
{button}
{this.state.isVisible && (
<div
className="p-2 border rounded bg-white"
style={{ width: "500px" }}
>
<strong className="p-1">Select a reply</strong>
<div className="my-1 py-2 border-top">
<input
type="text"
autoFocus
placeholder="Filter replies..."
className="border rounded-pill px-3 py-1 w-100"
style={{ outline: "none" }}
value={this.state.filterText}
onChange={this.setText}
/>
</div>
<div style={{ maxHeight: "200px", overflow: "auto" }}>
{replies}
</div>
</div>
)}
</div>
);
}
}

document.addEventListener("DOMContentLoaded", () => {
const element = document.querySelector("#replySection");
const replies = JSON.parse(element.dataset.replies);
ReactDOM.render(<CaseReply replies={replies} />, element);
});
5 changes: 5 additions & 0 deletions app/models/case_reply.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class CaseReply < ApplicationRecord
validates :title, :body, presence: true, allow_blank: false
end
9 changes: 9 additions & 0 deletions app/views/admin/case_replies/_form.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
= form_with model: [:admin, reply], local: true do |f|
.form-group
label.font-weight-bold Saved reply title
= f.text_field :title, { class:'form-control', placeholder: "Add a short title to your reply"}
.form-group
= f.text_area :body, { class:'form-control', rows:'4', placeholder: "Add your saved reply" }
=submit_tag(reply.persisted? ? "Update saved reply" : "Add saved reply", class: "btn btn-primary")
- if reply.persisted?
=link_to "Cancel", admin_case_replies_path, class: 'btn btn-dark ml-3'
7 changes: 7 additions & 0 deletions app/views/admin/case_replies/edit.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.row
.col-2.shadow-sm.pb-3
=render partial: 'admin/cases/sidebar'
.col-10
h4 Edit saved reply
hr
= render partial: 'form', locals: { reply: @reply }
Loading

0 comments on commit 24b5f1b

Please sign in to comment.