Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,7 @@ div.wunderbaum div.wb-header span.wb-col.wb-active {
margin-top: 40px !important;
}

.wunderbaum .wb-title.wb-title-match {
background-color: #fffd75;
font-weight: bold;
border-radius: 3px;
padding: 1px 4px;
}

.wunderbaum .wb-custom-match {
.wunderbaum .wb-row.wb-match > span.wb-col .wb-title {
background-color: #fffd75;
font-weight: bold;
border-radius: 3px;
Expand Down
150 changes: 50 additions & 100 deletions app/controllers/volumes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,64 @@ def file_tree_folders_search
assigned_to = params[:assigned_to].presence
return render(json: []) if q.blank? && assigned_to.blank?

base_scope = filtered_folder_scope
folders = q.present? ? apply_folder_text_search(base_scope, q.downcase) : base_scope
folder_records = folders.to_a
matched_folder_count = q.present? ? folder_records.count { |folder| folder_title_or_notes_match?(folder, q) } : folder_records.size
notes_match_count = q.present? ? folder_records.count { |folder| folder_notes_match?(folder, q) } : 0
folders = @volume.isilon_folders.includes(:parent_folder)

render json: {
results: folder_records.map { |folder| FileTreeSearchResultSerializer.new(folder).as_json },
total_count: matched_folder_count,
notes_match_count: notes_match_count
}
folders = folders.where("LOWER(full_path) LIKE ?", "%#{q.downcase}%") if q.present?

if assigned_to.present? && assigned_to != "unassigned"
folders = folders.where(assigned_to_id: assigned_to)
end

folders = folders.where(assigned_to_id: nil) if assigned_to == "unassigned"

render json: folders.map { |folder| FileTreeSearchResultSerializer.new(folder).as_json }
end

def file_tree_assets_search
q = params[:q].to_s.strip.downcase

base_scope = filtered_asset_scope
scope = q.present? ? apply_asset_text_search(base_scope, q) : base_scope
scope = @volume.isilon_assets.includes(:parent_folder)

# Text search
scope = scope.where("LOWER(isilon_name) LIKE ?", "%#{q}%") if q.present?

# Column filters
scope = scope.where(migration_status: params[:migration_status]) if params[:migration_status].present?

if params[:assigned_to].present? && params[:assigned_to] != "unassigned"
scope = scope.where(assigned_to_id: params[:assigned_to])
end

if params[:file_type].present?
normalized_file_type = params[:file_type].to_s.strip.downcase

scope = scope.where(
"LOWER(TRIM(isilon_assets.file_type)) = ?",
normalized_file_type
)
end

if params[:contentdm_collection_id].present?
scope = scope.where(contentdm_collection_id: params[:contentdm_collection_id])
end

if params[:aspace_collection_id].present?
scope = scope.where(aspace_collection_id: params[:aspace_collection_id])
end

if params.key?(:aspace_linking_status)
value = ActiveModel::Type::Boolean.new.cast(params[:aspace_linking_status])
scope = scope.where(aspace_linking_status: value)
end

if params.key?(:is_duplicate)
value = ActiveModel::Type::Boolean.new.cast(params[:is_duplicate])
scope = scope.where(has_duplicates: value)
end

# Handle unassigned users (assigned_to = nil)
scope = scope.where(assigned_to_id: nil) if params[:assigned_to] == "unassigned"
total_count = scope.count
notes_match_count = q.present? ? apply_asset_notes_search(base_scope, q).count : 0

assets = scope.includes(parent_folder: :parent_folder)

Expand All @@ -66,7 +103,6 @@ def file_tree_assets_search
render json: {
results: tree_nodes,
total_count: total_count,
notes_match_count: notes_match_count,
returned_count: tree_nodes.size
}
end
Expand Down Expand Up @@ -164,92 +200,6 @@ def set_volume
@volume = Volume.find(params[:id])
end

def apply_asset_text_search(scope, query)
pattern = "%#{query}%"

scope.where(
"LOWER(isilon_assets.isilon_name) LIKE :pattern OR LOWER(COALESCE(isilon_assets.notes, '')) LIKE :pattern",
pattern: pattern
)
end

def apply_asset_notes_search(scope, query)
pattern = "%#{query}%"

scope.where(
"LOWER(COALESCE(isilon_assets.notes, '')) LIKE ?",
pattern
)
end

def apply_folder_text_search(scope, query)
pattern = "%#{query}%"

scope.where(
"LOWER(isilon_folders.full_path) LIKE :pattern OR LOWER(COALESCE(isilon_folders.notes, '')) LIKE :pattern",
pattern: pattern
)
end

def filtered_folder_scope
scope = @volume.isilon_folders.includes(:parent_folder)

if params[:assigned_to].present? && params[:assigned_to] != "unassigned"
scope = scope.where(assigned_to_id: params[:assigned_to])
end

scope = scope.where(assigned_to_id: nil) if params[:assigned_to] == "unassigned"
scope
end

def folder_title_or_notes_match?(folder, query)
title = folder.full_path.to_s.split("/").reject(&:blank?).last.to_s.downcase
title.include?(query.downcase) || folder_notes_match?(folder, query)
end

def folder_notes_match?(folder, query)
folder.notes.to_s.downcase.include?(query.downcase)
end

def filtered_asset_scope
scope = @volume.isilon_assets.includes(:parent_folder)
scope = scope.where(migration_status: params[:migration_status]) if params[:migration_status].present?

if params[:assigned_to].present? && params[:assigned_to] != "unassigned"
scope = scope.where(assigned_to_id: params[:assigned_to])
end

if params[:file_type].present?
normalized_file_type = params[:file_type].to_s.strip.downcase

scope = scope.where(
"LOWER(TRIM(isilon_assets.file_type)) = ?",
normalized_file_type
)
end

if params[:contentdm_collection_id].present?
scope = scope.where(contentdm_collection_id: params[:contentdm_collection_id])
end

if params[:aspace_collection_id].present?
scope = scope.where(aspace_collection_id: params[:aspace_collection_id])
end

if params.key?(:aspace_linking_status)
value = ActiveModel::Type::Boolean.new.cast(params[:aspace_linking_status])
scope = scope.where(aspace_linking_status: value)
end

if params.key?(:is_duplicate)
value = ActiveModel::Type::Boolean.new.cast(params[:is_duplicate])
scope = scope.where(has_duplicates: value)
end

scope = scope.where(assigned_to_id: nil) if params[:assigned_to] == "unassigned"
scope
end

def volume_params
params.fetch(:volume, {}).permit(:name, :id, :parent_folder_id)
end
Expand Down
91 changes: 14 additions & 77 deletions app/javascript/controllers/wunderbaum_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,6 @@ export default class extends Controller {
}

util.setValueToElem(colInfo.elem, displayValue);

if (colId === "notes") {
this._syncNotesHighlight(colInfo.elem, node, displayValue);
}
}

const titleElem = e.nodeElem.querySelector("span.wb-title");
Expand Down Expand Up @@ -306,8 +302,6 @@ export default class extends Controller {
titleElem.innerHTML =
`<a href="${node.data.url}" class="asset-link" target="_blank" rel="noopener" data-turbo="false">${node.title}</a>`;
}

this._syncTitleHighlight(titleElem, node);
},

