collavre 0.5.0 → 0.7.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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/comment_versions.css +76 -0
  3. data/app/assets/stylesheets/collavre/comments_popup.css +347 -37
  4. data/app/assets/stylesheets/collavre/creatives.css +73 -1
  5. data/app/assets/stylesheets/collavre/org_chart.css +319 -0
  6. data/app/assets/stylesheets/collavre/popup.css +68 -1
  7. data/app/controllers/collavre/application_controller.rb +13 -0
  8. data/app/controllers/collavre/comments/versions_controller.rb +82 -0
  9. data/app/controllers/collavre/comments_controller.rb +14 -153
  10. data/app/controllers/collavre/concerns/exportable.rb +30 -0
  11. data/app/controllers/collavre/concerns/shareable.rb +28 -0
  12. data/app/controllers/collavre/concerns/slide_viewable.rb +37 -0
  13. data/app/controllers/collavre/concerns/tree_manageable.rb +141 -0
  14. data/app/controllers/collavre/creative_imports_controller.rb +6 -0
  15. data/app/controllers/collavre/creative_invitations_controller.rb +46 -0
  16. data/app/controllers/collavre/creative_plans_controller.rb +1 -1
  17. data/app/controllers/collavre/creative_shares_controller.rb +84 -14
  18. data/app/controllers/collavre/creatives_controller.rb +70 -194
  19. data/app/controllers/collavre/google_auth_controller.rb +3 -0
  20. data/app/controllers/collavre/invites_controller.rb +2 -1
  21. data/app/controllers/collavre/sessions_controller.rb +3 -0
  22. data/app/controllers/collavre/topics_controller.rb +39 -2
  23. data/app/controllers/collavre/users_controller.rb +5 -404
  24. data/app/controllers/concerns/collavre/comments/approval_actions.rb +108 -0
  25. data/app/controllers/concerns/collavre/comments/batch_operations.rb +55 -0
  26. data/app/controllers/concerns/collavre/comments/conversion.rb +46 -0
  27. data/app/controllers/concerns/collavre/users_controller/admin_operations.rb +74 -0
  28. data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +119 -0
  29. data/app/controllers/concerns/collavre/users_controller/contact_management.rb +166 -0
  30. data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +102 -0
  31. data/app/controllers/concerns/collavre/users_controller/registration.rb +63 -0
  32. data/app/helpers/collavre/application_helper.rb +1 -0
  33. data/app/helpers/collavre/creatives_helper.rb +12 -9
  34. data/app/helpers/collavre/navigation_helper.rb +1 -1
  35. data/app/javascript/collavre.js +0 -1
  36. data/app/javascript/controllers/comment_controller.js +33 -70
  37. data/app/javascript/controllers/comment_version_controller.js +164 -0
  38. data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
  39. data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
  40. data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
  41. data/app/javascript/controllers/comments/contexts_controller.js +363 -0
  42. data/app/javascript/controllers/comments/form_controller.js +304 -13
  43. data/app/javascript/controllers/comments/list_controller.js +151 -62
  44. data/app/javascript/controllers/comments/popup_controller.js +66 -38
  45. data/app/javascript/controllers/comments/presence_controller.js +2 -10
  46. data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
  47. data/app/javascript/controllers/comments/topics_controller.js +34 -10
  48. data/app/javascript/controllers/index.js +15 -1
  49. data/app/javascript/controllers/org_chart_controller.js +46 -0
  50. data/app/javascript/controllers/share_modal_controller.js +369 -0
  51. data/app/javascript/controllers/topic_search_controller.js +103 -0
  52. data/app/javascript/creatives/drag_drop/event_handlers.js +42 -1
  53. data/app/javascript/lib/api/creatives.js +12 -0
  54. data/app/javascript/lib/api/csrf_fetch.js +35 -0
  55. data/app/javascript/lib/api/drag_drop.js +17 -0
  56. data/app/javascript/modules/command_menu.js +40 -0
  57. data/app/javascript/modules/creative_row_editor.js +88 -0
  58. data/app/javascript/modules/slide_view.js +2 -1
  59. data/app/jobs/collavre/ai_agent_job.rb +42 -30
  60. data/app/jobs/collavre/compress_job.rb +92 -0
  61. data/app/models/collavre/comment.rb +36 -1
  62. data/app/models/collavre/comment_version.rb +15 -0
  63. data/app/models/collavre/creative/describable.rb +1 -1
  64. data/app/models/collavre/creative.rb +51 -0
  65. data/app/models/collavre/task.rb +30 -2
  66. data/app/models/collavre/user.rb +20 -3
  67. data/app/services/collavre/ai_agent/a2a_dispatcher.rb +68 -0
  68. data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +89 -0
  69. data/app/services/collavre/ai_agent/message_builder.rb +85 -6
  70. data/app/services/collavre/ai_agent/response_finalizer.rb +97 -0
  71. data/app/services/collavre/ai_agent/response_streamer.rb +56 -0
  72. data/app/services/collavre/ai_agent/review_handler.rb +18 -1
  73. data/app/services/collavre/ai_agent_service.rb +130 -183
  74. data/app/services/collavre/ai_client.rb +6 -0
  75. data/app/services/collavre/auto_theme_generator.rb +1 -1
  76. data/app/services/collavre/command_menu_service.rb +19 -0
  77. data/app/services/collavre/comments/command_processor.rb +3 -1
  78. data/app/services/collavre/comments/compress_command.rb +75 -0
  79. data/app/services/collavre/comments/concerns/workflow_support.rb +115 -0
  80. data/app/services/collavre/comments/work_command.rb +161 -0
  81. data/app/services/collavre/comments/workflow_executor.rb +276 -0
  82. data/app/services/collavre/creatives/plan_tagger.rb +14 -3
  83. data/app/services/collavre/creatives/tree_formatter.rb +53 -13
  84. data/app/services/collavre/gemini_parent_recommender.rb +4 -4
  85. data/app/services/collavre/orchestration/agent_context_builder.rb +1 -3
  86. data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
  87. data/app/services/collavre/orchestration/policy_resolver.rb +0 -19
  88. data/app/services/collavre/orchestration/scheduler.rb +3 -2
  89. data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
  90. data/app/services/collavre/system_events/dispatcher.rb +9 -0
  91. data/app/services/collavre/tools/creative_create_service.rb +1 -8
  92. data/app/services/collavre/tools/creative_import_service.rb +46 -0
  93. data/app/services/collavre/tools/creative_retrieval_service.rb +157 -96
  94. data/app/services/collavre/tools/creative_update_service.rb +1 -8
  95. data/app/services/collavre/tools/cron_list_service.rb +1 -1
  96. data/app/services/collavre/tools/description_normalizable.rb +16 -0
  97. data/app/views/collavre/comments/_comment.html.erb +25 -8
  98. data/app/views/collavre/comments/_comments_popup.html.erb +32 -5
  99. data/app/views/collavre/creatives/_inline_edit_form.html.erb +13 -0
  100. data/app/views/collavre/creatives/_share_button.html.erb +4 -1
  101. data/app/views/collavre/creatives/_share_modal.html.erb +31 -1
  102. data/app/views/collavre/creatives/index.html.erb +5 -5
  103. data/app/views/collavre/creatives/slide_view.html.erb +1 -1
  104. data/app/views/collavre/users/{_contact_management.html.erb → _contact_list.html.erb} +4 -8
  105. data/app/views/collavre/users/_org_chart.html.erb +68 -0
  106. data/app/views/collavre/users/_org_chart_node.html.erb +169 -0
  107. data/app/views/collavre/users/new_ai.html.erb +9 -0
  108. data/app/views/collavre/users/show.html.erb +32 -8
  109. data/config/locales/comments.en.yml +57 -2
  110. data/config/locales/comments.ko.yml +57 -2
  111. data/config/locales/contacts.en.yml +31 -0
  112. data/config/locales/contacts.ko.yml +31 -0
  113. data/config/locales/contexts.en.yml +8 -0
  114. data/config/locales/contexts.ko.yml +8 -0
  115. data/config/locales/creatives.en.yml +6 -0
  116. data/config/locales/creatives.ko.yml +6 -0
  117. data/config/locales/users.en.yml +1 -0
  118. data/config/locales/users.ko.yml +1 -0
  119. data/config/routes.rb +14 -1
  120. data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
  121. data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
  122. data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
  123. data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
  124. data/lib/collavre/version.rb +1 -1
  125. metadata +47 -10
  126. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +0 -91
  127. data/app/javascript/lib/lexical/action_text_attachment_node.js +0 -459
  128. data/app/javascript/lib/lexical/dom_attachment_utils.js +0 -66
  129. data/app/javascript/modules/share_modal.js +0 -76
  130. data/app/javascript/modules/share_user_popup.js +0 -77
  131. data/app/services/collavre/orchestration/self_reflection_evaluator.rb +0 -231
  132. data/app/views/collavre/comments/_presence_avatars.html.erb +0 -8
  133. data/app/views/collavre/creatives/_delete_button.html.erb +0 -12
