Class: CoPlan::CommentThread

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

Constant Summary collapse

STATUSES =
%w[open resolved accepted dismissed].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.



5
6
7
# File 'app/models/coplan/comment_thread.rb', line 5

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.



28
29
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
# File 'app/models/coplan/comment_thread.rb', line 28

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



96
97
98
# File 'app/models/coplan/comment_thread.rb', line 96

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

#anchor_context_with_highlight(chars: 100) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'app/models/coplan/comment_thread.rb', line 128

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.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'app/models/coplan/comment_thread.rb', line 112

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



87
88
89
90
# File 'app/models/coplan/comment_thread.rb', line 87

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)


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

def anchor_valid?
  return true unless anchored?
  !out_of_date
end

#anchored?Boolean

Returns:

  • (Boolean)


74
75
76
# File 'app/models/coplan/comment_thread.rb', line 74

def anchored?
  anchor_text.present?
end

#dismiss!(user) ⇒ Object



100
101
102
# File 'app/models/coplan/comment_thread.rb', line 100

def dismiss!(user)
  update!(status: "dismissed", resolved_by_user: user)
end

#line_range_textObject



82
83
84
85
# File 'app/models/coplan/comment_thread.rb', line 82

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)


78
79
80
# File 'app/models/coplan/comment_thread.rb', line 78

def line_specific?
  start_line.present?
end

#resolve!(user) ⇒ Object



92
93
94
# File 'app/models/coplan/comment_thread.rb', line 92

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