buttonClick: (e) => {
Expand Down Expand Up @@ -644,8 +638,7 @@ export default class extends Controller {
searchCtrl
).catch(() => ({
results: [],
total_count: 0,
notes_match_count: 0
total_count: 0
}))
]);

Expand All @@ -654,16 +647,11 @@ export default class extends Controller {
}

let folders = [];
let folderTotalCount = 0;
let folderNotesMatchCount = 0;

if (Array.isArray(folderResponse)) {
folders = folderResponse;
folderTotalCount = folders.length;
} else if (Array.isArray(folderResponse?.results)) {
folders = folderResponse.results;
folderTotalCount = Number(folderResponse.total_count ?? folders.length);
folderNotesMatchCount = Number(folderResponse.notes_match_count ?? 0);
}

let assets = [];
Expand All @@ -675,33 +663,30 @@ export default class extends Controller {
}

this._buildFolderMatchCounts(assets);
const folderMatchCount = Number.isFinite(folderTotalCount) ? folderTotalCount : folders.length;
const folderMatchCount = folders.length;

let backendAssetCount = assets.length;
let backendNotesMatchCount = 0;

if (
!Array.isArray(assetResponse) &&
assetResponse?.total_count != null
) {
backendAssetCount = Number(assetResponse.total_count);
backendNotesMatchCount = Number(assetResponse.notes_match_count ?? 0);
}

