coplan-engine 0.2.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 +350 -7
- 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 +41 -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 +57 -0
- data/app/helpers/coplan/comments_helper.rb +4 -3
- data/app/helpers/coplan/markdown_helper.rb +7 -1
- data/app/javascript/controllers/coplan/comment_nav_controller.js +126 -13
- 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 +203 -56
- 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/apply_operations.rb +43 -0
- data/app/services/coplan/plans/commit_session.rb +26 -1
- data/app/services/coplan/plans/markdown_text_extractor.rb +142 -0
- data/app/services/coplan/plans/position_resolver.rb +111 -0
- data/app/views/coplan/comment_threads/_reply_form.html.erb +1 -1
- data/app/views/coplan/comment_threads/_thread_popover.html.erb +4 -4
- 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/app/views/layouts/coplan/application.html.erb +2 -0
- 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;
|
|
@@ -119,6 +121,17 @@ img, svg {
|
|
|
119
121
|
border-radius: 6px;
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
.env-badge {
|
|
125
|
+
font-size: 10px;
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
color: #fff;
|
|
128
|
+
padding: 1px 6px;
|
|
129
|
+
border-radius: 4px;
|
|
130
|
+
text-transform: uppercase;
|
|
131
|
+
letter-spacing: 0.5px;
|
|
132
|
+
line-height: 1.4;
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
.site-nav__brand:hover {
|
|
123
136
|
text-decoration: none;
|
|
124
137
|
color: var(--color-primary);
|
|
@@ -173,6 +186,87 @@ img, svg {
|
|
|
173
186
|
font-weight: 700;
|
|
174
187
|
}
|
|
175
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
|
+
|
|
176
270
|
/* Flash messages */
|
|
177
271
|
.flash {
|
|
178
272
|
padding: var(--space-md) var(--space-lg);
|
|
@@ -268,6 +362,11 @@ img, svg {
|
|
|
268
362
|
.badge--developing { background: #dbeafe; color: var(--color-status-developing); }
|
|
269
363
|
.badge--live { background: #d1fae5; color: var(--color-status-live); }
|
|
270
364
|
.badge--abandoned { background: #f3f4f6; color: var(--color-status-abandoned); }
|
|
365
|
+
.badge--pending { background: #fef3c7; color: var(--color-status-considering); }
|
|
366
|
+
.badge--todo { background: #dbeafe; color: var(--color-status-developing); }
|
|
367
|
+
.badge--discarded { background: #f3f4f6; color: var(--color-status-abandoned); }
|
|
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; }
|
|
271
370
|
|
|
272
371
|
/* Forms */
|
|
273
372
|
.form-group {
|
|
@@ -619,6 +718,24 @@ img, svg {
|
|
|
619
718
|
background: rgba(108, 140, 255, 0.22);
|
|
620
719
|
}
|
|
621
720
|
|
|
721
|
+
.anchor-highlight--pending {
|
|
722
|
+
background: rgba(245, 158, 11, 0.12);
|
|
723
|
+
border-bottom: 2px solid rgba(245, 158, 11, 0.6);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.anchor-highlight--pending:hover {
|
|
727
|
+
background: rgba(245, 158, 11, 0.22);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.anchor-highlight--todo {
|
|
731
|
+
background: rgba(59, 130, 246, 0.12);
|
|
732
|
+
border-bottom: 2px solid rgba(59, 130, 246, 0.6);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.anchor-highlight--todo:hover {
|
|
736
|
+
background: rgba(59, 130, 246, 0.22);
|
|
737
|
+
}
|
|
738
|
+
|
|
622
739
|
.anchor-highlight--resolved {
|
|
623
740
|
background: none;
|
|
624
741
|
border-bottom: none;
|
|
@@ -689,6 +806,18 @@ img, svg {
|
|
|
689
806
|
min-width: 0;
|
|
690
807
|
}
|
|
691
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
|
+
|
|
692
821
|
/* Comment form */
|
|
693
822
|
.comment-form {
|
|
694
823
|
position: absolute;
|
|
@@ -1004,24 +1133,47 @@ img, svg {
|
|
|
1004
1133
|
/* Margin dots */
|
|
1005
1134
|
.margin-dot {
|
|
1006
1135
|
position: absolute;
|
|
1007
|
-
width:
|
|
1008
|
-
height:
|
|
1136
|
+
width: 10px;
|
|
1137
|
+
height: 10px;
|
|
1009
1138
|
border-radius: 50%;
|
|
1139
|
+
border: 2px solid transparent;
|
|
1140
|
+
background-clip: padding-box;
|
|
1010
1141
|
cursor: pointer;
|
|
1011
1142
|
left: 14px;
|
|
1012
|
-
transition: transform 0.15s, box-shadow 0.15s;
|
|
1143
|
+
transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
|
|
1013
1144
|
display: flex;
|
|
1014
1145
|
align-items: center;
|
|
1015
1146
|
justify-content: center;
|
|
1147
|
+
padding: 0;
|
|
1148
|
+
outline: none;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
.margin-dot::after {
|
|
1152
|
+
content: "";
|
|
1153
|
+
position: absolute;
|
|
1154
|
+
inset: -8px;
|
|
1155
|
+
border-radius: 50%;
|
|
1016
1156
|
}
|
|
1017
1157
|
|
|
1018
1158
|
.margin-dot:hover {
|
|
1019
|
-
transform: scale(1.
|
|
1159
|
+
transform: scale(1.4);
|
|
1020
1160
|
}
|
|
1021
1161
|
|
|
1022
|
-
.margin-dot
|
|
1023
|
-
|
|
1024
|
-
|
|
1162
|
+
.margin-dot:focus-visible {
|
|
1163
|
+
outline: 2px solid var(--color-primary);
|
|
1164
|
+
outline-offset: 2px;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.margin-dot--pending {
|
|
1168
|
+
background: var(--color-status-considering);
|
|
1169
|
+
border-color: rgba(245, 158, 11, 0.3);
|
|
1170
|
+
box-shadow: 0 0 6px rgba(245, 158, 11, 0.25);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
.margin-dot--todo {
|
|
1174
|
+
background: var(--color-status-developing);
|
|
1175
|
+
border-color: rgba(59, 130, 246, 0.3);
|
|
1176
|
+
box-shadow: 0 0 6px rgba(59, 130, 246, 0.25);
|
|
1025
1177
|
}
|
|
1026
1178
|
|
|
1027
1179
|
.margin-dot--resolved {
|
|
@@ -1087,3 +1239,194 @@ body:has(.comment-toolbar) .main-content {
|
|
|
1087
1239
|
.comment-toolbar__toggle input[type="checkbox"] {
|
|
1088
1240
|
cursor: pointer;
|
|
1089
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
|
|
@@ -281,6 +281,44 @@ module CoPlan
|
|
|
281
281
|
return
|
|
282
282
|
end
|
|
283
283
|
end
|
|
284
|
+
when "replace_section"
|
|
285
|
+
return unless op["heading"]
|
|
286
|
+
include_heading = op.fetch("include_heading", true)
|
|
287
|
+
include_heading = include_heading != false && include_heading != "false"
|
|
288
|
+
|
|
289
|
+
transformed_ranges.each do |tr|
|
|
290
|
+
if include_heading
|
|
291
|
+
# Verify the heading is the first line of the section range
|
|
292
|
+
first_line_end = content.index("\n", tr[0]) || tr[1]
|
|
293
|
+
first_line = content[tr[0]...[first_line_end, tr[1]].min]
|
|
294
|
+
unless first_line&.rstrip == op["heading"]&.rstrip
|
|
295
|
+
render json: {
|
|
296
|
+
error: "Conflict: section at target position has changed",
|
|
297
|
+
current_revision: @plan.current_revision,
|
|
298
|
+
expected_heading: op["heading"],
|
|
299
|
+
found: content[tr[0]...tr[1]]&.slice(0, 200)
|
|
300
|
+
}, status: :conflict
|
|
301
|
+
return
|
|
302
|
+
end
|
|
303
|
+
else
|
|
304
|
+
# Body-only: verify the heading appears on the line before tr[0].
|
|
305
|
+
# Walk backwards past any blank lines to find the heading text.
|
|
306
|
+
search_pos = tr[0]
|
|
307
|
+
search_pos -= 1 while search_pos > 0 && content[search_pos - 1] == "\n"
|
|
308
|
+
heading_line_end = search_pos
|
|
309
|
+
heading_line_start = search_pos > 0 ? (content.rindex("\n", search_pos - 1) || -1) + 1 : 0
|
|
310
|
+
heading_text = content[heading_line_start...heading_line_end]
|
|
311
|
+
unless heading_text == op["heading"]
|
|
312
|
+
render json: {
|
|
313
|
+
error: "Conflict: section heading before target position has changed",
|
|
314
|
+
current_revision: @plan.current_revision,
|
|
315
|
+
expected_heading: op["heading"],
|
|
316
|
+
found: heading_text
|
|
317
|
+
}, status: :conflict
|
|
318
|
+
return
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
284
322
|
end
|
|
285
323
|
end
|
|
286
324
|
|
|
@@ -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,4 +1,61 @@
|
|
|
1
1
|
module CoPlan
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
include MarkdownHelper
|
|
4
|
+
FAVICON_COLORS = {
|
|
5
|
+
"production" => { start: "#3B82F6", stop: "#1E40AF" },
|
|
6
|
+
"staging" => { start: "#F59E0B", stop: "#D97706" },
|
|
7
|
+
"development" => { start: "#10B981", stop: "#047857" },
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
def coplan_favicon_tag
|
|
11
|
+
colors = FAVICON_COLORS.fetch(Rails.env, FAVICON_COLORS["development"])
|
|
12
|
+
svg = <<~SVG
|
|
13
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
14
|
+
<defs>
|
|
15
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
16
|
+
<stop offset="0%" stop-color="#{colors[:start]}"/>
|
|
17
|
+
<stop offset="100%" stop-color="#{colors[:stop]}"/>
|
|
18
|
+
</linearGradient>
|
|
19
|
+
</defs>
|
|
20
|
+
<rect width="100" height="100" rx="22" fill="url(#g)"/>
|
|
21
|
+
<g opacity=".25" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round">
|
|
22
|
+
<line x1="16" y1="18" x2="84" y2="18"/>
|
|
23
|
+
<line x1="16" y1="28" x2="84" y2="28"/>
|
|
24
|
+
<line x1="16" y1="38" x2="84" y2="38"/>
|
|
25
|
+
<line x1="16" y1="48" x2="84" y2="48"/>
|
|
26
|
+
<line x1="16" y1="58" x2="84" y2="58"/>
|
|
27
|
+
<line x1="16" y1="68" x2="84" y2="68"/>
|
|
28
|
+
<line x1="16" y1="78" x2="84" y2="78"/>
|
|
29
|
+
</g>
|
|
30
|
+
<circle cx="40" cy="46" r="22" fill="#fff" opacity=".55"/>
|
|
31
|
+
<path d="M26,62 L18,78 L36,66 Z" fill="#fff" opacity=".55"/>
|
|
32
|
+
<circle cx="62" cy="54" r="22" fill="#fff" opacity=".55"/>
|
|
33
|
+
<path d="M76,70 L84,84 L68,72 Z" fill="#fff" opacity=".55"/>
|
|
34
|
+
</svg>
|
|
35
|
+
SVG
|
|
36
|
+
|
|
37
|
+
data_uri = "data:image/svg+xml,#{ERB::Util.url_encode(svg.strip)}"
|
|
38
|
+
tag.link(rel: "icon", type: "image/svg+xml", href: data_uri)
|
|
39
|
+
end
|
|
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
|
+
|
|
53
|
+
def coplan_environment_badge
|
|
54
|
+
return if Rails.env.production?
|
|
55
|
+
|
|
56
|
+
label = Rails.env.capitalize
|
|
57
|
+
colors = FAVICON_COLORS.fetch(Rails.env, FAVICON_COLORS["development"])
|
|
58
|
+
tag.span(label, class: "env-badge", style: "background: #{colors[:start]};")
|
|
59
|
+
end
|
|
3
60
|
end
|
|
4
61
|
end
|