Class: CoPlan::CommentThread

Inherits:
ApplicationRecord show all
Defined in:
app/models/coplan/comment_thread.rb

Constant Summary collapse

STATUSES =
%w[pending todo resolved discarded].freeze
OPEN_STATUSES =
%w[pending todo].freeze
CLOSED_STATUSES =
%w[resolved discarded].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#anchor_occurrenceObject

Returns the value of attribute anchor_occurrence.



7
8
9
# File 'app/models/coplan/comment_thread.rb', line 7

def anchor_occurrence
  @anchor_occurrence
end

Class Method Details

.mark_out_of_date_for_new_version!(new_version) ⇒ Object

Transforms anchor positions through intervening version edits using OT. Threads without positional data (anchor_start/anchor_end/anchor_revision) are marked out-of-date unconditionally — all new threads resolve positions on creation via resolve_anchor_position.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'app/models/coplan/comment_thread.rb', line 30

def self.mark_out_of_date_for_new_version!(new_version)
  threads = where(out_of_date: false).where.not(plan_version_id: new_version.id)
  anchored_threads = threads.select(&:anchored?)

  # Pre-fetch all versions that any thread might need (from the oldest
  # anchor_revision to the new version) in a single query.
  min_anchor_rev = anchored_threads
    .filter_map { |t| t.anchor_revision if t.anchor_start.present? && t.anchor_end.present? && t.anchor_revision.present? }
    .min

  all_versions = if min_anchor_rev
    new_version.plan.plan_versions
      .where("revision > ? AND revision <= ?", min_anchor_rev, new_version.revision)
      .order(revision: :asc)
      .to_a
  else
    []
  end

  anchored_threads.each do |thread|
    unless thread.anchor_start.present? && thread.anchor_end.present? && thread.anchor_revision.present?
      thread.update_columns(out_of_date: true, out_of_date_since_version_id: new_version.id)
      next
    end

    intervening = all_versions.select { |v| v.revision > thread.anchor_revision && v.revision <= new_version.revision }

    begin
      new_range = Plans::TransformRange.transform_through_versions(
        [thread.anchor_start, thread.anchor_end],
        intervening
      )
      thread.update_columns(
        anchor_start: new_range[0],
        anchor_end: new_range[1],
        anchor_revision: new_version.revision
      )
    rescue Plans::TransformRange::Conflict
      thread.update_columns(
        out_of_date: true,
        out_of_date_since_version_id: new_version.id
      )
    end
  end
end

Instance Method Details

#accept!(user) ⇒ Object



98
99
100
# File 'app/models/coplan/comment_thread.rb', line 98

def accept!(user)
  update!(status: "todo", resolved_by_user: user)
end

#anchor_context_with_highlight(chars: 100) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'app/models/coplan/comment_thread.rb', line 134

def anchor_context_with_highlight(chars: 100)
  return nil unless anchored? && anchor_start.present?

  content = plan.current_content
  return nil unless content.present?

  context_start = [anchor_start - chars, 0].max
  context_end = [anchor_end + chars, content.length].min

  before = content[context_start...anchor_start]
  anchor = content[anchor_start...anchor_end]
  after = content[anchor_end...context_end]

  "#{before}**#{anchor}**#{after}"
end

#anchor_occurrence_indexObject

Returns the 0-based occurrence index of anchor_text in the raw markdown, computed from anchor_start. The frontend uses this to find the correct occurrence in the rendered DOM text instead of relying on context matching.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'app/models/coplan/comment_thread.rb', line 118

def anchor_occurrence_index
  return nil unless anchored? && anchor_start.present?

  content = plan.current_content
  return nil unless content.present?

  count = 0
  pos = 0
  while (idx = content.index(anchor_text, pos))
    break if idx >= anchor_start
    count += 1
    pos = idx + 1
  end
  count
end

#anchor_preview(max_length: 80) ⇒ Object



89
90
91
92
# File 'app/models/coplan/comment_thread.rb', line 89

def anchor_preview(max_length: 80)
  return nil unless anchored?
  anchor_text.length > max_length ? "#{anchor_text[0...max_length]}" : anchor_text
end

#anchor_valid?Boolean

Returns:

  • (Boolean)


110
111
112
113
# File 'app/models/coplan/comment_thread.rb', line 110

def anchor_valid?
  return true unless anchored?
  !out_of_date
end

#anchored?Boolean

Returns:

  • (Boolean)


76
77
78
# File 'app/models/coplan/comment_thread.rb', line 76

def anchored?
  anchor_text.present?
end

#discard!(user) ⇒ Object



102
103
104
# File 'app/models/coplan/comment_thread.rb', line 102

def discard!(user)
  update!(status: "discarded", resolved_by_user: user)
end

#line_range_textObject



84
85
86
87
# File 'app/models/coplan/comment_thread.rb', line 84

def line_range_text
  return nil unless line_specific?
  start_line == end_line ? "Line #{start_line}" : "Lines #{start_line}#{end_line}"
end

#line_specific?Boolean

Returns:

  • (Boolean)


80
81
82
# File 'app/models/coplan/comment_thread.rb', line 80

def line_specific?
  start_line.present?
end

#open?Boolean

Returns:

  • (Boolean)


106
107
108
# File 'app/models/coplan/comment_thread.rb', line 106

def open?
  OPEN_STATUSES.include?(status)
end

#resolve!(user) ⇒ Object



94
95
96
# File 'app/models/coplan/comment_thread.rb', line 94

def resolve!(user)
  update!(status: "resolved", resolved_by_user: user)
end