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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
5. **If requirements are not clear, ask for clarification before proceeding**
6. **Never commit directly to `up-develop` or `develop` branches** - always create feature branches and pull requests
7. **Focus on configuration over code** - most features should be achievable through admin panel settings rather than new Ruby code
8. **Create new files and edit directly in the editor**; avoid using command line file operations unless absolutely necessary


### Critical Rules for Running Terminal Commands
Expand Down
4 changes: 2 additions & 2 deletions app/assets/javascripts/app/_fpa.js
Original file line number Diff line number Diff line change
Expand Up @@ -1316,8 +1316,8 @@ _fpa = {
})

if (modal_index) {
// Hide a previously shown modal back
$('.modal.in').removeClass('in').addClass('was-in');
// Hide a previously shown modal back, but not the one we're currently showing
$('.modal.in').not(pm).removeClass('in').addClass('was-in');

pm.on('click.dismiss.bs.modal', `[data-dismiss="modal${modal_index}"]`, function () {
_fpa.hide_modal(modal_index);
Expand Down
71 changes: 55 additions & 16 deletions app/assets/javascripts/app/_fpa_ajax_processors_reports.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,35 +36,74 @@ _fpa.postprocessors_reports = {
$('#modal_results_block').prop('id', 'modal_results_block_1')
},

/**
* Postprocessor for embedded dynamic block links in reports.
* Opens a modal dialog displaying a dynamic model or activity log record.
*
* Supports two modes:
* - Show mode: Displays the record in read-only view
* - Edit mode: Opens the edit form directly (GitHub #325)
* When data-edit-mode="true", clicks the edit button and closes modal on save
*/
report_embed_dynamic_block: function (block, data) {
var us_name = block.attr('data-model-name')
var hyph_name = us_name.hyphenate()
var id = block.attr('data-id')
var master_id = block.attr('data-master-id')
var target_block = "report-result-embedded-block"
var html = $(`<div id="${target_block}-outer"><div id="${target_block}" class="common-template-item index-1" data-model-data-type="dynamic_model" data-subscription="${hyph_name}-edit-form-${master_id}-${id}" data-template="${hyph_name}-result-template" data-item-class="dynamic_model__${us_name}" data-sub-item="dynamic_model__${us_name}" data-sub-id="${id}" data-item-id="" data-preprocessor="${us_name}_edit_form"></div></div>`)
if ($(block).contents().length == 0) {
var modelName = block.attr('data-model-name');
var hyphenatedName = modelName.hyphenate();
var recordId = block.attr('data-id');
var masterId = block.attr('data-master-id');
var isEditMode = block.attr('data-edit-mode') === 'true';
var targetBlockId = 'report-result-embedded-block';

// Build the modal content container
var modalContent = $(`<div id="${targetBlockId}-outer"><div id="${targetBlockId}" class="common-template-item index-1" data-model-data-type="dynamic_model" data-subscription="${hyphenatedName}-edit-form-${masterId}-${recordId}" data-template="${hyphenatedName}-result-template" data-item-class="dynamic_model__${modelName}" data-sub-item="dynamic_model__${modelName}" data-sub-id="${recordId}" data-item-id="" data-preprocessor="${modelName}_edit_form"></div></div>`);

// If no content was loaded via AJAX, hide the modal and return
if ($(block).contents().length === 0) {
_fpa.hide_modal(1);
return;
}
block.removeClass('sv-added-setup-links')
_fpa.show_modal(html, null, true, 'embedded-dynamic-block', 1)

block.removeClass('sv-added-setup-links');
_fpa.show_modal(modalContent, null, true, 'embedded-dynamic-block', 1);

// Move content into modal and set up handlers
window.setTimeout(function () {
$(block).contents().appendTo(`#${target_block}`)
$(block).contents().appendTo(`#${targetBlockId}`);
$(block).html('');

window.setTimeout(function () {
const $target_block = $(`#${target_block}`);
_fpa.form_utils.resize_labels($target_block, null, true)
const $targetBlock = $(`#${targetBlockId}`);
_fpa.form_utils.resize_labels($targetBlock, null, true);

// Ensure that the viewer is set up with the user's capabilities to view and download
var sv_opt = { allow_actions: null };
sv_opt.allow_actions = _fpa.state.user_can;
// Set up secure view links with user capabilities
var secureViewOptions = { allow_actions: _fpa.state.user_can };
_fpa.secure_view.setup_links($targetBlock, 'a.redcap-file-use-secure-view', secureViewOptions);

_fpa.secure_view.setup_links($target_block, 'a.redcap-file-use-secure-view', sv_opt);
// Handle edit mode: click edit button and set up save handler
if (isEditMode) {
_fpa.postprocessors_reports.setup_edit_mode($targetBlock);
}
}, 500);
}, 500);
},

/**
* Sets up edit mode behavior for an embedded block.
* Clicks the edit button to open the form and closes modal on successful save.
*/
setup_edit_mode: function ($targetBlock) {
var editButton = $targetBlock.find('.edit-entity.glyphicon-pencil').first();
if (editButton.length) {
editButton.click();
}

// Close modal after successful form save
$targetBlock.on('ajax:success', 'form', function () {
window.setTimeout(function () {
_fpa.hide_modal(1);
}, 300);
});
},

reports_form: function (block, data) {
_fpa.report_criteria.reports_form(block, data);
},
Expand Down
75 changes: 60 additions & 15 deletions app/helpers/report_results/reports_common_result_cell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,36 +177,81 @@ def cell_content_for_url
html.html_safe
end

#
# Generate HTML for opening a dynamic model or activity log record in a modal dialog.
# Supports both show and edit modes:
# - Show mode: URL like /masters/123/dynamic_model/table_name/456
# - Edit mode: URL ending with /edit (GitHub #325)
#
# The content can be either a plain URL or a markdown format link [label](url)
def cell_content_for_embedded_block
return cell_content unless cell_content.present?

url = cell_content
parsed = parse_embedded_block_url(cell_content)
build_embedded_block_html(parsed)
end

private

if url.start_with? '['
# This is a markdown format link
col_url_parts = url&.scan(/^\[(.+)\]\((.+)\)$/)
a_text = html_escape col_url_parts&.first&.first
url = html_escape col_url_parts&.first&.last
#
# Parse the URL from embedded_block content.
# Returns a hash with parsed components including edit_mode flag.
def parse_embedded_block_url(content)
url = content
link_text = nil
icon_class = nil

if content.start_with?('[')
# Markdown format link: [Label](/url/path)
url_parts = content.scan(/^\[(.+)\]\((.+)\)$/)
link_text = html_escape(url_parts&.first&.first)
url = html_escape(url_parts&.first&.last)
else
# This is a plain URL
icon = 'glyphicon glyphicon-tasks'
# Plain URL - show icon
icon_class = 'glyphicon glyphicon-tasks'
end

split_url = url.split('/')
edit_mode = url.end_with?('/edit')
url_segments = url.split('/').reject(&:blank?)

# Remove 'edit' suffix before extracting record id
url_segments.pop if edit_mode

record_id = url_segments.last
master_id = url_segments[1] if url_segments.first == 'masters'
# Join the last two path segments to form model name (e.g., dynamic_model__table_name)
# rubocop:disable Style/SafeNavigationChainLength
model_name_hyphenated = url_segments[-3..-2]&.join('__')&.hyphenate&.singularize || ''
# rubocop:enable Style/SafeNavigationChainLength

{
url:,
link_text:,
icon_class:,
edit_mode:,
record_id:,
master_id:,
model_name_hyphenated:
}
end

split_url = split_url.reject(&:blank?)
id = split_url.last
master_id = split_url[1] if split_url.first == 'masters'
hyph_name = split_url[-3..-2]&.join('__')&.hyphenate&.singularize || ''
#
# Build the HTML for an embedded_block link and target div
def build_embedded_block_html(parsed)
hyph_name = parsed[:model_name_hyphenated]
record_id = parsed[:record_id]
edit_mode_attr = parsed[:edit_mode] ? ' data-edit-mode="true"' : ''

html = <<~END_HTML
<a class="report-embedded-block-link #{icon}" title="open result" href="#{url}" data-remote="true" data-#{hyph_name}-id="#{id}" data-result-target="#report-result-embedded-block--#{id}" data-template="#{hyph_name}-OPTION_TYPE-result-template" data-result-target-force="true">#{a_text}</a>
<div id="report-result-embedded-block--#{id}" class="report-temp-embedded-block" data-preprocessor="report_embed_dynamic_block" data-model-name="#{hyph_name.underscore}" data-id="#{id}" data-master-id="#{master_id}"></div>
<a class="report-embedded-block-link #{parsed[:icon_class]}" title="open result" href="#{parsed[:url]}" data-remote="true" data-preprocessor="report_embed_dynamic_block" data-#{hyph_name}-id="#{record_id}" data-result-target="#report-result-embedded-block--#{record_id}" data-template="#{hyph_name}-OPTION_TYPE-result-template" data-result-target-force="true">#{parsed[:link_text]}</a>
<div id="report-result-embedded-block--#{record_id}" class="report-temp-embedded-block" data-preprocessor="report_embed_dynamic_block" data-model-name="#{hyph_name.underscore}" data-id="#{record_id}" data-master-id="#{parsed[:master_id]}"#{edit_mode_attr}></div>
END_HTML

html.html_safe
end

public

def cell_content_for_embedded_report
return cell_content unless cell_content.present?

Expand Down
119 changes: 119 additions & 0 deletions spec/helpers/report_results/reports_common_result_cell_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# frozen_string_literal: true

# ReportsCommonResultCell Spec
#
# Tests the cell content rendering for report table cells, specifically focusing on
# the embedded_block feature that displays dynamic model and activity log records
# in a modal dialog when clicked.
#
# Test Coverage:
# - #cell_content_for_embedded_block: Generates HTML for opening records in a modal
# - Handles plain URLs like /dynamic_model/table_name/123
# - Handles markdown format links like [Label](/dynamic_model/table_name/123)
# - Handles activity log URLs like /activity_log/log_type/456
# - Handles edit mode URLs ending with /edit (GitHub #325)

require 'rails_helper'

RSpec.describe ReportResults::ReportsCommonResultCell do
let(:table_name) { 'dynamic_model__test_items' }
let(:col_name) { 'edit_link' }
let(:col_tag) { nil }
let(:col_show_as) { 'embedded_block' }
let(:selection_options) { double('selection_options') }

def build_cell(content)
described_class.new(table_name, content, col_name, col_tag, col_show_as, selection_options)
end

describe '#cell_content_for_embedded_block' do
describe 'edge cases' do
it 'returns blank content unchanged' do
expect(build_cell('').cell_content_for_embedded_block).to eq('')
end

it 'returns nil content as nil' do
expect(build_cell(nil).cell_content_for_embedded_block).to be_nil
end
end

describe 'URL parsing' do
shared_examples 'parses URL correctly' do |url, expected|
it "extracts correct attributes from #{url}" do
html = build_cell(url).cell_content_for_embedded_block

expect(html).to include("data-id=\"#{expected[:id]}\"")
expect(html).not_to include('data-id="edit"')
expect(html).to include("data-model-name=\"#{expected[:model_name]}\"")

expect(html).to include("data-master-id=\"#{expected[:master_id]}\"") if expected[:master_id]

if expected[:edit_mode]
expect(html).to include('data-edit-mode="true"')
else
expect(html).not_to include('data-edit-mode')
end
end
end

context 'with dynamic model URLs' do
include_examples 'parses URL correctly',
'/dynamic_model/datadic_variables/123',
{ id: '123', model_name: 'dynamic_model__datadic_variable', edit_mode: false }

include_examples 'parses URL correctly',
'/masters/789/dynamic_model/test_items/456',
{ id: '456', model_name: 'dynamic_model__test_item', master_id: '789', edit_mode: false }

include_examples 'parses URL correctly',
'/dynamic_model/datadic_variables/123/edit',
{ id: '123', model_name: 'dynamic_model__datadic_variable', edit_mode: true }

include_examples 'parses URL correctly',
'/masters/999/dynamic_model/test_items/123/edit',
{ id: '123', model_name: 'dynamic_model__test_item', master_id: '999', edit_mode: true }
end

context 'with activity log URLs' do
include_examples 'parses URL correctly',
'/activity_log/test_processes/456',
{ id: '456', model_name: 'activity_log__test_process', edit_mode: false }

include_examples 'parses URL correctly',
'/activity_log/test_processes/456/edit',
{ id: '456', model_name: 'activity_log__test_process', edit_mode: true }
end
end

describe 'HTML generation' do
it 'generates link with correct attributes for plain URL' do
html = build_cell('/dynamic_model/datadic_variables/123').cell_content_for_embedded_block

expect(html).to include('href="/dynamic_model/datadic_variables/123"')
expect(html).to include('data-remote="true"')
expect(html).to include('class="report-embedded-block-link glyphicon glyphicon-tasks"')
expect(html).to include('data-preprocessor="report_embed_dynamic_block"')
end

it 'generates link with label for markdown format' do
html = build_cell('[Edit Item](/dynamic_model/datadic_variables/456)').cell_content_for_embedded_block

expect(html).to include('>Edit Item</a>')
expect(html).to include('href="/dynamic_model/datadic_variables/456"')
end

it 'generates target div with correct id' do
html = build_cell('/dynamic_model/test/789').cell_content_for_embedded_block

expect(html).to include('id="report-result-embedded-block--789"')
expect(html).to include('class="report-temp-embedded-block"')
end

it 'preserves full URL including /edit for edit mode' do
html = build_cell('/dynamic_model/test/123/edit').cell_content_for_embedded_block

expect(html).to include('href="/dynamic_model/test/123/edit"')
end
end
end
end
Loading