@@ -1,459 +0,0 @@
1
- import React, {useCallback, useEffect, useMemo, useRef} from "react"
2
- import {DecoratorNode} from "lexical"
3
- import {useLexicalComposerContext} from "@lexical/react/LexicalComposerContext"
4
- import {useLexicalNodeSelection} from "@lexical/react/useLexicalNodeSelection"
5
- import {mergeRegister} from "@lexical/utils"
6
- import {
7
- $getNodeByKey,
8
- COMMAND_PRIORITY_LOW,
9
- KEY_BACKSPACE_COMMAND,
10
- KEY_DELETE_COMMAND
11
- } from "lexical"
12
-
13
- import {
14
- sanitizeAttachmentPayload,
15
- attachmentPayloadFromAttachmentElement,
16
- attachmentPayloadFromFigure,
17
- attachmentPayloadToHTMLElement,
18
- ensureSgid,
19
- formatFileSize
20
- } from "./attachment_payload"
21
-
22
- const TYPE = "action-text-attachment"
23
-
24
- const STATUS_READY = "ready"
25
- const STATUS_UPLOADING = "uploading"
26
- const STATUS_ERROR = "error"
27
-
28
- export class ActionTextAttachmentNode extends DecoratorNode {
29
- constructor(payload, key) {
30
- super(key)
31
- this.__payload = sanitizeAttachmentPayload(payload)
32
- }
33
-
34
- static getType() {
35
- return TYPE
36
- }
37
-
38
- static clone(node) {
39
- return new ActionTextAttachmentNode(node.__payload, node.__key)
40
- }
41
-
42
- static importJSON(serializedNode) {
43
- return new ActionTextAttachmentNode(serializedNode.payload ?? {}, serializedNode.key)
44
- }
45
-
46
- exportJSON() {
47
- return {
48
- type: TYPE,
49
- version: 1,
50
- payload: this.__payload
51
- }
52
- }
53
-
54
- static importDOM() {
55
- return {
56
- "action-text-attachment": (domNode) => ({
57
- conversion: (node) => {
58
- const payload = attachmentPayloadFromAttachmentElement(node)
59
- return payload ? {node: new ActionTextAttachmentNode(payload)} : null
60
- },
61
- priority: 1
62
- }),
63
- figure: (domNode) => ({
64
- conversion: (node) => {
65
- const payload = attachmentPayloadFromFigure(node)
66
- return payload ? {node: new ActionTextAttachmentNode(payload)} : null
67
- },
68
- priority: 0
69
- })
70
- }
71
- }
72
-
73
- exportDOM() {
74
- const {element} = attachmentPayloadToHTMLElement(this.__payload)
75
- return {element}
76
- }
77
-
78
- createDOM() {
79
- const container = document.createElement("div")
80
- container.className = "lexical-attachment-block"
81
- return container
82
- }
83
-
84
- updateDOM() {
85
- return false
86
- }
87
-
88
- getPayload() {
89
- return this.__payload
90
- }
91
-
92
- setPayload(updates) {
93
- const writable = this.getWritable()
94
- const merged = {
95
- ...writable.__payload,
96
- ...updates
97
- }
98
- if (updates.previewable === undefined) {
99
- merged.previewable = writable.__payload.previewable
100
- }
101
- if (updates.caption === undefined) {
102
- merged.caption = writable.__payload.caption
103
- }
104
- writable.__payload = sanitizeAttachmentPayload(merged)
105
- }
106
-
107
- setCaption(caption) {
108
- this.setPayload({caption})
109
- }
110
-
111
- setDimensions(width, height) {
112
- this.setPayload({width, height})
113
- }
114
-
115
- setProgress(progress) {
116
- this.setPayload({progress: Math.max(0, Math.min(100, progress || 0))})
117
- }
118
-
119
- setLocalPreview(localUrl) {
120
- this.setPayload({localUrl})
121
- }
122
-
123
- markUploading(updates = {}) {
124
- this.setPayload({
125
- ...updates,
126
- status: STATUS_UPLOADING,
127
- progress: 0,
128
- previewable:
129
- updates.previewable !== undefined
130
- ? !!updates.previewable
131
- : this.__payload.previewable
132
- })
133
- }
134
-
135
- applyUploadResult(result) {
136
- this.setPayload({
137
- ...result,
138
- status: STATUS_READY,
139
- progress: 100
140
- })
141
- }
142
-
143
- markUploadError(message) {
144
- this.setPayload({
145
- status: STATUS_ERROR,
146
- progress: 0,
147
- error: message || "Upload failed"
148
- })
149
- }
150
-
151
- markUploaded() {
152
- this.setPayload({status: STATUS_READY, progress: 100})
153
- }
154
-
155
- clearLocalUrl() {
156
- this.setPayload({localUrl: null})
157
- }
158
-
159
- getTextContent() {
160
- return " "
161
- }
162
-
163
- isInline() {
164
- return false
165
- }
166
-
167
- decorate() {
168
- return React.createElement(AttachmentComponent, {
169
- nodeKey: this.getKey(),
170
- payload: this.__payload
171
- })
172
- }
173
- }
174
-
175
- export function $createActionTextAttachmentNode(payload = {}) {
176
- return new ActionTextAttachmentNode(payload)
177
- }
178
-
179
- export function $isActionTextAttachmentNode(node) {
180
- return node instanceof ActionTextAttachmentNode
181
- }
182
-
183
- function isBlobUrl(url) {
184
- return typeof url === "string" && url.startsWith("blob:")
185
- }
186
-
187
- function AttachmentComponent({payload, nodeKey}) {
188
- const [editor] = useLexicalComposerContext()
189
- const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey)
190
- const imageRef = useRef(null)
191
-
192
- const className = useMemo(() => {
193
- const classes = ["lexical-attachment"]
194
- if (payload.previewable) classes.push("lexical-attachment--image")
195
- if (isSelected) classes.push("is-selected")
196
- if (payload.status === STATUS_UPLOADING) classes.push("is-uploading")
197
- if (payload.status === STATUS_ERROR) classes.push("is-error")
198
- return classes.join(" ")
199
- }, [isSelected, payload.previewable, payload.status])
200
-
201
- const removeNode = useCallback(() => {
202
- editor.update(() => {
203
- const node = $getNodeByKey(nodeKey)
204
- if ($isActionTextAttachmentNode(node)) {
205
- node.remove()
206
- }
207
- })
208
- }, [editor, nodeKey])
209
-
210
- useEffect(() => {
211
- return mergeRegister(
212
- editor.registerCommand(
213
- KEY_DELETE_COMMAND,
214
- (event) => {
215
- if (isSelected) {
216
- event?.preventDefault()
217
- removeNode()
218
- return true
219
- }
220
- return false
221
- },
222
- COMMAND_PRIORITY_LOW
223
- ),
224
- editor.registerCommand(
225
- KEY_BACKSPACE_COMMAND,
226
- (event) => {
227
- if (isSelected) {
228
- event?.preventDefault()
229
- removeNode()
230
- return true
231
- }
232
- return false
233
- },
234
- COMMAND_PRIORITY_LOW
235
- )
236
- )
237
- }, [editor, isSelected, removeNode])
238
-
239
- useEffect(() => () => {
240
- if (isBlobUrl(payload.localUrl)) {
241
- URL.revokeObjectURL(payload.localUrl)
242
- }
243
- }, [payload.localUrl])
244
-
245
- const selectNode = useCallback(
246
- (event) => {
247
- if (event.target.closest("input, textarea, button, a")) return
248
- event.preventDefault()
249
- if (!event.shiftKey) {
250
- clearSelection()
251
- setSelected(true)
252
- } else {
253
- setSelected(!isSelected)
254
- }
255
- },
256
- [clearSelection, isSelected, setSelected]
257
- )
258
-
259
- const handleCaptionChange = useCallback(
260
- (event) => {
261
- const nextCaption = event.target.value
262
- editor.update(() => {
263
- const node = $getNodeByKey(nodeKey)
264
- if ($isActionTextAttachmentNode(node)) {
265
- node.setCaption(nextCaption)
266
- }
267
- })
268
- },
269
- [editor, nodeKey]
270
- )
271
-
272
- const startResize = useCallback(
273
- (event) => {
274
- if (!payload.previewable) return
275
- event.preventDefault()
276
- const img = imageRef.current
277
- if (!img) return
278
- const rect = img.getBoundingClientRect()
279
- const ratio = rect.width && rect.height ? rect.width / rect.height : 1
280
- const startWidth = rect.width
281
- const startX = event.clientX
282
-
283
- const handleMove = (moveEvent) => {
284
- const delta = moveEvent.clientX - startX
285
- const nextWidth = Math.max(80, Math.round(startWidth + delta))
286
- const nextHeight = Math.round(nextWidth / ratio)
287
- editor.update(() => {
288
- const node = $getNodeByKey(nodeKey)
289
- if ($isActionTextAttachmentNode(node)) {
290
- node.setDimensions(nextWidth, nextHeight)
291
- }
292
- })
293
- }
294
-
295
- const handleUp = () => {
296
- document.removeEventListener("pointermove", handleMove)
297
- document.removeEventListener("pointerup", handleUp)
298
- }
299
-
300
- document.addEventListener("pointermove", handleMove)
301
- document.addEventListener("pointerup", handleUp)
302
- },
303
- [editor, nodeKey, payload.previewable]
304
- )
305
-
306
- const children = []
307
-
308
- children.push(
309
- React.createElement(
310
- "button",
311
- {
312
- type: "button",
313
- className: "lexical-attachment__remove",
314
- onMouseDown: (event) => event.preventDefault(),
315
- onClick: (event) => {
316
- event.preventDefault()
317
- removeNode()
318
- },
319
- "aria-label": "Remove attachment"
320
- },
321
- "×"
322
- )
323
- )
324
-
325
- if (payload.status === STATUS_UPLOADING) {
326
- children.push(
327
- React.createElement(
328
- "div",
329
- {className: "lexical-attachment__overlay"},
330
- React.createElement(
331
- "div",
332
- {className: "lexical-attachment__progress"},
333
- `Uploading… ${Math.round(payload.progress || 0)}%`
334
- )
335
- )
336
- )
337
- }
338
-
339
- if (payload.status === STATUS_ERROR) {
340
- children.push(
341
- React.createElement(
342
- "div",
343
- {className: "lexical-attachment__overlay lexical-attachment__overlay--error"},
344
- React.createElement("div", null, "Upload failed. Remove and try again.")
345
- )
346
- )
347
- }
348
-
349
- const figureChildren = []
350
-
351
- if (payload.previewable && (payload.localUrl || payload.url)) {
352
- const resolvedWidth = payload.width ? Math.round(payload.width) : null
353
- figureChildren.push(
354
- React.createElement("img", {
355
- ref: imageRef,
356
- src: payload.localUrl || payload.url,
357
- alt: payload.caption || payload.filename || "",
358
- style: {
359
- maxWidth: "100%",
360
- width: resolvedWidth ? `${resolvedWidth}px` : undefined,
361
- height: resolvedWidth ? "auto" : payload.height ? `${Math.round(payload.height)}px` : undefined
362
- }
363
- })
364
- )
365
- } else {
366
- const infoChildren = []
367
- if (payload.url) {
368
- infoChildren.push(
369
- React.createElement(
370
- "a",
371
- {
372
- href: payload.url,
373
- target: "_blank",
374
- rel: "noopener",
375
- className: "lexical-attachment__file-name",
376
- onClick: (event) => event.stopPropagation()
377
- },
378
- payload.filename || "Attachment"
379
- )
380
- )
381
- } else {
382
- infoChildren.push(
383
- React.createElement(
384
- "div",
385
- {className: "lexical-attachment__file-name"},
386
- payload.filename || "Attachment"
387
- )
388
- )
389
- }
390
- if (Number.isFinite(payload.filesize)) {
391
- infoChildren.push(
392
- React.createElement(
393
- "div",
394
- {className: "lexical-attachment__file-size"},
395
- formatFileSize(payload.filesize)
396
- )
397
- )
398
- }
399
-
400
- figureChildren.push(
401
- React.createElement(
402
- "div",
403
- {className: "lexical-attachment__file"},
404
- React.createElement(
405
- "div",
406
- {className: "lexical-attachment__file-icon", "aria-hidden": "true"},
407
- "📎"
408
- ),
409
- React.createElement(
410
- "div",
411
- {className: "lexical-attachment__file-info"},
412
- ...infoChildren
413
- )
414
- )
415
- )
416
- }
417
-
418
- const captionChildren = [
419
- React.createElement("input", {
420
- type: "text",
421
- className: "lexical-attachment__caption-input",
422
- value: payload.caption || "",
423
- placeholder: payload.previewable ? "Add caption" : "Describe attachment",
424
- onChange: handleCaptionChange,
425
- onClick: (event) => event.stopPropagation(),
426
- onFocus: (event) => event.stopPropagation()
427
- })
428
- ]
429
-
430
- if (payload.previewable && Number.isFinite(payload.filesize)) {
431
- captionChildren.push(
432
- React.createElement(
433
- "span",
434
- {className: "lexical-attachment__caption-size"},
435
- formatFileSize(payload.filesize)
436
- )
437
- )
438
- }
439
-
440
- figureChildren.push(React.createElement("figcaption", null, ...captionChildren))
441
-
442
- children.push(React.createElement("figure", null, ...figureChildren))
443
-
444
- if (payload.previewable) {
445
- children.push(
446
- React.createElement("div", {
447
- className: "lexical-attachment__resize-handle",
448
- role: "presentation",
449
- onPointerDown: startResize
450
- })
451
- )
452
- }
453
-
454
- return React.createElement(
455
- "div",
456
- {className, contentEditable: false, onClick: selectNode},
457
- ...children
458
- )
459
- }
@@ -1,66 +0,0 @@
1
- import {
2
- sanitizeAttachmentPayload,
3
- attachmentPayloadFromAttachmentElement,
4
- attachmentPayloadFromFigure,
5
- attachmentPayloadToHTMLElement
6
- } from "./attachment_payload"
7
-
8
- export function extractAttachmentPayloadFromDOM(element) {
9
- if (!(element instanceof Element)) return null
10
- if (element.tagName === "ACTION-TEXT-ATTACHMENT") {
11
- return attachmentPayloadFromAttachmentElement(element)
12
- }
13
- if (element.tagName === "FIGURE") {
14
- return attachmentPayloadFromFigure(element)
15
- }
16
- return null
17
- }
18
-
19
- export function ensureAttachmentWrapper(element) {
20
- if (!(element instanceof Element)) return null
21
- const payload = attachmentPayloadFromAttachmentElement(element)
22
- if (payload) return {element, payload}
23
-
24
- const figure = element.tagName === "FIGURE" ? element : element.querySelector("figure.attachment")
25
- if (!figure) return null
26
- const figurePayload = attachmentPayloadFromFigure(figure)
27
- if (!figurePayload) return null
28
- const {element: wrapper, payload: sanitized} = attachmentPayloadToHTMLElement(figurePayload)
29
- figure.replaceWith(wrapper)
30
- return {element: wrapper, payload: sanitized}
31
- }
32
-
33
- export function serializeAttachmentPayloadToHTML(payload) {
34
- const {element} = attachmentPayloadToHTMLElement(payload)
35
- return element.outerHTML
36
- }
37
-
38
- export function normalizeAttachmentPair(container) {
39
- if (!(container instanceof Element)) return
40
- const attachments = Array.from(container.querySelectorAll("action-text-attachment"))
41
- const seen = new Set()
42
-
43
- attachments.forEach((attachment) => {
44
- const payload = attachmentPayloadFromAttachmentElement(attachment)
45
- if (!payload) return
46
- const key = `${payload.sgid || payload.filename}-${payload.url || ""}`
47
- if (seen.has(key)) {
48
- attachment.remove()
49
- return
50
- }
51
- seen.add(key)
52
-
53
- const figure = attachment.querySelector("figure.attachment")
54
- if (figure) {
55
- attachmentPayloadToHTMLElement(payload) // ensures figure JSON is in-sync
56
- }
57
- })
58
-
59
- container.querySelectorAll("figure.attachment").forEach((figure) => {
60
- if (figure.closest("action-text-attachment")) return
61
- const payload = attachmentPayloadFromFigure(figure)
62
- if (!payload) return
63
- const {element} = attachmentPayloadToHTMLElement(payload)
64
- figure.replaceWith(element)
65
- })
66
- }
@@ -1,76 +0,0 @@
1
- // Share creative modal functionality
2
- // Handles share button click, invite link generation, and modal interactions
3
-
4
- document.addEventListener('turbo:load', function() {
5
- const shareBtn = document.getElementById('share-creative-btn')
6
- const modal = document.getElementById('share-creative-modal')
7
- const closeBtn = document.getElementById('close-share-modal')
8
- const emailInput = document.getElementById('share-user-email')
9
- const inviteLinkBtn = document.getElementById('creative-invite-link')
10
- const permissionSelect = document.getElementById('share-permission')
11
-
12
- if (inviteLinkBtn) {
13
- inviteLinkBtn.onclick = function() {
14
- const creativeId = inviteLinkBtn.dataset.creativeId
15
- const permission = permissionSelect ? permissionSelect.value : 'read'
16
- const permissionLabel = permissionSelect ? permissionSelect.options[permissionSelect.selectedIndex].text : ''
17
- const noAccessMessage = inviteLinkBtn.dataset.noAccessMessage || 'Cannot create invite link for "No Access" permission.'
18
- const copiedTemplate = inviteLinkBtn.dataset.copiedTemplate || 'Invite link copied with __PERMISSION__ permission!'
19
-
20
- if (permission === 'no_access') {
21
- alert(noAccessMessage)
22
- return
23
- }
24
- fetch('/invite', {
25
- method: 'POST',
26
- headers: {
27
- 'Content-Type': 'application/json',
28
- 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content
29
- },
30
- body: JSON.stringify({ creative_id: creativeId, permission: permission })
31
- }).then(r => r.json()).then(data => {
32
- const plan42Copy = window.Plan42 && window.Plan42.copyTextToClipboard
33
- let copyPromise = null
34
- if (plan42Copy) {
35
- copyPromise = plan42Copy(data.url)
36
- } else if (navigator.clipboard && navigator.clipboard.writeText) {
37
- copyPromise = navigator.clipboard.writeText(data.url)
38
- }
39
- if (copyPromise) {
40
- copyPromise.then(function() {
41
- alert(copiedTemplate.replace('__PERMISSION__', permissionLabel))
42
- })
43
- }
44
- })
45
- }
46
- }
47
-
48
- if (shareBtn && modal && closeBtn) {
49
- shareBtn.onclick = function() {
50
- modal.style.display = 'flex'
51
- document.body.classList.add('no-scroll')
52
- }
53
- closeBtn.onclick = function() {
54
- modal.style.display = 'none'
55
- document.body.classList.remove('no-scroll')
56
- }
57
- modal.onclick = function(e) {
58
- if (e.target === modal) {
59
- modal.style.display = 'none'
60
- document.body.classList.remove('no-scroll')
61
- }
62
- }
63
- const params = new URLSearchParams(window.location.search)
64
- const reqEmail = params.get('share_request')
65
- if (reqEmail) {
66
- shareBtn.click()
67
- if (emailInput) {
68
- emailInput.value = reqEmail
69
- emailInput.dispatchEvent(new Event('blur'))
70
- }
71
- }
72
- if (params.get('open_share') === 'true') {
73
- shareBtn.click()
74
- }
75
- }
76
- })
@@ -1,77 +0,0 @@
1
- import CommonPopup from '../lib/common_popup'
2
-
3
- let sharePopupInitialized = false
4
-
5
- if (!sharePopupInitialized) {
6
- sharePopupInitialized = true
7
-
8
- document.addEventListener('turbo:load', function () {
9
- const input = document.getElementById('share-user-email')
10
- const menu = document.getElementById('share-user-suggestions')
11
- const modal = document.getElementById('share-creative-modal')
12
- const closeBtn = document.getElementById('close-share-modal')
13
-
14
- if (!input || !menu) return
15
-
16
- const list = menu.querySelector('.mention-results') || menu.querySelector('.common-popup-list')
17
- let fetchTimer
18
-
19
- const popupMenu = new CommonPopup(menu, {
20
- listElement: list,
21
- renderItem: (user) => `<div class="mention-item"><img src="${user.avatar_url || ''}" width="20" height="20" class="avatar" /> ${user.name} <span style="opacity:0.7">${user.email || ''}</span></div>`,
22
- onSelect: (user) => {
23
- input.value = user.email || user.name
24
- popupMenu.hide()
25
- input.focus()
26
- input.dispatchEvent(new Event('input', { bubbles: true }))
27
- input.dispatchEvent(new Event('blur', { bubbles: true }))
28
- },
29
- })
30
-
31
- function hide() {
32
- popupMenu.hide()
33
- }
34
-
35
- function show(users) {
36
- if (!users || users.length === 0) {
37
- hide()
38
- return
39
- }
40
- popupMenu.setItems(users)
41
- popupMenu.showAt(input.getBoundingClientRect())
42
- }
43
-
44
- input.addEventListener('keydown', function (event) {
45
- if (popupMenu.handleKey(event)) return
46
- })
47
-
48
- input.addEventListener('input', function () {
49
- const term = input.value.trim()
50
- if (!term) {
51
- hide()
52
- return
53
- }
54
- clearTimeout(fetchTimer)
55
- fetchTimer = setTimeout(function () {
56
- const url = new URL('/users/search', window.location.origin)
57
- url.searchParams.set('q', term)
58
- if (modal?.dataset?.creativeId) {
59
- url.searchParams.set('creative_id', modal.dataset.creativeId)
60
- }
61
- fetch(url, { headers: { Accept: 'application/json' } })
62
- .then(function (r) { return r.ok ? r.json() : [] })
63
- .then(show)
64
- .catch(function () { })
65
- }, 200)
66
- })
67
-
68
- input.addEventListener('blur', function () {
69
- setTimeout(hide, 150)
70
- })
71
-
72
- closeBtn?.addEventListener('click', hide)
73
- modal?.addEventListener('click', function (event) {
74
- if (event.target === modal) hide()
75
- })
76
- })
77
- }