From 4193bffa45908f76732bcef3e2d61e4e06ddb900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C4=82LTEANU?= Date: Fri, 25 Jul 2025 00:50:18 +0300 Subject: [PATCH] WIP --- app/controllers/issues_controller.rb | 4 ++ app/controllers/wiki_controller.rb | 2 + .../controllers/task_list_controller.js | 62 +++++++++++++++++++ app/models/concerns/task_list_updatable.rb | 40 ++++++++++++ app/models/issue.rb | 1 + app/models/wiki_content.rb | 3 + app/views/issues/show.html.erb | 5 +- app/views/wiki/_content.html.erb | 4 +- .../wiki_formatting/common_mark/formatter.rb | 1 + .../common_mark/markdown_filter.rb | 4 +- .../common_mark/sanitization_filter.rb | 2 +- 11 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 app/javascript/controllers/task_list_controller.js create mode 100644 app/models/concerns/task_list_updatable.rb diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index a9c3f6183..ccd3fad87 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -198,6 +198,10 @@ class IssuesController < ApplicationController def update return unless update_issue_from_params + if params[:sourcepos].present? + @issue.update_task_list_item(:description, params[:sourcepos], params[:checked]) + end + attachments = params[:attachments] || params.dig(:issue, :uploads) if @issue.attachments_addable? @issue.save_attachments(attachments) diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index bcb3b0891..16cfb229b 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -172,6 +172,8 @@ class WikiController < ApplicationController @section = params[:section].to_i @section_hash = params[:section_hash] @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(@section, @text, @section_hash) + elsif params[:sourcepos].present? + @content.update_task_list_item(:text, params[:sourcepos], params[:checked]) else @content.version = content_params[:version] if content_params[:version] @content.text = @text diff --git a/app/javascript/controllers/task_list_controller.js b/app/javascript/controllers/task_list_controller.js new file mode 100644 index 000000000..38137e357 --- /dev/null +++ b/app/javascript/controllers/task_list_controller.js @@ -0,0 +1,62 @@ +import {Controller} from '@hotwired/stimulus' + +export default class extends Controller { + // Define the 'updateUrl' value that we passed from the HTML + static values = { + updateUrl: String, + }; + + connect() { + // Find all checkboxes within this controller's scope and enable them. + this.element.querySelectorAll('input[type="checkbox"].task-list-item-checkbox').forEach(checkbox => { + checkbox.disabled = false; + + checkbox.addEventListener('change', (event) => { + const isChecked = event.target.checked; + const sourcePosition = checkbox.parentElement.dataset.sourcepos; + + if (!sourcePosition) { + console.error('Task item is missing data-sourcepos attribute.'); + // Optionally revert the checkbox and show an error + event.target.checked = !isChecked; + return; + } + + this.updateTaskListItem(this.updateUrlValue, sourcePosition, isChecked); + }); + }); + } + + async updateTaskListItem(updateUrl, sourcePosition, isChecked) { + + // You need to get the CSRF token for Rails + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + + try { + const response = await fetch(updateUrl, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-Token': csrfToken, + }, + body: JSON.stringify({ + sourcepos: sourcePosition, + checked: isChecked, + }), + }); + + if (!response.ok) { + // Handle server error: revert the checkbox state and alert the user + throw new Error(); + } + + console.log('Task updated successfully!'); + + } catch (error) { + console.error('Error:', error); + // Revert the checkbox state + } + } +} + diff --git a/app/models/concerns/task_list_updatable.rb b/app/models/concerns/task_list_updatable.rb new file mode 100644 index 000000000..ddc8f6c15 --- /dev/null +++ b/app/models/concerns/task_list_updatable.rb @@ -0,0 +1,40 @@ +# app/models/concerns/task_list_updatable.rb +module TaskListUpdatable + extend ActiveSupport::Concern + + # Updates a task list item in a text attribute based on its position. + # This method modifies the attribute on the object in memory; it does not save the record. + # + # @param attribute_name [Symbol] The name of the text attribute (e.g., :description). + # @param source_pos [String] The position of the task item, e.g., "10:1-10:15". + # @param checked_value [Object] The value from params indicating the new checked state. + def update_task_list_item(attribute_name, source_pos, checked_value) + text = public_send(attribute_name) + + Rails.logger.debug "*****"*88 + Rails.logger.debug text.inspect + return if text.blank? || source_pos.blank? + + # The line number from sourcepos is 1-based. + line_number = source_pos.to_s.split(':').first.to_i + return if line_number <= 0 + + lines = text.lines + # The lines array is 0-based, so we subtract 1. + target_line = lines[line_number - 1] + return unless target_line + + # Robustly determine the checked state from params. + is_checked = [true, 'true', '1'].include?(checked_value) + + if is_checked + # Change first '[ ]' to '[x]' on the line. + target_line.sub!(/\[\s\]/, '[x]') + else + # Change first '[x]' or '[X]' to '[ ]' on the line. + target_line.sub!(/\[x\]/i, '[ ]') + end + + public_send(:"#{attribute_name}=", lines.join) + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 576840843..26ce1f805 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -26,6 +26,7 @@ class Issue < ApplicationRecord before_save :set_parent_id include Redmine::NestedSet::IssueNestedSet include Redmine::Reaction::Reactable + include TaskListUpdatable belongs_to :project belongs_to :tracker diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb index ff1431941..ca33c30e3 100644 --- a/app/models/wiki_content.rb +++ b/app/models/wiki_content.rb @@ -21,6 +21,9 @@ require 'zlib' class WikiContent < ApplicationRecord self.locking_column = 'version' + + include TaskListUpdatable + belongs_to :page, :class_name => 'WikiPage' belongs_to :author, :class_name => 'User' has_many :versions, :class_name => 'WikiContentVersion', :dependent => :delete_all diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb index 696b8f0ec..62fb40db2 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -98,7 +98,10 @@ end %>

<%=l(:field_description)%>

-
+
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
diff --git a/app/views/wiki/_content.html.erb b/app/views/wiki/_content.html.erb index 60b6a7452..9c5a303b1 100644 --- a/app/views/wiki/_content.html.erb +++ b/app/views/wiki/_content.html.erb @@ -1,4 +1,6 @@ -
+
<%= textilizable content, :text, :attachments => content.page.attachments, :edit_section_links => (@sections_editable && {:controller => 'wiki', :action => 'edit', :project_id => @page.project, :id => @page.title}) %>
diff --git a/lib/redmine/wiki_formatting/common_mark/formatter.rb b/lib/redmine/wiki_formatting/common_mark/formatter.rb index 8b7a18394..4c0b7036e 100644 --- a/lib/redmine/wiki_formatting/common_mark/formatter.rb +++ b/lib/redmine/wiki_formatting/common_mark/formatter.rb @@ -47,6 +47,7 @@ module Redmine github_pre_lang: false, hardbreaks: Redmine::Configuration['common_mark_enable_hardbreaks'] == true, tasklist_classes: true, + sourcepos: true, }.freeze, commonmarker_plugins: { syntax_highlighter: nil diff --git a/lib/redmine/wiki_formatting/common_mark/markdown_filter.rb b/lib/redmine/wiki_formatting/common_mark/markdown_filter.rb index b52a20f1c..cf5c92818 100644 --- a/lib/redmine/wiki_formatting/common_mark/markdown_filter.rb +++ b/lib/redmine/wiki_formatting/common_mark/markdown_filter.rb @@ -33,10 +33,10 @@ module Redmine def call html = Commonmarker.to_html(@text, options: { - extension: extensions, + extension: extensions, render: render_options, parse: parse_options - }, plugins: plugins) + }, plugins: plugins) html.rstrip! html diff --git a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb index af72adc32..5be803f0e 100644 --- a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb +++ b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb @@ -112,7 +112,7 @@ module Redmine # allow `id` in li element for footnotes # allow `class` in li element for task list items - allowlist[:attributes]["li"] = %w(id class) + allowlist[:attributes]["li"] = %w(id class data-sourcepos) allowlist[:transformers].push lambda{|env| node = env[:node] return unless node.name == "li" -- 2.39.5 (Apple Git-154)