coplan-engine 0.4.0 → 1.0.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 +287 -0
- data/app/channels/coplan/plan_presence_channel.rb +45 -0
- data/app/controllers/coplan/api/v1/comments_controller.rb +2 -2
- data/app/controllers/coplan/api/v1/operations_controller.rb +3 -3
- data/app/controllers/coplan/api/v1/plans_controller.rb +2 -2
- data/app/controllers/coplan/api/v1/sessions_controller.rb +2 -2
- data/app/controllers/coplan/plans_controller.rb +1 -0
- data/app/controllers/coplan/settings/tokens_controller.rb +1 -1
- data/app/helpers/coplan/application_helper.rb +13 -0
- data/app/helpers/coplan/comments_helper.rb +4 -3
- data/app/helpers/coplan/markdown_helper.rb +6 -1
- data/app/javascript/controllers/coplan/content_nav_controller.js +252 -0
- data/app/javascript/controllers/coplan/presence_controller.js +44 -0
- data/app/javascript/controllers/coplan/text_selection_controller.js +145 -34
- data/app/models/coplan/comment.rb +4 -0
- data/app/models/coplan/comment_thread.rb +58 -16
- data/app/models/coplan/plan.rb +1 -0
- data/app/models/coplan/plan_viewer.rb +26 -0
- data/app/services/coplan/plans/markdown_text_extractor.rb +142 -0
- data/app/views/coplan/comments/_comment.html.erb +3 -0
- data/app/views/coplan/plans/_header.html.erb +1 -0
- data/app/views/coplan/plans/_viewers.html.erb +16 -0
- data/app/views/coplan/plans/show.html.erb +25 -3
- data/db/migrate/20260327000000_create_coplan_plan_viewers.rb +15 -0
- data/lib/coplan/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25dc4e49ba0c60553ed7d736f692711ed95908f904ced5ab0eab166a6afd5454
|
|
4
|
+
data.tar.gz: 9081eee65fbcadaef5bfb8af91a50bf67a1825382ff8763de0d3b1cb38a00ac4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d75ee11d917e817192149b2a69a33b1089333312ebd8ac4f7b64142479d5108c5b70e801c6b4ae3b64f78e9190d123164f4e5b87f56ced1aad7df0d9b72cb93
|
|
7
|
+
data.tar.gz: c6b96bd3fe16455118551bd01ae3769914bcc2f41d0b2bd59bf159ee8eddd6e84a7fb24ebb74461d522d07802b242b08bc8f691acf9772dabcc0864ab4fdfaed
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
--color-status-developing: #3b82f6;
|
|
22
22
|
--color-status-live: #10b981;
|
|
23
23
|
--color-status-abandoned: #6b7280;
|
|
24
|
+
--color-agent: #4338ca;
|
|
25
|
+
--color-agent-bg: #e0e7ff;
|
|
24
26
|
|
|
25
27
|
/* Spacing */
|
|
26
28
|
--space-xs: 0.25rem;
|
|
@@ -184,6 +186,87 @@ img, svg {
|
|
|
184
186
|
font-weight: 700;
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
/* Plan viewers / presence */
|
|
190
|
+
.plan-viewers {
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
gap: var(--space-sm);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.plan-viewers__avatars {
|
|
197
|
+
display: flex;
|
|
198
|
+
flex-direction: row-reverse;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.plan-viewers__avatar {
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
width: 2rem;
|
|
206
|
+
height: 2rem;
|
|
207
|
+
border-radius: 50%;
|
|
208
|
+
background: var(--color-primary);
|
|
209
|
+
color: #fff;
|
|
210
|
+
font-size: 0.7rem;
|
|
211
|
+
font-weight: 600;
|
|
212
|
+
border: 2px solid var(--color-surface);
|
|
213
|
+
margin-left: -0.5rem;
|
|
214
|
+
cursor: default;
|
|
215
|
+
position: relative;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.plan-viewers__avatar--you {
|
|
219
|
+
background: var(--color-success);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.plan-viewers__avatar::after {
|
|
223
|
+
content: attr(data-tooltip);
|
|
224
|
+
position: absolute;
|
|
225
|
+
bottom: calc(100% + 6px);
|
|
226
|
+
left: 50%;
|
|
227
|
+
transform: translateX(-50%);
|
|
228
|
+
background: var(--color-text);
|
|
229
|
+
color: var(--color-surface);
|
|
230
|
+
padding: 4px 8px;
|
|
231
|
+
border-radius: var(--radius);
|
|
232
|
+
font-size: 0.75rem;
|
|
233
|
+
font-weight: 500;
|
|
234
|
+
white-space: nowrap;
|
|
235
|
+
pointer-events: none;
|
|
236
|
+
opacity: 0;
|
|
237
|
+
transition: opacity 0.15s;
|
|
238
|
+
z-index: 10;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.plan-viewers__avatar:hover::after {
|
|
242
|
+
opacity: 1;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.plan-viewers__avatars > :last-child {
|
|
246
|
+
margin-left: 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.plan-viewers__overflow {
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
justify-content: center;
|
|
253
|
+
width: 2rem;
|
|
254
|
+
height: 2rem;
|
|
255
|
+
border-radius: 50%;
|
|
256
|
+
background: var(--color-border);
|
|
257
|
+
color: var(--color-text-muted);
|
|
258
|
+
font-size: 0.7rem;
|
|
259
|
+
font-weight: 600;
|
|
260
|
+
border: 2px solid var(--color-surface);
|
|
261
|
+
margin-left: -0.5rem;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.plan-viewers__label {
|
|
265
|
+
font-size: var(--text-sm);
|
|
266
|
+
color: var(--color-text-muted);
|
|
267
|
+
white-space: nowrap;
|
|
268
|
+
}
|
|
269
|
+
|
|
187
270
|
/* Flash messages */
|
|
188
271
|
.flash {
|
|
189
272
|
padding: var(--space-md) var(--space-lg);
|
|
@@ -283,6 +366,7 @@ img, svg {
|
|
|
283
366
|
.badge--todo { background: #dbeafe; color: var(--color-status-developing); }
|
|
284
367
|
.badge--discarded { background: #f3f4f6; color: var(--color-status-abandoned); }
|
|
285
368
|
.badge--resolved { background: #d1fae5; color: var(--color-status-live); }
|
|
369
|
+
.badge--agent { background: var(--color-agent-bg); color: var(--color-agent); font-size: 0.65rem; padding: 1px 6px; vertical-align: middle; }
|
|
286
370
|
|
|
287
371
|
/* Forms */
|
|
288
372
|
.form-group {
|
|
@@ -722,6 +806,18 @@ img, svg {
|
|
|
722
806
|
min-width: 0;
|
|
723
807
|
}
|
|
724
808
|
|
|
809
|
+
#plan-threads {
|
|
810
|
+
position: absolute;
|
|
811
|
+
width: 0;
|
|
812
|
+
height: 0;
|
|
813
|
+
overflow: hidden;
|
|
814
|
+
pointer-events: none;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
#plan-threads .thread-popover {
|
|
818
|
+
pointer-events: auto;
|
|
819
|
+
}
|
|
820
|
+
|
|
725
821
|
/* Comment form */
|
|
726
822
|
.comment-form {
|
|
727
823
|
position: absolute;
|
|
@@ -1143,3 +1239,194 @@ body:has(.comment-toolbar) .main-content {
|
|
|
1143
1239
|
.comment-toolbar__toggle input[type="checkbox"] {
|
|
1144
1240
|
cursor: pointer;
|
|
1145
1241
|
}
|
|
1242
|
+
|
|
1243
|
+
/* Content navigation sidebar */
|
|
1244
|
+
.content-nav {
|
|
1245
|
+
position: sticky;
|
|
1246
|
+
top: calc(var(--nav-height) + var(--space-lg));
|
|
1247
|
+
align-self: flex-start;
|
|
1248
|
+
width: 220px;
|
|
1249
|
+
flex-shrink: 0;
|
|
1250
|
+
max-height: calc(100vh - var(--nav-height) - var(--space-xl) * 2);
|
|
1251
|
+
transition: width 0.2s ease, opacity 0.2s ease;
|
|
1252
|
+
margin-right: var(--space-lg);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
.content-nav--hidden {
|
|
1256
|
+
width: 0;
|
|
1257
|
+
opacity: 0;
|
|
1258
|
+
overflow: hidden;
|
|
1259
|
+
margin-right: 0;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
.content-nav__header {
|
|
1263
|
+
display: flex;
|
|
1264
|
+
align-items: center;
|
|
1265
|
+
justify-content: space-between;
|
|
1266
|
+
padding-bottom: var(--space-sm);
|
|
1267
|
+
margin-bottom: var(--space-sm);
|
|
1268
|
+
border-bottom: 1px solid var(--color-border);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
.content-nav__title {
|
|
1272
|
+
font-size: var(--text-sm);
|
|
1273
|
+
font-weight: 600;
|
|
1274
|
+
color: var(--color-text-muted);
|
|
1275
|
+
text-transform: uppercase;
|
|
1276
|
+
letter-spacing: 0.05em;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
.content-nav__toggle {
|
|
1280
|
+
background: none;
|
|
1281
|
+
border: none;
|
|
1282
|
+
cursor: pointer;
|
|
1283
|
+
color: var(--color-text-muted);
|
|
1284
|
+
padding: 2px;
|
|
1285
|
+
font-size: var(--text-sm);
|
|
1286
|
+
line-height: 1;
|
|
1287
|
+
border-radius: var(--radius);
|
|
1288
|
+
transition: color 0.15s;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
.content-nav__toggle:hover {
|
|
1292
|
+
color: var(--color-text);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
.content-nav__list {
|
|
1296
|
+
list-style: none;
|
|
1297
|
+
padding: 0;
|
|
1298
|
+
margin: 0;
|
|
1299
|
+
overflow-y: auto;
|
|
1300
|
+
max-height: calc(100vh - var(--nav-height) - 8rem);
|
|
1301
|
+
scrollbar-width: thin;
|
|
1302
|
+
scrollbar-color: var(--color-border) transparent;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.content-nav__item {
|
|
1306
|
+
margin: 0;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
.content-nav__item--h1 {
|
|
1310
|
+
/* top level — no indent */
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
.content-nav__item--h2 {
|
|
1314
|
+
padding-left: var(--space-md);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
.content-nav__item--h3 {
|
|
1318
|
+
padding-left: var(--space-xl);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
.content-nav__link {
|
|
1322
|
+
display: flex;
|
|
1323
|
+
align-items: center;
|
|
1324
|
+
gap: var(--space-xs);
|
|
1325
|
+
padding: 3px var(--space-sm);
|
|
1326
|
+
font-size: 0.8rem;
|
|
1327
|
+
line-height: 1.4;
|
|
1328
|
+
color: var(--color-text-muted);
|
|
1329
|
+
text-decoration: none;
|
|
1330
|
+
border-radius: var(--radius);
|
|
1331
|
+
border-left: 2px solid transparent;
|
|
1332
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
1333
|
+
word-break: break-word;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
.content-nav__link:hover {
|
|
1337
|
+
color: var(--color-text);
|
|
1338
|
+
background: var(--color-bg);
|
|
1339
|
+
text-decoration: none;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
.content-nav__link--active {
|
|
1343
|
+
color: var(--color-primary);
|
|
1344
|
+
border-left-color: var(--color-primary);
|
|
1345
|
+
font-weight: 600;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.content-nav__link-text {
|
|
1349
|
+
flex: 1;
|
|
1350
|
+
min-width: 0;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
.content-nav__badge {
|
|
1354
|
+
flex-shrink: 0;
|
|
1355
|
+
font-size: 0.65rem;
|
|
1356
|
+
font-weight: 700;
|
|
1357
|
+
min-width: 16px;
|
|
1358
|
+
height: 16px;
|
|
1359
|
+
display: inline-flex;
|
|
1360
|
+
align-items: center;
|
|
1361
|
+
justify-content: center;
|
|
1362
|
+
border-radius: 8px;
|
|
1363
|
+
color: white;
|
|
1364
|
+
line-height: 1;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
.content-nav__badge--pending {
|
|
1368
|
+
background: var(--color-status-considering);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
.content-nav__badge--todo {
|
|
1372
|
+
background: var(--color-status-developing);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/* Toggle button when sidebar is hidden — shown outside the sidebar */
|
|
1376
|
+
.content-nav-show-btn {
|
|
1377
|
+
position: sticky;
|
|
1378
|
+
top: calc(var(--nav-height) + var(--space-lg));
|
|
1379
|
+
align-self: flex-start;
|
|
1380
|
+
flex-shrink: 0;
|
|
1381
|
+
background: var(--color-surface);
|
|
1382
|
+
border: 1px solid var(--color-border);
|
|
1383
|
+
border-radius: var(--radius);
|
|
1384
|
+
padding: var(--space-xs) var(--space-sm);
|
|
1385
|
+
cursor: pointer;
|
|
1386
|
+
color: var(--color-text-muted);
|
|
1387
|
+
font-size: var(--text-sm);
|
|
1388
|
+
box-shadow: var(--shadow);
|
|
1389
|
+
transition: color 0.15s, box-shadow 0.15s;
|
|
1390
|
+
margin-right: var(--space-sm);
|
|
1391
|
+
display: none;
|
|
1392
|
+
white-space: nowrap;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
.content-nav-show-btn:hover {
|
|
1396
|
+
color: var(--color-text);
|
|
1397
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
.content-nav-show-btn kbd {
|
|
1401
|
+
font-family: var(--font-mono);
|
|
1402
|
+
font-size: 0.7rem;
|
|
1403
|
+
background: var(--color-bg);
|
|
1404
|
+
border: 1px solid var(--color-border);
|
|
1405
|
+
border-radius: 3px;
|
|
1406
|
+
padding: 0 4px;
|
|
1407
|
+
color: var(--color-text-muted);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
.content-nav--hidden + .content-nav-show-btn {
|
|
1411
|
+
display: block;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/* Add scroll-margin to headings so they clear the sticky nav */
|
|
1415
|
+
.markdown-rendered h1,
|
|
1416
|
+
.markdown-rendered h2,
|
|
1417
|
+
.markdown-rendered h3,
|
|
1418
|
+
.markdown-rendered h4,
|
|
1419
|
+
.markdown-rendered h5,
|
|
1420
|
+
.markdown-rendered h6 {
|
|
1421
|
+
scroll-margin-top: calc(var(--nav-height) + var(--space-lg));
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/* Hide sidebar on small screens */
|
|
1425
|
+
@media (max-width: 1024px) {
|
|
1426
|
+
.content-nav {
|
|
1427
|
+
display: none;
|
|
1428
|
+
}
|
|
1429
|
+
.content-nav-show-btn {
|
|
1430
|
+
display: none !important;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module CoPlan
|
|
2
|
+
class PlanPresenceChannel < ActionCable::Channel::Base
|
|
3
|
+
def subscribed
|
|
4
|
+
@plan = Plan.find_by(id: params[:plan_id])
|
|
5
|
+
policy = @plan && PlanPolicy.new(current_user, @plan)
|
|
6
|
+
unless @plan && policy.show?
|
|
7
|
+
reject
|
|
8
|
+
return
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
PlanViewer.track(plan: @plan, user: current_user)
|
|
12
|
+
broadcast_viewers
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def unsubscribed
|
|
16
|
+
return unless @plan
|
|
17
|
+
|
|
18
|
+
PlanViewer.expire(plan: @plan, user: current_user)
|
|
19
|
+
broadcast_viewers
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ping
|
|
23
|
+
return unless @plan
|
|
24
|
+
|
|
25
|
+
PlanViewer.track(plan: @plan, user: current_user)
|
|
26
|
+
broadcast_viewers
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def current_user
|
|
32
|
+
connection.current_user
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def broadcast_viewers
|
|
36
|
+
viewers = PlanViewer.active_viewers_for(@plan)
|
|
37
|
+
Broadcaster.replace_to(
|
|
38
|
+
@plan,
|
|
39
|
+
target: "plan-viewers",
|
|
40
|
+
partial: "coplan/plans/viewers",
|
|
41
|
+
locals: { viewers: viewers }
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -34,7 +34,7 @@ module CoPlan
|
|
|
34
34
|
}, status: :created
|
|
35
35
|
|
|
36
36
|
rescue ActiveRecord::RecordInvalid => e
|
|
37
|
-
render json: { error: e.message }, status: :
|
|
37
|
+
render json: { error: e.message }, status: :unprocessable_content
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def resolve
|
|
@@ -98,7 +98,7 @@ module CoPlan
|
|
|
98
98
|
}, status: :created
|
|
99
99
|
|
|
100
100
|
rescue ActiveRecord::RecordInvalid => e
|
|
101
|
-
render json: { error: e.message }, status: :
|
|
101
|
+
render json: { error: e.message }, status: :unprocessable_content
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
private
|
|
@@ -10,12 +10,12 @@ module CoPlan
|
|
|
10
10
|
base_revision = params[:base_revision]&.to_i
|
|
11
11
|
|
|
12
12
|
unless base_revision.present?
|
|
13
|
-
render json: { error: "base_revision is required" }, status: :
|
|
13
|
+
render json: { error: "base_revision is required" }, status: :unprocessable_content
|
|
14
14
|
return
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
unless operations.is_a?(Array) && operations.any?
|
|
18
|
-
render json: { error: "operations must be a non-empty array" }, status: :
|
|
18
|
+
render json: { error: "operations must be a non-empty array" }, status: :unprocessable_content
|
|
19
19
|
return
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -27,7 +27,7 @@ module CoPlan
|
|
|
27
27
|
apply_direct(operations, base_revision)
|
|
28
28
|
end
|
|
29
29
|
rescue Plans::OperationError => e
|
|
30
|
-
render json: { error: e.message }, status: :
|
|
30
|
+
render json: { error: e.message }, status: :unprocessable_content
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
private
|
|
@@ -32,7 +32,7 @@ module CoPlan
|
|
|
32
32
|
current_revision: plan.current_revision
|
|
33
33
|
), status: :created
|
|
34
34
|
rescue ActiveRecord::RecordInvalid => e
|
|
35
|
-
render json: { error: e.message }, status: :
|
|
35
|
+
render json: { error: e.message }, status: :unprocessable_content
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def update
|
|
@@ -61,7 +61,7 @@ module CoPlan
|
|
|
61
61
|
current_revision: @plan.current_revision
|
|
62
62
|
)
|
|
63
63
|
rescue ActiveRecord::RecordInvalid => e
|
|
64
|
-
render json: { error: e.record.errors.full_messages.join(", ") }, status: :
|
|
64
|
+
render json: { error: e.record.errors.full_messages.join(", ") }, status: :unprocessable_content
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def versions
|
|
@@ -11,7 +11,7 @@ module CoPlan
|
|
|
11
11
|
def create
|
|
12
12
|
actor_type = params[:actor_type].presence || ApiToken::HOLDER_TYPE
|
|
13
13
|
unless EditSession::ACTOR_TYPES.include?(actor_type)
|
|
14
|
-
render json: { error: "Invalid actor_type" }, status: :
|
|
14
|
+
render json: { error: "Invalid actor_type" }, status: :unprocessable_content
|
|
15
15
|
return
|
|
16
16
|
end
|
|
17
17
|
ttl = actor_type == "cloud_persona" ? EditSession::CLOUD_PERSONA_TTL : EditSession::LOCAL_AGENT_TTL
|
|
@@ -56,7 +56,7 @@ module CoPlan
|
|
|
56
56
|
|
|
57
57
|
render json: response
|
|
58
58
|
rescue Plans::CommitSession::SessionNotOpenError => e
|
|
59
|
-
render json: { error: e.message }, status: :
|
|
59
|
+
render json: { error: e.message }, status: :unprocessable_content
|
|
60
60
|
rescue Plans::CommitSession::StaleSessionError => e
|
|
61
61
|
render json: { error: e.message }, status: :conflict
|
|
62
62
|
rescue Plans::CommitSession::SessionConflictError => e
|
|
@@ -20,7 +20,7 @@ module CoPlan
|
|
|
20
20
|
rescue ActiveRecord::RecordInvalid => e
|
|
21
21
|
@api_tokens = current_user.api_tokens.order(created_at: :desc)
|
|
22
22
|
flash.now[:alert] = e.message
|
|
23
|
-
render :index, status: :
|
|
23
|
+
render :index, status: :unprocessable_content
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def destroy
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module CoPlan
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
include MarkdownHelper
|
|
3
4
|
FAVICON_COLORS = {
|
|
4
5
|
"production" => { start: "#3B82F6", stop: "#1E40AF" },
|
|
5
6
|
"staging" => { start: "#F59E0B", stop: "#D97706" },
|
|
@@ -37,6 +38,18 @@ module CoPlan
|
|
|
37
38
|
tag.link(rel: "icon", type: "image/svg+xml", href: data_uri)
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
def plan_og_description(plan)
|
|
42
|
+
status = plan.status.capitalize
|
|
43
|
+
author = plan.created_by_user.name
|
|
44
|
+
prefix = "#{status} · by #{author}"
|
|
45
|
+
content = plan.current_content
|
|
46
|
+
return prefix if content.blank?
|
|
47
|
+
|
|
48
|
+
plain = markdown_to_plain_text(content)
|
|
49
|
+
truncated = truncate(plain, length: 200, omission: "…")
|
|
50
|
+
"#{prefix} — #{truncated}"
|
|
51
|
+
end
|
|
52
|
+
|
|
40
53
|
def coplan_environment_badge
|
|
41
54
|
return if Rails.env.production?
|
|
42
55
|
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
module CoPlan
|
|
2
2
|
module CommentsHelper
|
|
3
3
|
def comment_author_name(comment)
|
|
4
|
-
case comment.author_type
|
|
4
|
+
user_name = case comment.author_type
|
|
5
5
|
when "human"
|
|
6
6
|
CoPlan::User.find_by(id: comment.author_id)&.name || "Unknown"
|
|
7
7
|
when "local_agent"
|
|
8
|
-
|
|
8
|
+
CoPlan::User
|
|
9
9
|
.joins(:api_tokens)
|
|
10
10
|
.where(coplan_api_tokens: { id: comment.author_id })
|
|
11
11
|
.pick(:name) || "Agent"
|
|
12
|
-
comment.agent_name.present? ? "#{user_name} (#{comment.agent_name})" : user_name
|
|
13
12
|
when "cloud_persona"
|
|
14
13
|
AutomatedPlanReviewer.find_by(id: comment.author_id)&.name || "Reviewer"
|
|
15
14
|
else
|
|
16
15
|
comment.author_type
|
|
17
16
|
end
|
|
17
|
+
|
|
18
|
+
comment.agent_name.present? ? "#{comment.agent_name} (via #{user_name})" : user_name
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
end
|
|
@@ -17,11 +17,16 @@ module CoPlan
|
|
|
17
17
|
ALLOWED_ATTRIBUTES = %w[id class href src alt title].freeze
|
|
18
18
|
|
|
19
19
|
def render_markdown(content)
|
|
20
|
-
html = Commonmarker.to_html(content.to_s.encode("UTF-8"), plugins: { syntax_highlighter: nil })
|
|
20
|
+
html = Commonmarker.to_html(content.to_s.encode("UTF-8"), options: { render: { unsafe: true } }, plugins: { syntax_highlighter: nil })
|
|
21
21
|
sanitized = sanitize(html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES)
|
|
22
22
|
tag.div(sanitized, class: "markdown-rendered")
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def markdown_to_plain_text(content)
|
|
26
|
+
html = Commonmarker.to_html(content.to_s.encode("UTF-8"), plugins: { syntax_highlighter: nil })
|
|
27
|
+
Nokogiri::HTML::DocumentFragment.parse(html).text.squish
|
|
28
|
+
end
|
|
29
|
+
|
|
25
30
|
def render_line_view(content)
|
|
26
31
|
lines = content.to_s.split("\n", -1)
|
|
27
32
|
line_divs = lines.each_with_index.map do |line, index|
|