coplan-engine 0.1.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/coplan/application.css +268 -85
- data/app/controllers/coplan/api/v1/comments_controller.rb +7 -7
- data/app/controllers/coplan/api/v1/operations_controller.rb +38 -0
- data/app/controllers/coplan/comment_threads_controller.rb +26 -72
- data/app/controllers/coplan/plans_controller.rb +1 -3
- data/app/helpers/coplan/application_helper.rb +44 -0
- data/app/helpers/coplan/markdown_helper.rb +1 -0
- data/app/javascript/controllers/coplan/comment_form_controller.js +14 -0
- data/app/javascript/controllers/coplan/comment_nav_controller.js +247 -0
- data/app/javascript/controllers/coplan/text_selection_controller.js +143 -169
- data/app/jobs/coplan/automated_review_job.rb +4 -4
- data/app/models/coplan/comment_thread.rb +13 -7
- data/app/policies/coplan/comment_thread_policy.rb +1 -1
- data/app/services/coplan/plans/apply_operations.rb +43 -0
- data/app/services/coplan/plans/commit_session.rb +26 -1
- data/app/services/coplan/plans/position_resolver.rb +111 -0
- data/app/views/coplan/comment_threads/_new_comment_form.html.erb +2 -1
- data/app/views/coplan/comment_threads/_reply_form.html.erb +2 -1
- data/app/views/coplan/comment_threads/_thread.html.erb +3 -6
- data/app/views/coplan/comment_threads/_thread_popover.html.erb +58 -0
- data/app/views/coplan/plans/show.html.erb +22 -30
- data/app/views/layouts/coplan/application.html.erb +5 -0
- data/config/routes.rb +2 -2
- data/db/migrate/20260320145453_migrate_comment_thread_statuses.rb +31 -0
- data/lib/coplan/version.rb +1 -1
- metadata +5 -2
- data/app/javascript/controllers/coplan/tabs_controller.js +0 -18
|
@@ -20,6 +20,8 @@ module CoPlan
|
|
|
20
20
|
resolve_insert_under_heading
|
|
21
21
|
when "delete_paragraph_containing"
|
|
22
22
|
resolve_delete_paragraph_containing
|
|
23
|
+
when "replace_section"
|
|
24
|
+
resolve_replace_section
|
|
23
25
|
else
|
|
24
26
|
raise OperationError, "Unknown operation: #{@op["op"]}"
|
|
25
27
|
end
|
|
@@ -153,6 +155,115 @@ module CoPlan
|
|
|
153
155
|
paragraphs
|
|
154
156
|
end
|
|
155
157
|
|
|
158
|
+
def resolve_replace_section
|
|
159
|
+
heading = @op["heading"]
|
|
160
|
+
raise OperationError, "replace_section requires 'heading'" if heading.blank?
|
|
161
|
+
raise OperationError, "replace_section requires 'new_content'" if @op["new_content"].nil?
|
|
162
|
+
|
|
163
|
+
include_heading = @op.fetch("include_heading", true)
|
|
164
|
+
# Normalize: accept both string and boolean
|
|
165
|
+
include_heading = include_heading != false && include_heading != "false"
|
|
166
|
+
|
|
167
|
+
headings = parse_headings(@content)
|
|
168
|
+
matches = headings.select { |h| h[:text] == heading }
|
|
169
|
+
|
|
170
|
+
if matches.empty?
|
|
171
|
+
raise OperationError, "replace_section: heading_not_found — no heading matching '#{heading}'"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if matches.length > 1
|
|
175
|
+
match_details = matches.map { |m| { heading: m[:text], line: m[:line] } }
|
|
176
|
+
raise OperationError, "replace_section: ambiguous_heading — found #{matches.length} headings matching '#{heading}': #{match_details.inspect}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
match = matches.first
|
|
180
|
+
target_level = match[:level]
|
|
181
|
+
|
|
182
|
+
# Section starts at the heading line start
|
|
183
|
+
section_start = match[:line_start]
|
|
184
|
+
|
|
185
|
+
# Section ends at the next heading of equal or higher level, or EOF
|
|
186
|
+
next_heading = headings.find { |h| h[:line_start] > match[:line_start] && h[:level] <= target_level }
|
|
187
|
+
section_end = next_heading ? next_heading[:line_start] : @content.length
|
|
188
|
+
|
|
189
|
+
# Strip all trailing newlines from the section range so the separator
|
|
190
|
+
# between sections falls outside the replaced range. This ensures
|
|
191
|
+
# replacement content won't merge into the next heading.
|
|
192
|
+
section_end = section_end.to_i
|
|
193
|
+
while section_end > section_start && @content[section_end - 1] == "\n"
|
|
194
|
+
section_end -= 1
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
range = if include_heading
|
|
198
|
+
[section_start, section_end]
|
|
199
|
+
else
|
|
200
|
+
# Skip past the heading line itself
|
|
201
|
+
heading_line_end = @content.index("\n", section_start)
|
|
202
|
+
if heading_line_end
|
|
203
|
+
body_start = heading_line_end + 1
|
|
204
|
+
# Skip blank line after heading
|
|
205
|
+
while body_start < section_end && @content[body_start] == "\n"
|
|
206
|
+
body_start += 1
|
|
207
|
+
end
|
|
208
|
+
# When trailing newlines are stripped, section_end can retreat
|
|
209
|
+
# behind body_start. Use an empty range at body_start to avoid
|
|
210
|
+
# an inverted range and keep the insertion point after the heading newline.
|
|
211
|
+
[body_start, [body_start, section_end].max]
|
|
212
|
+
else
|
|
213
|
+
# Heading is the only line — body is empty
|
|
214
|
+
[section_end, section_end]
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
Resolution.new(op: "replace_section", ranges: [range])
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Parse markdown headings, respecting code fences (``` blocks).
|
|
222
|
+
# Returns an array of hashes: { text:, level:, line:, line_start:, line_end: }
|
|
223
|
+
def parse_headings(content)
|
|
224
|
+
headings = []
|
|
225
|
+
in_code_fence = false
|
|
226
|
+
fence_char = nil
|
|
227
|
+
fence_length = 0
|
|
228
|
+
line_number = 0
|
|
229
|
+
pos = 0
|
|
230
|
+
|
|
231
|
+
content.each_line do |line|
|
|
232
|
+
line_number += 1
|
|
233
|
+
line_start = pos
|
|
234
|
+
line_end = pos + line.length
|
|
235
|
+
stripped = line.chomp
|
|
236
|
+
|
|
237
|
+
fence_match = stripped.match(/\A(`{3,}|~{3,})(.*)\z/)
|
|
238
|
+
if fence_match
|
|
239
|
+
fence_chars = fence_match[1]
|
|
240
|
+
info_string = fence_match[2]
|
|
241
|
+
if in_code_fence
|
|
242
|
+
# Close only if fence char and length match, and no info string
|
|
243
|
+
if fence_chars[0] == fence_char && fence_chars.length >= fence_length && info_string.strip.empty?
|
|
244
|
+
in_code_fence = false
|
|
245
|
+
end
|
|
246
|
+
else
|
|
247
|
+
in_code_fence = true
|
|
248
|
+
fence_char = fence_chars[0]
|
|
249
|
+
fence_length = fence_chars.length
|
|
250
|
+
end
|
|
251
|
+
elsif !in_code_fence && (m = stripped.match(/\A(\#{1,6})\s+(.+)/))
|
|
252
|
+
headings << {
|
|
253
|
+
level: m[1].length,
|
|
254
|
+
text: stripped,
|
|
255
|
+
line: line_number,
|
|
256
|
+
line_start: line_start,
|
|
257
|
+
line_end: line_end
|
|
258
|
+
}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
pos = line_end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
headings
|
|
265
|
+
end
|
|
266
|
+
|
|
156
267
|
# Determine the character range to delete so that removing
|
|
157
268
|
# content[range[0]...range[1]] produces clean output with
|
|
158
269
|
# correct paragraph spacing.
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
<blockquote class="comment-form__quote" data-coplan--text-selection-target="anchorQuote"></blockquote>
|
|
9
9
|
</div>
|
|
10
10
|
<div class="form-group">
|
|
11
|
-
<textarea name="comment_thread[body_markdown]" id="comment_thread_body_markdown" rows="3" placeholder="Write a comment..." required
|
|
11
|
+
<textarea name="comment_thread[body_markdown]" id="comment_thread_body_markdown" rows="3" placeholder="Write a comment..." required
|
|
12
|
+
data-controller="coplan--comment-form" data-action="keydown->coplan--comment-form#submitOnEnter"></textarea>
|
|
12
13
|
</div>
|
|
13
14
|
<div class="comment-form__actions">
|
|
14
15
|
<button type="submit" class="btn btn--primary btn--sm">Comment</button>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<div class="comment-thread__reply" id="<%= dom_id(thread, :reply_form) %>">
|
|
2
2
|
<%= form_with url: plan_comment_thread_comments_path(plan, thread), method: :post, data: { action: "turbo:submit-end->coplan--text-selection#resetReplyForm" } do |f| %>
|
|
3
3
|
<div class="form-group">
|
|
4
|
-
<textarea name="comment[body_markdown]" rows="2" placeholder="
|
|
4
|
+
<textarea name="comment[body_markdown]" rows="2" placeholder="Press r to reply" required
|
|
5
|
+
data-controller="coplan--comment-form" data-action="keydown->coplan--comment-form#submitOnEnter"></textarea>
|
|
5
6
|
</div>
|
|
6
7
|
<button type="submit" class="btn btn--secondary btn--sm">Reply</button>
|
|
7
8
|
<% end %>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
data-thread-id="<%= thread.id %>">
|
|
6
6
|
<div class="comment-thread__header">
|
|
7
7
|
<div class="comment-thread__meta">
|
|
8
|
-
<span class="badge badge--<%= thread.
|
|
8
|
+
<span class="badge badge--<%= thread.open? ? 'live' : 'abandoned' %>"><%= thread.status %></span>
|
|
9
9
|
<% if thread.out_of_date? %>
|
|
10
10
|
<span class="badge badge--abandoned">out of date</span>
|
|
11
11
|
<% end %>
|
|
@@ -26,16 +26,13 @@
|
|
|
26
26
|
<% is_plan_author = viewer.nil? || plan.created_by_user_id == viewer.id %>
|
|
27
27
|
<% is_thread_author = viewer.nil? || thread.created_by_user_id == viewer.id %>
|
|
28
28
|
|
|
29
|
-
<% if thread.
|
|
29
|
+
<% if thread.open? %>
|
|
30
30
|
<%= render partial: "coplan/comment_threads/reply_form", locals: { thread: thread, plan: plan } %>
|
|
31
31
|
|
|
32
32
|
<div class="comment-thread__actions">
|
|
33
|
-
<% if is_plan_author || is_thread_author %>
|
|
34
|
-
<%= link_to "Resolve", resolve_plan_comment_thread_path(plan, thread), data: { turbo_method: :patch }, class: "btn btn--secondary btn--sm" %>
|
|
35
|
-
<% end %>
|
|
36
33
|
<% if is_plan_author %>
|
|
37
34
|
<%= link_to "Accept", accept_plan_comment_thread_path(plan, thread), data: { turbo_method: :patch }, class: "btn btn--secondary btn--sm" %>
|
|
38
|
-
<%= link_to "
|
|
35
|
+
<%= link_to "Discard", discard_plan_comment_thread_path(plan, thread), data: { turbo_method: :patch }, class: "btn btn--secondary btn--sm" %>
|
|
39
36
|
<% end %>
|
|
40
37
|
</div>
|
|
41
38
|
<% else %>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<div class="thread-popover-data"
|
|
2
|
+
id="<%= dom_id(thread) %>"
|
|
3
|
+
data-anchor-text="<%= thread.anchor_text %>"
|
|
4
|
+
data-anchor-occurrence="<%= thread.anchor_occurrence_index %>"
|
|
5
|
+
data-thread-id="<%= thread.id %>"
|
|
6
|
+
data-thread-status="<%= thread.status %>">
|
|
7
|
+
|
|
8
|
+
<div popover="auto" id="<%= dom_id(thread) %>_popover" class="thread-popover">
|
|
9
|
+
<div class="thread-popover__header">
|
|
10
|
+
<span class="badge badge--<%= thread.status %>"><%= thread.status %></span>
|
|
11
|
+
<% if thread.out_of_date? %>
|
|
12
|
+
<span class="badge badge--abandoned">out of date</span>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<% if thread.anchored? %>
|
|
17
|
+
<blockquote class="thread-popover__quote"><%= thread.anchor_preview %></blockquote>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
<div class="thread-popover__comments" id="<%= dom_id(thread, :comments) %>">
|
|
21
|
+
<% thread.comments.each do |comment| %>
|
|
22
|
+
<%= render partial: "coplan/comments/comment", locals: { comment: comment } %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<%# current_user may not be available during Turbo Stream broadcasts %>
|
|
27
|
+
<% viewer = local_assigns.fetch(:current_user, nil) %>
|
|
28
|
+
<% is_plan_author = viewer.nil? || plan.created_by_user_id == viewer&.id %>
|
|
29
|
+
<% is_thread_author = viewer.nil? || thread.created_by_user_id == viewer&.id %>
|
|
30
|
+
|
|
31
|
+
<% if thread.open? %>
|
|
32
|
+
<div class="thread-popover__reply">
|
|
33
|
+
<%= form_with url: plan_comment_thread_comments_path(plan, thread), method: :post, data: { action: "turbo:submit-end->coplan--text-selection#resetReplyForm" } do |f| %>
|
|
34
|
+
<div class="form-group">
|
|
35
|
+
<textarea name="comment[body_markdown]" rows="2" placeholder="Press r to reply" required
|
|
36
|
+
data-controller="coplan--comment-form" data-action="keydown->coplan--comment-form#submitOnEnter"></textarea>
|
|
37
|
+
</div>
|
|
38
|
+
<button type="submit" class="btn btn--secondary btn--sm">Reply</button>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="thread-popover__actions">
|
|
43
|
+
<% if is_plan_author %>
|
|
44
|
+
<% if thread.status == "pending" %>
|
|
45
|
+
<%= button_to "Accept (a)", accept_plan_comment_thread_path(plan, thread), method: :patch, class: "btn btn--secondary btn--sm", form: { "data-action-name": "accept" } %>
|
|
46
|
+
<% end %>
|
|
47
|
+
<%= button_to "Discard (d)", discard_plan_comment_thread_path(plan, thread), method: :patch, class: "btn btn--secondary btn--sm", form: { "data-action-name": "discard" } %>
|
|
48
|
+
<% end %>
|
|
49
|
+
</div>
|
|
50
|
+
<% else %>
|
|
51
|
+
<% if is_plan_author || is_thread_author %>
|
|
52
|
+
<div class="thread-popover__actions">
|
|
53
|
+
<%= button_to "Reopen", reopen_plan_comment_thread_path(plan, thread), method: :patch, class: "btn btn--secondary btn--sm" %>
|
|
54
|
+
</div>
|
|
55
|
+
<% end %>
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<div class="plan-content card">
|
|
6
6
|
<% if @plan.current_content.present? %>
|
|
7
7
|
<div class="plan-layout" data-controller="coplan--text-selection" data-coplan--text-selection-plan-id-value="<%= @plan.id %>">
|
|
8
|
+
<div class="plan-layout__margin" data-coplan--text-selection-target="margin"></div>
|
|
8
9
|
<div class="plan-layout__content" data-coplan--text-selection-target="content">
|
|
9
10
|
<%= render_markdown(@plan.current_content) %>
|
|
10
11
|
|
|
@@ -13,39 +14,14 @@
|
|
|
13
14
|
💬 Comment
|
|
14
15
|
</button>
|
|
15
16
|
</div>
|
|
16
|
-
</div>
|
|
17
17
|
|
|
18
|
-
<div class="plan-layout__sidebar">
|
|
19
18
|
<%= render partial: "coplan/comment_threads/new_comment_form", locals: { plan: @plan } %>
|
|
19
|
+
</div>
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
</button>
|
|
26
|
-
<button class="comment-tab" data-coplan--tabs-target="tab" data-action="coplan--tabs#switch coplan--text-selection#repositionThreads" data-index="1">
|
|
27
|
-
Resolved <span class="comment-tab__count" id="resolved-thread-count"><%= @archived_threads.size if @archived_threads.any? %></span>
|
|
28
|
-
</button>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<div class="comment-tabs__panel" data-coplan--tabs-target="panel">
|
|
32
|
-
<div class="comment-threads-list" id="comment-threads">
|
|
33
|
-
<% @active_threads.each do |thread| %>
|
|
34
|
-
<%= render partial: "coplan/comment_threads/thread", locals: { thread: thread, plan: @plan, current_user: current_user } %>
|
|
35
|
-
<% end %>
|
|
36
|
-
<p class="text-sm text-muted" id="open-threads-empty" <% if @active_threads.any? %>style="display: none;"<% end %>>No open comments.</p>
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<div class="comment-tabs__panel" data-coplan--tabs-target="panel" style="display: none;">
|
|
41
|
-
<div class="comment-threads-list" id="resolved-comment-threads">
|
|
42
|
-
<% @archived_threads.each do |thread| %>
|
|
43
|
-
<%= render partial: "coplan/comment_threads/thread", locals: { thread: thread, plan: @plan, current_user: current_user } %>
|
|
44
|
-
<% end %>
|
|
45
|
-
<p class="text-sm text-muted" id="resolved-threads-empty" <% if @archived_threads.any? %>style="display: none;"<% end %>>No resolved comments.</p>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
21
|
+
<div id="plan-threads" data-coplan--text-selection-target="threads">
|
|
22
|
+
<% @threads.select(&:anchored?).each do |thread| %>
|
|
23
|
+
<%= render partial: "coplan/comment_threads/thread_popover", locals: { thread: thread, plan: @plan, current_user: current_user } %>
|
|
24
|
+
<% end %>
|
|
49
25
|
</div>
|
|
50
26
|
</div>
|
|
51
27
|
<% else %>
|
|
@@ -54,3 +30,19 @@
|
|
|
54
30
|
</div>
|
|
55
31
|
<% end %>
|
|
56
32
|
</div>
|
|
33
|
+
|
|
34
|
+
<% open_count = @threads.count(&:open?) %>
|
|
35
|
+
<% if @threads.any? %>
|
|
36
|
+
<div class="comment-toolbar" data-controller="coplan--comment-nav" data-coplan--comment-nav-plan-id-value="<%= @plan.id %>">
|
|
37
|
+
<span class="comment-toolbar__count">💬 <%= open_count %> open</span>
|
|
38
|
+
<div class="comment-toolbar__nav">
|
|
39
|
+
<button class="btn btn--secondary btn--sm" data-action="coplan--comment-nav#prev" title="Previous comment">↑</button>
|
|
40
|
+
<span class="comment-toolbar__position" data-coplan--comment-nav-target="position"></span>
|
|
41
|
+
<button class="btn btn--secondary btn--sm" data-action="coplan--comment-nav#next" title="Next comment">↓</button>
|
|
42
|
+
</div>
|
|
43
|
+
<label class="comment-toolbar__toggle">
|
|
44
|
+
<input type="checkbox" data-action="coplan--comment-nav#toggleResolved" data-coplan--comment-nav-target="resolvedToggle">
|
|
45
|
+
Show resolved (<%= @threads.count { |t| !t.open? } %>)
|
|
46
|
+
</label>
|
|
47
|
+
</div>
|
|
48
|
+
<% end %>
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
6
|
<%= csrf_meta_tags %>
|
|
7
7
|
<%= csp_meta_tag %>
|
|
8
|
+
<%= coplan_favicon_tag %>
|
|
8
9
|
<%= yield :head %>
|
|
10
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
11
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
12
|
+
<link href="https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
9
13
|
<%= stylesheet_link_tag "coplan/application", "data-turbo-track": "reload" %>
|
|
10
14
|
<%= javascript_importmap_tags %>
|
|
11
15
|
</head>
|
|
@@ -15,6 +19,7 @@
|
|
|
15
19
|
<%= link_to coplan.root_path, class: "site-nav__brand" do %>
|
|
16
20
|
<%= image_tag "coplan/coplan-logo-sm.png", alt: "CoPlan", class: "site-nav__logo" %>
|
|
17
21
|
CoPlan
|
|
22
|
+
<%= coplan_environment_badge %>
|
|
18
23
|
<% end %>
|
|
19
24
|
<ul class="site-nav__links">
|
|
20
25
|
<% if signed_in? %>
|
data/config/routes.rb
CHANGED
|
@@ -7,7 +7,7 @@ CoPlan::Engine.routes.draw do
|
|
|
7
7
|
member do
|
|
8
8
|
patch :resolve
|
|
9
9
|
patch :accept
|
|
10
|
-
patch :
|
|
10
|
+
patch :discard
|
|
11
11
|
patch :reopen
|
|
12
12
|
end
|
|
13
13
|
resources :comments, only: [:create]
|
|
@@ -31,7 +31,7 @@ CoPlan::Engine.routes.draw do
|
|
|
31
31
|
resources :comments, only: [:create], controller: "comments" do
|
|
32
32
|
post :reply, on: :member
|
|
33
33
|
patch :resolve, on: :member
|
|
34
|
-
patch :
|
|
34
|
+
patch :discard, on: :member
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class MigrateCommentThreadStatuses < ActiveRecord::Migration[8.0]
|
|
2
|
+
def up
|
|
3
|
+
# open → pending (default for new comments from non-authors)
|
|
4
|
+
# accepted → resolved (collapse accepted into resolved)
|
|
5
|
+
# dismissed → discarded (rename)
|
|
6
|
+
# resolved stays resolved
|
|
7
|
+
#
|
|
8
|
+
# We also add "todo" as a new status (author agrees with feedback).
|
|
9
|
+
# Existing "open" threads become "pending" since we can't determine authorship here.
|
|
10
|
+
execute <<~SQL
|
|
11
|
+
UPDATE coplan_comment_threads SET status = 'pending' WHERE status = 'open'
|
|
12
|
+
SQL
|
|
13
|
+
execute <<~SQL
|
|
14
|
+
UPDATE coplan_comment_threads SET status = 'discarded' WHERE status = 'dismissed'
|
|
15
|
+
SQL
|
|
16
|
+
execute <<~SQL
|
|
17
|
+
UPDATE coplan_comment_threads SET status = 'resolved' WHERE status = 'accepted'
|
|
18
|
+
SQL
|
|
19
|
+
change_column_default :coplan_comment_threads, :status, "pending"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def down
|
|
23
|
+
change_column_default :coplan_comment_threads, :status, "open"
|
|
24
|
+
execute <<~SQL
|
|
25
|
+
UPDATE coplan_comment_threads SET status = 'open' WHERE status IN ('pending', 'todo')
|
|
26
|
+
SQL
|
|
27
|
+
execute <<~SQL
|
|
28
|
+
UPDATE coplan_comment_threads SET status = 'dismissed' WHERE status = 'discarded'
|
|
29
|
+
SQL
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/coplan/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: coplan-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Block
|
|
@@ -159,9 +159,10 @@ files:
|
|
|
159
159
|
- app/helpers/coplan/application_helper.rb
|
|
160
160
|
- app/helpers/coplan/comments_helper.rb
|
|
161
161
|
- app/helpers/coplan/markdown_helper.rb
|
|
162
|
+
- app/javascript/controllers/coplan/comment_form_controller.js
|
|
163
|
+
- app/javascript/controllers/coplan/comment_nav_controller.js
|
|
162
164
|
- app/javascript/controllers/coplan/dropdown_controller.js
|
|
163
165
|
- app/javascript/controllers/coplan/line_selection_controller.js
|
|
164
|
-
- app/javascript/controllers/coplan/tabs_controller.js
|
|
165
166
|
- app/javascript/controllers/coplan/text_selection_controller.js
|
|
166
167
|
- app/jobs/coplan/application_job.rb
|
|
167
168
|
- app/jobs/coplan/automated_review_job.rb
|
|
@@ -197,6 +198,7 @@ files:
|
|
|
197
198
|
- app/views/coplan/comment_threads/_new_comment_form.html.erb
|
|
198
199
|
- app/views/coplan/comment_threads/_reply_form.html.erb
|
|
199
200
|
- app/views/coplan/comment_threads/_thread.html.erb
|
|
201
|
+
- app/views/coplan/comment_threads/_thread_popover.html.erb
|
|
200
202
|
- app/views/coplan/comments/_comment.html.erb
|
|
201
203
|
- app/views/coplan/dashboard/show.html.erb
|
|
202
204
|
- app/views/coplan/plan_versions/index.html.erb
|
|
@@ -216,6 +218,7 @@ files:
|
|
|
216
218
|
- config/routes.rb
|
|
217
219
|
- db/migrate/20260226200000_create_coplan_schema.rb
|
|
218
220
|
- db/migrate/20260313210000_expand_content_markdown_to_mediumtext.rb
|
|
221
|
+
- db/migrate/20260320145453_migrate_comment_thread_statuses.rb
|
|
219
222
|
- lib/coplan.rb
|
|
220
223
|
- lib/coplan/configuration.rb
|
|
221
224
|
- lib/coplan/engine.rb
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
|
|
3
|
-
export default class extends Controller {
|
|
4
|
-
static targets = ["tab", "panel"]
|
|
5
|
-
static classes = ["active"]
|
|
6
|
-
|
|
7
|
-
switch(event) {
|
|
8
|
-
const index = parseInt(event.currentTarget.dataset.index)
|
|
9
|
-
|
|
10
|
-
this.tabTargets.forEach((tab, i) => {
|
|
11
|
-
tab.classList.toggle(this.activeClass, i === index)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
this.panelTargets.forEach((panel, i) => {
|
|
15
|
-
panel.style.display = i === index ? "" : "none"
|
|
16
|
-
})
|
|
17
|
-
}
|
|
18
|
-
}
|