if (!Number.isFinite(backendAssetCount)) {
backendAssetCount = assets.length;
}

if (!Number.isFinite(backendNotesMatchCount)) {
backendNotesMatchCount = 0;
}
const hasFolderCapableFilters =
q.length > 0 ||
(this.columnFilters.has("assigned_to") &&
String(this.columnFilters.get("assigned_to") ?? "") !== "");

if (!Number.isFinite(folderNotesMatchCount)) {
folderNotesMatchCount = 0;
}

const backendMatchCount = folderMatchCount + backendAssetCount;
const totalNotesMatchCount = folderNotesMatchCount + backendNotesMatchCount;
const backendMatchCount =
hasFolderCapableFilters
? folderMatchCount + backendAssetCount
: backendAssetCount;

if (!q && this.columnFilters.size > 0) {
await this._materializeColumnFilterResults(
Expand All @@ -722,7 +707,7 @@ export default class extends Controller {
}

this._applyPredicate(q);
this._updateMatchCount(backendMatchCount, totalNotesMatchCount);
this._updateMatchCount(backendMatchCount);
} finally {
this.inflightControllers.delete(searchCtrl);
this._setLoading(false);
Expand All @@ -741,9 +726,7 @@ export default class extends Controller {
""
).toLowerCase();

const notes = String(node.data.notes ?? "").toLowerCase();

if (!text.includes(q) && !notes.includes(q)) return false;
if (!text.includes(q)) return false;
}

for (const [colId, val] of this.columnFilters.entries()) {
Expand Down Expand Up @@ -1250,47 +1233,6 @@ export default class extends Controller {
return label.trim().toLowerCase();
}

// Applies notes-match highlighting without replacing the editable notes input.
_syncNotesHighlight(elem, node, value) {
const notes = String(value ?? "");
const query = String(this.currentQuery || "").trim();
const input = elem.querySelector("input[name='notes']");

elem.classList.remove("wb-custom-match");
input?.classList.remove("wb-custom-match");

if (!input || !notes || !query) {
return;
}

const predicate = this.currentFilterPredicate;
if (typeof predicate === "function" && !predicate(node)) {
return;
}

const title = String(node?.title || "").toLowerCase();
const normalizedQuery = query.toLowerCase();
const noteMatches = normalizedQuery && notes.toLowerCase().includes(normalizedQuery);
const titleMatches = normalizedQuery && title.includes(normalizedQuery);

if (noteMatches && !titleMatches) {
input.classList.add("wb-custom-match");
}
}

// Applies title highlighting only when the visible node title itself matches the query.
_syncTitleHighlight(titleElem, node) {
titleElem.classList.remove("wb-title-match");

const query = String(this.currentQuery || "").trim().toLowerCase();
if (!query) return;

const title = String(node?.title || "").toLowerCase();
if (!title.includes(query)) return;

titleElem.classList.add("wb-title-match");
}

// Renders and positions the column filter dropdown.
_showDropdownFilter(anchorEl, colId, colIdx, opts = {}) {
const isInline = typeof opts.onSelect === "function";
Expand Down Expand Up @@ -1683,7 +1625,7 @@ export default class extends Controller {
}

// Displays count for query search matches
_updateMatchCount(count, notesMatchCount = 0) {
_updateMatchCount(count) {
const input = document.getElementById("tree-filter");
if (!input) return;

Expand All @@ -1709,12 +1651,7 @@ export default class extends Controller {
el.style.left = "0";
el.style.display = "block";

let text = `${count.toLocaleString()} matches`;
if (notesMatchCount > 0) {
text += ` (${notesMatchCount.toLocaleString()} notes matches)`;
}

el.textContent = text;
el.textContent = `${count.toLocaleString()} matches`;
}

_showMatchCountStatus(text) {
Expand Down
2 changes: 1 addition & 1 deletion app/views/volumes/_volume.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<input
id="tree-filter"
type="search"
placeholder="Search titles and notes"
placeholder="Enter search pattern"
autofocus
/>

Expand Down
Loading