collavre 0.22.0 → 0.23.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/README.md +1 -0
- data/app/assets/stylesheets/collavre/actiontext.css +251 -90
- data/app/assets/stylesheets/collavre/code_highlight.css +7 -201
- data/app/assets/stylesheets/collavre/comments_popup.css +118 -61
- data/app/assets/stylesheets/collavre/creatives.css +11 -2
- data/app/assets/stylesheets/collavre/modal_dialog.css +32 -0
- data/app/assets/stylesheets/collavre/tables.css +91 -0
- data/app/channels/collavre/inbox_badge_channel.rb +30 -0
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +224 -0
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +95 -0
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +31 -0
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +25 -0
- data/app/controllers/collavre/creatives_controller.rb +16 -5
- data/app/controllers/collavre/tasks_controller.rb +13 -4
- data/app/controllers/collavre/topics_controller.rb +49 -1
- data/app/controllers/collavre/typo_corrections_controller.rb +39 -0
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +16 -1
- data/app/controllers/concerns/collavre/users_controller/registration.rb +41 -1
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/javascript/collavre.js +2 -0
- data/app/javascript/components/ImageResizer.jsx +9 -3
- data/app/javascript/components/InlineLexicalEditor.jsx +155 -38
- data/app/javascript/components/creative_tree_row.js +20 -3
- data/app/javascript/components/plugins/list_tab_indent_plugin.jsx +16 -0
- data/app/javascript/components/plugins/table_hover_actions_plugin.jsx +405 -0
- data/app/javascript/controllers/__tests__/inbox_badge_controller.test.js +73 -0
- data/app/javascript/controllers/comment_controller.js +5 -4
- data/app/javascript/controllers/comment_version_controller.js +2 -1
- data/app/javascript/controllers/comments/__tests__/form_controller_double_submit.test.js +159 -0
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +3 -2
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +94 -0
- data/app/javascript/controllers/comments/form_controller.js +21 -5
- data/app/javascript/controllers/comments/list_controller.js +18 -17
- data/app/javascript/controllers/comments/presence_controller.js +2 -1
- data/app/javascript/controllers/comments/topics_controller.js +14 -8
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +150 -0
- data/app/javascript/controllers/creatives/import_controller.js +2 -1
- data/app/javascript/controllers/creatives/select_mode_controller.js +2 -1
- data/app/javascript/controllers/creatives/tree_controller.js +142 -1
- data/app/javascript/controllers/image_lightbox_controller.js +2 -1
- data/app/javascript/controllers/inbox_badge_controller.js +33 -0
- data/app/javascript/controllers/index.js +4 -1
- data/app/javascript/controllers/share_modal_controller.js +4 -3
- data/app/javascript/controllers/topic_search_controller.js +2 -1
- data/app/javascript/creatives/drag_drop/event_handlers.js +14 -5
- data/app/javascript/creatives/topic_move_members_popup.js +156 -0
- data/app/javascript/creatives/tree_renderer.js +11 -0
- data/app/javascript/lib/__tests__/turbo_confirm.test.js +81 -0
- data/app/javascript/lib/__tests__/typo_correction.test.js +192 -0
- data/app/javascript/lib/api/__tests__/api_error.test.js +96 -0
- data/app/javascript/lib/api/__tests__/queue_manager.test.js +88 -1
- data/app/javascript/lib/api/api_error.js +108 -0
- data/app/javascript/lib/api/queue_manager.js +38 -4
- data/app/javascript/lib/common_popup.js +18 -5
- data/app/javascript/lib/editor/__tests__/code_edit_view_token_parity.test.js +121 -0
- data/app/javascript/lib/editor/__tests__/code_language_roundtrip.test.js +152 -0
- data/app/javascript/lib/editor/__tests__/code_languages.test.js +93 -0
- data/app/javascript/lib/editor/code_languages.js +173 -0
- data/app/javascript/lib/editor/code_token_theme.js +41 -0
- data/app/javascript/lib/lexical/__tests__/image_focus.test.js +139 -0
- data/app/javascript/lib/lexical/__tests__/list_tab_indent.test.js +633 -0
- data/app/javascript/lib/lexical/__tests__/markdown_serialize.test.js +627 -0
- data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +20 -1
- data/app/javascript/lib/lexical/__tests__/selection_boundary.test.js +88 -0
- data/app/javascript/lib/lexical/__tests__/table_transformer.test.js +163 -0
- data/app/javascript/lib/lexical/__tests__/trailing_paragraph.test.js +104 -0
- data/app/javascript/lib/lexical/list_tab_indent.js +210 -0
- data/app/javascript/lib/lexical/markdown_serialize.js +320 -0
- data/app/javascript/lib/lexical/selection_boundary.js +58 -0
- data/app/javascript/lib/lexical/table_transformer.js +182 -0
- data/app/javascript/lib/lexical/trailing_paragraph.js +29 -0
- data/app/javascript/lib/turbo_confirm.js +46 -0
- data/app/javascript/lib/typo_correction.js +146 -0
- data/app/javascript/lib/utils/__tests__/confirm_dialog.test.js +88 -0
- data/app/javascript/lib/utils/__tests__/dialog.test.js +92 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +153 -0
- data/app/javascript/lib/utils/__tests__/sanitize_description.test.js +68 -0
- data/app/javascript/lib/utils/__tests__/table_download.test.js +93 -0
- data/app/javascript/lib/utils/confirm_dialog.js +10 -0
- data/app/javascript/lib/utils/dialog.js +300 -0
- data/app/javascript/lib/utils/markdown.js +154 -67
- data/app/javascript/lib/utils/sanitize_description.js +31 -0
- data/app/javascript/lib/utils/table_download.js +15 -0
- data/app/javascript/modules/__tests__/typo_corrector.test.js +365 -0
- data/app/javascript/modules/creative_row_editor.js +110 -70
- data/app/javascript/modules/export_to_markdown.js +2 -1
- data/app/javascript/modules/lexical_inline_editor.jsx +2 -1
- data/app/javascript/modules/slide_view.js +11 -2
- data/app/javascript/modules/typo_corrector.js +534 -0
- data/app/jobs/collavre/ai_agent_job.rb +7 -4
- data/app/jobs/collavre/compress_job.rb +6 -2
- data/app/models/collavre/comment/broadcastable.rb +46 -7
- data/app/models/collavre/comment/notifiable.rb +14 -4
- data/app/models/collavre/comment.rb +79 -31
- data/app/models/collavre/creative/describable.rb +89 -10
- data/app/models/collavre/task.rb +15 -0
- data/app/models/collavre/user.rb +57 -1
- data/app/services/collavre/ai_client.rb +28 -10
- data/app/services/collavre/auto_theme_generator.rb +1 -1
- data/app/services/collavre/creatives/index_query.rb +85 -16
- data/app/services/collavre/creatives/tree_builder.rb +2 -1
- data/app/services/collavre/gemini_parent_recommender.rb +1 -1
- data/app/services/collavre/inbox_reply_service.rb +5 -0
- data/app/services/collavre/markdown_converter.rb +13 -3
- data/app/services/collavre/mobile/event_summarizer.rb +40 -0
- data/app/services/collavre/orchestration/agent_orchestrator.rb +33 -7
- data/app/services/collavre/orchestration/arbiter.rb +16 -0
- data/app/services/collavre/orchestration/matcher.rb +79 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +4 -3
- data/app/services/collavre/orchestration/stuck_detector.rb +141 -34
- data/app/services/collavre/tools/creative_batch_service.rb +3 -2
- data/app/services/collavre/tools/creative_create_service.rb +8 -8
- data/app/services/collavre/tools/creative_update_service.rb +23 -8
- data/app/services/collavre/typo_corrector.rb +188 -0
- data/app/views/collavre/comments/_comment.html.erb +5 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +14 -1
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +1 -0
- data/app/views/collavre/creatives/_topic_move_members_modal.html.erb +42 -0
- data/app/views/collavre/creatives/index.html.erb +14 -1
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/users/show.html.erb +3 -0
- data/app/views/collavre/users/typo_correction.html.erb +50 -0
- data/app/views/layouts/collavre/slide.html.erb +1 -0
- data/config/locales/comments.en.yml +15 -0
- data/config/locales/comments.ko.yml +15 -0
- data/config/locales/integrations.en.yml +1 -1
- data/config/locales/integrations.ko.yml +1 -1
- data/config/locales/mobile.en.yml +16 -0
- data/config/locales/mobile.ko.yml +16 -0
- data/config/locales/orchestration.en.yml +1 -0
- data/config/locales/orchestration.ko.yml +1 -0
- data/config/locales/users.en.yml +15 -0
- data/config/locales/users.ko.yml +15 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20260612000000_add_topic_concurrency_defer_to_comments.rb +38 -0
- data/db/migrate/20260617090000_add_typo_correction_settings_to_users.rb +18 -0
- data/db/seeds.rb +51 -0
- data/lib/collavre/version.rb +1 -1
- data/lib/generators/collavre/install/install_generator.rb +1 -0
- metadata +55 -2
- data/app/services/collavre/tools/description_normalizable.rb +0 -16
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import creativesApi from '../lib/api/creatives'
|
|
2
2
|
import apiQueue from '../lib/api/queue_manager'
|
|
3
|
-
import { $
|
|
3
|
+
import { $getSelection } from 'lexical'
|
|
4
|
+
import { isSelectionAtDocumentStart, isSelectionAtDocumentEnd } from '../lib/lexical/selection_boundary'
|
|
4
5
|
import { createInlineEditor } from './lexical_inline_editor'
|
|
5
6
|
import { renderCreativeTree, dispatchCreativeTreeUpdated } from '../creatives/tree_renderer'
|
|
6
7
|
import { isProgressComplete, progressBaselineValueFrom, progressValueChangedFrom } from './creative_progress'
|
|
7
8
|
import { renderMarkdown } from '../lib/utils/markdown'
|
|
8
9
|
import { reconcileMarkdownSource } from './markdown_source_reconcile'
|
|
9
10
|
import { isHtmlEmpty } from './html_content_empty'
|
|
11
|
+
import { confirmDialog, alertDialog } from '../lib/utils/dialog'
|
|
12
|
+
import { serverErrorMessage } from '../lib/api/api_error'
|
|
10
13
|
import yaml from 'js-yaml'
|
|
11
14
|
// Import Stimulus application from the global window (set by host app)
|
|
12
15
|
const application = window.Stimulus
|
|
@@ -57,11 +60,15 @@ export function initializeCreativeRowEditor() {
|
|
|
57
60
|
// In a real app, use a toast notification. For now, alert is safe.
|
|
58
61
|
// Suppress 404 errors for PATCH requests, as this likely means the item was deleted
|
|
59
62
|
// and we don't need to alert the user about it.
|
|
60
|
-
const is404 = error && error.toString().includes('404');
|
|
63
|
+
const is404 = (error && error.status === 404) || (error && error.toString().includes('404'));
|
|
61
64
|
const isPatch = item && item.method === 'PATCH';
|
|
62
65
|
|
|
63
66
|
if (!(is404 && isPatch)) {
|
|
64
|
-
|
|
67
|
+
// Prefer the server's own error message (e.g. "Description cannot be
|
|
68
|
+
// changed directly for GitHub synced content") and fall back to the
|
|
69
|
+
// generic copy only when the failure carries no usable payload.
|
|
70
|
+
const serverMessage = serverErrorMessage(error);
|
|
71
|
+
alertDialog(serverMessage || 'Failed to save changes. Please check your connection and try again.');
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
// If the failed item matches the current creative, mark it as dirty so it can be retried
|
|
@@ -112,6 +119,7 @@ export function initializeCreativeRowEditor() {
|
|
|
112
119
|
|
|
113
120
|
// Markdown editor elements
|
|
114
121
|
const contentTypeInput = document.getElementById('inline-content-type');
|
|
122
|
+
const markdownEditorInput = document.getElementById('inline-markdown-editor');
|
|
115
123
|
const markdownSourceInput = document.getElementById('inline-markdown-source');
|
|
116
124
|
const markdownWrapper = document.getElementById('markdown-editor-wrapper');
|
|
117
125
|
const markdownTextarea = document.getElementById('markdown-editor-textarea');
|
|
@@ -258,6 +266,9 @@ export function initializeCreativeRowEditor() {
|
|
|
258
266
|
if (Object.prototype.hasOwnProperty.call(data, 'markdown_source')) {
|
|
259
267
|
setRowDatasetValue(row, 'markdownSource', data.markdown_source ?? '');
|
|
260
268
|
}
|
|
269
|
+
if (Object.prototype.hasOwnProperty.call(data, 'markdown_editor')) {
|
|
270
|
+
setRowDatasetValue(row, 'markdownEditor', data.markdown_editor ?? '');
|
|
271
|
+
}
|
|
261
272
|
if (Object.prototype.hasOwnProperty.call(data, 'has_children')) {
|
|
262
273
|
if (data.has_children) {
|
|
263
274
|
row.setAttribute('has-children', '');
|
|
@@ -298,6 +309,7 @@ export function initializeCreativeRowEditor() {
|
|
|
298
309
|
parent_id: parentId,
|
|
299
310
|
progress: Number.isNaN(progressValue) ? 0 : progressValue,
|
|
300
311
|
content_type: row.dataset?.contentType || null,
|
|
312
|
+
markdown_editor: row.dataset?.markdownEditor || null,
|
|
301
313
|
markdown_source: row.dataset?.markdownSource || null
|
|
302
314
|
};
|
|
303
315
|
}
|
|
@@ -309,6 +321,7 @@ export function initializeCreativeRowEditor() {
|
|
|
309
321
|
function activateMarkdownMode(source) {
|
|
310
322
|
markdownMode = true;
|
|
311
323
|
if (contentTypeInput) contentTypeInput.value = 'markdown';
|
|
324
|
+
if (markdownEditorInput) markdownEditorInput.value = 'source';
|
|
312
325
|
if (markdownTextarea) markdownTextarea.value = source || '';
|
|
313
326
|
if (markdownWrapper) markdownWrapper.style.display = '';
|
|
314
327
|
if (editorContainer) editorContainer.style.display = 'none';
|
|
@@ -323,6 +336,7 @@ export function initializeCreativeRowEditor() {
|
|
|
323
336
|
function deactivateMarkdownMode() {
|
|
324
337
|
markdownMode = false;
|
|
325
338
|
if (contentTypeInput) contentTypeInput.value = 'html';
|
|
339
|
+
if (markdownEditorInput) markdownEditorInput.value = '';
|
|
326
340
|
if (markdownSourceInput) markdownSourceInput.value = '';
|
|
327
341
|
if (markdownWrapper) markdownWrapper.style.display = 'none';
|
|
328
342
|
if (editorContainer) editorContainer.style.display = '';
|
|
@@ -349,20 +363,33 @@ export function initializeCreativeRowEditor() {
|
|
|
349
363
|
const content = data.description_raw_html || data.description || '';
|
|
350
364
|
descriptionInput.value = content;
|
|
351
365
|
|
|
352
|
-
//
|
|
366
|
+
// Markdown is the canonical storage format for BOTH editors now. Which
|
|
367
|
+
// surface opens is decided by the persisted editor preference: only
|
|
368
|
+
// explicitly rich-authored Markdown reopens in Lexical; "source" and
|
|
369
|
+
// legacy (no preference) Markdown reopen in the advanced textarea.
|
|
353
370
|
const isMarkdown = data.content_type === 'markdown';
|
|
354
|
-
|
|
371
|
+
const useTextarea = isMarkdown && data.markdown_editor !== 'rich';
|
|
372
|
+
if (useTextarea) {
|
|
355
373
|
activateMarkdownMode(data.markdown_source || '');
|
|
356
374
|
// Also load Lexical with HTML for fallback/switching
|
|
357
375
|
lexicalEditor.load(content, `creative-${creativeId}-${Date.now()}`);
|
|
358
376
|
} else {
|
|
359
377
|
deactivateMarkdownMode();
|
|
378
|
+
if (isMarkdown) {
|
|
379
|
+
// Rich-authored Markdown: prime the hidden fields so a no-edit save
|
|
380
|
+
// (move, progress toggle) preserves Markdown canonical instead of
|
|
381
|
+
// demoting back to HTML before the first Lexical change fires.
|
|
382
|
+
if (contentTypeInput) contentTypeInput.value = 'markdown';
|
|
383
|
+
if (markdownSourceInput) markdownSourceInput.value = data.markdown_source || '';
|
|
384
|
+
if (markdownEditorInput) markdownEditorInput.value = 'rich';
|
|
385
|
+
}
|
|
360
386
|
lexicalEditor.load(content, `creative-${creativeId}-${Date.now()}`);
|
|
361
387
|
}
|
|
362
388
|
|
|
363
389
|
pendingSave = false;
|
|
364
|
-
//
|
|
365
|
-
|
|
390
|
+
// Dirty detection is HTML-based for the rich surface (compares the editor's
|
|
391
|
+
// HTML projection), and Markdown-source-based for the textarea surface.
|
|
392
|
+
originalContent = useTextarea ? (data.markdown_source || '') : content;
|
|
366
393
|
isDirty = false;
|
|
367
394
|
const progressNumber = Number(data.progress ?? 0);
|
|
368
395
|
const normalizedProgress = Number.isNaN(progressNumber) ? 0 : progressNumber;
|
|
@@ -384,7 +411,12 @@ export function initializeCreativeRowEditor() {
|
|
|
384
411
|
const effectiveParent = parentInput.value;
|
|
385
412
|
if (unconvertBtn) unconvertBtn.style.display = effectiveParent ? '' : 'none';
|
|
386
413
|
originalProgress = normalizedProgress;
|
|
387
|
-
|
|
414
|
+
// Focus the Lexical editor whenever it is the active surface. Gating on
|
|
415
|
+
// `!isMarkdown` was correct when `content_type === 'markdown'` always meant
|
|
416
|
+
// the textarea surface, but rich-authored Markdown now reopens in Lexical
|
|
417
|
+
// (markdown_editor === 'rich'), so use `!useTextarea` to also focus it.
|
|
418
|
+
// The textarea surface focuses itself in activateMarkdownMode().
|
|
419
|
+
if (!useTextarea) {
|
|
388
420
|
lexicalEditor.focus();
|
|
389
421
|
}
|
|
390
422
|
updateActionButtonStates();
|
|
@@ -1288,8 +1320,12 @@ export function initializeCreativeRowEditor() {
|
|
|
1288
1320
|
|
|
1289
1321
|
// CRITICAL: Capture ALL values BEFORE awaiting, because the editor may switch
|
|
1290
1322
|
// to a different creative while we're waiting for uploads
|
|
1291
|
-
|
|
1292
|
-
|
|
1323
|
+
// Both editor surfaces persist Markdown now. The textarea surface
|
|
1324
|
+
// (markdownMode) syncs its value to the hidden fields here; the rich
|
|
1325
|
+
// surface already kept them current via onLexicalChange/applyCreativeData.
|
|
1326
|
+
if (markdownMode) syncMarkdownToForm();
|
|
1327
|
+
const capturedContentType = contentTypeInput ? contentTypeInput.value : 'html';
|
|
1328
|
+
const isMarkdownSave = capturedContentType === 'markdown';
|
|
1293
1329
|
let currentContent = descriptionInput.value;
|
|
1294
1330
|
let currentProgress = readProgressValue();
|
|
1295
1331
|
let shouldPersistProgress = progressValueChanged();
|
|
@@ -1297,8 +1333,8 @@ export function initializeCreativeRowEditor() {
|
|
|
1297
1333
|
const currentBeforeId = tree.previousElementSibling ? creativeIdFrom(tree.previousElementSibling) : '';
|
|
1298
1334
|
const currentAfterId = tree.nextElementSibling ? creativeIdFrom(tree.nextElementSibling) : '';
|
|
1299
1335
|
const startCreativeId = creativeId;
|
|
1300
|
-
let capturedMarkdownSource = isMarkdownSave ? (
|
|
1301
|
-
const
|
|
1336
|
+
let capturedMarkdownSource = isMarkdownSave ? (markdownSourceInput ? markdownSourceInput.value : '') : '';
|
|
1337
|
+
const capturedMarkdownEditor = markdownEditorInput ? markdownEditorInput.value : '';
|
|
1302
1338
|
|
|
1303
1339
|
// Prevent saving empty content, matching saveForm behavior
|
|
1304
1340
|
// This avoids overwriting existing descriptions with empty strings during quick navigation
|
|
@@ -1320,9 +1356,12 @@ export function initializeCreativeRowEditor() {
|
|
|
1320
1356
|
// so we must re-sync and re-capture the latest textarea value too — otherwise edits
|
|
1321
1357
|
// made during the upload wait get overwritten by the stale pre-wait source.
|
|
1322
1358
|
if (form.dataset.creativeId === startCreativeId) {
|
|
1323
|
-
if (
|
|
1359
|
+
if (markdownMode) {
|
|
1324
1360
|
syncMarkdownToForm();
|
|
1325
|
-
capturedMarkdownSource =
|
|
1361
|
+
capturedMarkdownSource = markdownSourceInput ? markdownSourceInput.value : '';
|
|
1362
|
+
} else if (isMarkdownSave && markdownSourceInput) {
|
|
1363
|
+
// Rich surface: re-capture any Markdown produced by edits during the wait.
|
|
1364
|
+
capturedMarkdownSource = markdownSourceInput.value;
|
|
1326
1365
|
}
|
|
1327
1366
|
currentContent = descriptionInput.value;
|
|
1328
1367
|
currentProgress = readProgressValue();
|
|
@@ -1338,6 +1377,9 @@ export function initializeCreativeRowEditor() {
|
|
|
1338
1377
|
};
|
|
1339
1378
|
if (isMarkdownSave) {
|
|
1340
1379
|
body['creative[markdown_source]'] = capturedMarkdownSource;
|
|
1380
|
+
if (capturedMarkdownEditor) {
|
|
1381
|
+
body['creative[markdown_editor]'] = capturedMarkdownEditor;
|
|
1382
|
+
}
|
|
1341
1383
|
}
|
|
1342
1384
|
|
|
1343
1385
|
if (shouldPersistProgress) {
|
|
@@ -1368,6 +1410,10 @@ export function initializeCreativeRowEditor() {
|
|
|
1368
1410
|
}
|
|
1369
1411
|
row.dataset.contentType = capturedContentType;
|
|
1370
1412
|
row.dataset.markdownSource = isMarkdownSave ? capturedMarkdownSource : '';
|
|
1413
|
+
// Persist which surface authored this save so a row re-opened from this
|
|
1414
|
+
// cached payload (before any full GET refresh) reopens in the right
|
|
1415
|
+
// editor — without it, rich-authored Markdown falls back to the textarea.
|
|
1416
|
+
row.dataset.markdownEditor = isMarkdownSave ? capturedMarkdownEditor : '';
|
|
1371
1417
|
if (currentParentId) {
|
|
1372
1418
|
tree.dataset.parentId = currentParentId;
|
|
1373
1419
|
row.parentId = currentParentId;
|
|
@@ -1929,10 +1975,18 @@ export function initializeCreativeRowEditor() {
|
|
|
1929
1975
|
saveTimer = setTimeout(saveForm, 5000);
|
|
1930
1976
|
}
|
|
1931
1977
|
|
|
1932
|
-
function onLexicalChange(
|
|
1933
|
-
if (markdownMode) return; // Ignore Lexical changes
|
|
1978
|
+
function onLexicalChange(payload) {
|
|
1979
|
+
if (markdownMode) return; // Ignore Lexical changes when the textarea is active
|
|
1980
|
+
const html = (payload && payload.html) || '';
|
|
1981
|
+
const markdown = (payload && payload.markdown) || '';
|
|
1934
1982
|
descriptionInput.value = html;
|
|
1935
|
-
//
|
|
1983
|
+
// The rich editor is now a Markdown-canonical surface: persist the Markdown
|
|
1984
|
+
// projection (text color/background as normalized <span> fragments) and
|
|
1985
|
+
// record that the rich surface authored it, so it reopens in Lexical.
|
|
1986
|
+
if (contentTypeInput) contentTypeInput.value = 'markdown';
|
|
1987
|
+
if (markdownSourceInput) markdownSourceInput.value = markdown;
|
|
1988
|
+
if (markdownEditorInput) markdownEditorInput.value = 'rich';
|
|
1989
|
+
// Mark as dirty if the HTML projection changed from original
|
|
1936
1990
|
isDirty = (html !== originalContent);
|
|
1937
1991
|
scheduleSave();
|
|
1938
1992
|
}
|
|
@@ -1992,9 +2046,11 @@ export function initializeCreativeRowEditor() {
|
|
|
1992
2046
|
let atEnd = false;
|
|
1993
2047
|
editorInstance.getEditorState().read(() => {
|
|
1994
2048
|
const selection = $getSelection();
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2049
|
+
// atStart must reflect the start of the whole document, not just offset 0
|
|
2050
|
+
// of the current node — otherwise the start of a second paragraph (e.g.
|
|
2051
|
+
// right after pressing Enter) is mistaken for the top and ArrowUp jumps
|
|
2052
|
+
// to the row above instead of moving the cursor up. See isSelectionAtDocumentStart.
|
|
2053
|
+
atStart = isSelectionAtDocumentStart(selection);
|
|
1998
2054
|
atEnd = isSelectionAtDocumentEnd(selection);
|
|
1999
2055
|
});
|
|
2000
2056
|
|
|
@@ -2014,32 +2070,6 @@ export function initializeCreativeRowEditor() {
|
|
|
2014
2070
|
}
|
|
2015
2071
|
}
|
|
2016
2072
|
|
|
2017
|
-
function isSelectionAtDocumentEnd(selection) {
|
|
2018
|
-
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false;
|
|
2019
|
-
|
|
2020
|
-
const focus = selection.focus;
|
|
2021
|
-
let node = focus.getNode();
|
|
2022
|
-
if (!node) return false;
|
|
2023
|
-
|
|
2024
|
-
const offset = focus.offset;
|
|
2025
|
-
if ($isTextNode(node)) {
|
|
2026
|
-
if (offset !== node.getTextContentSize()) return false;
|
|
2027
|
-
} else if (typeof node.getChildrenSize === 'function') {
|
|
2028
|
-
if (offset !== node.getChildrenSize()) return false;
|
|
2029
|
-
} else {
|
|
2030
|
-
// Fallback for nodes without children size (e.g., line breaks)
|
|
2031
|
-
const textSize = node.getTextContentSize?.() ?? 0;
|
|
2032
|
-
if (offset !== textSize) return false;
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
while (node && !$isRootOrShadowRoot(node)) {
|
|
2036
|
-
if (node.getNextSibling()) return false;
|
|
2037
|
-
node = node.getParent();
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
return !!node && $isRootOrShadowRoot(node);
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
2073
|
if (progressInput) {
|
|
2044
2074
|
progressInput.addEventListener('change', function () {
|
|
2045
2075
|
if (progressValue) {
|
|
@@ -2052,7 +2082,7 @@ export function initializeCreativeRowEditor() {
|
|
|
2052
2082
|
completionCascadePending = true;
|
|
2053
2083
|
const alertMessage = progressInput.dataset.childrenAlertMessage;
|
|
2054
2084
|
if (alertMessage) {
|
|
2055
|
-
|
|
2085
|
+
alertDialog(alertMessage);
|
|
2056
2086
|
}
|
|
2057
2087
|
}
|
|
2058
2088
|
updateProgressInputAvailability(readProgressValue());
|
|
@@ -2138,14 +2168,14 @@ export function initializeCreativeRowEditor() {
|
|
|
2138
2168
|
}
|
|
2139
2169
|
|
|
2140
2170
|
if (archiveBtn) {
|
|
2141
|
-
archiveBtn.addEventListener('click', function () {
|
|
2171
|
+
archiveBtn.addEventListener('click', async function () {
|
|
2142
2172
|
const creativeId = form.dataset.creativeId;
|
|
2143
2173
|
if (!creativeId) return;
|
|
2144
2174
|
const row = document.querySelector(`creative-tree-row[creative-id="${creativeId}"]`);
|
|
2145
2175
|
const isArchived = row?.hasAttribute('archived');
|
|
2146
2176
|
const confirmMsg = isArchived ? archiveBtn.dataset.restoreConfirm : archiveBtn.dataset.confirm;
|
|
2147
2177
|
|
|
2148
|
-
if (
|
|
2178
|
+
if (await confirmDialog(confirmMsg)) {
|
|
2149
2179
|
const apiCall = isArchived ? creativesApi.unarchive(creativeId) : creativesApi.archive(creativeId);
|
|
2150
2180
|
apiCall.then(res => {
|
|
2151
2181
|
if (res.ok) {
|
|
@@ -2172,14 +2202,14 @@ export function initializeCreativeRowEditor() {
|
|
|
2172
2202
|
}
|
|
2173
2203
|
|
|
2174
2204
|
if (deleteBtn) {
|
|
2175
|
-
deleteBtn.addEventListener('click', function () {
|
|
2176
|
-
if (
|
|
2205
|
+
deleteBtn.addEventListener('click', async function () {
|
|
2206
|
+
if (await confirmDialog(deleteBtn.dataset.confirm, { danger: true })) deleteCurrent(false);
|
|
2177
2207
|
});
|
|
2178
2208
|
}
|
|
2179
2209
|
|
|
2180
2210
|
if (deleteWithChildrenBtn) {
|
|
2181
|
-
deleteWithChildrenBtn.addEventListener('click', function () {
|
|
2182
|
-
if (
|
|
2211
|
+
deleteWithChildrenBtn.addEventListener('click', async function () {
|
|
2212
|
+
if (await confirmDialog(deleteWithChildrenBtn.dataset.confirm, { danger: true })) deleteCurrent(true);
|
|
2183
2213
|
});
|
|
2184
2214
|
}
|
|
2185
2215
|
|
|
@@ -2215,17 +2245,17 @@ export function initializeCreativeRowEditor() {
|
|
|
2215
2245
|
}
|
|
2216
2246
|
|
|
2217
2247
|
if (unlinkBtn) {
|
|
2218
|
-
unlinkBtn.addEventListener('click', function () {
|
|
2219
|
-
if (
|
|
2248
|
+
unlinkBtn.addEventListener('click', async function () {
|
|
2249
|
+
if (await confirmDialog(unlinkBtn.dataset.confirm, { danger: true })) deleteCurrent(false);
|
|
2220
2250
|
});
|
|
2221
2251
|
}
|
|
2222
2252
|
|
|
2223
2253
|
if (unconvertBtn) {
|
|
2224
|
-
unconvertBtn.addEventListener('click', function () {
|
|
2254
|
+
unconvertBtn.addEventListener('click', async function () {
|
|
2225
2255
|
const creativeId = form.dataset.creativeId;
|
|
2226
2256
|
if (!creativeId) return;
|
|
2227
2257
|
const confirmText = unconvertBtn.dataset.confirm;
|
|
2228
|
-
if (confirmText && !
|
|
2258
|
+
if (confirmText && !(await confirmDialog(confirmText))) return;
|
|
2229
2259
|
const errorMessage = unconvertBtn.dataset.error || 'Failed to unconvert.';
|
|
2230
2260
|
unconvertBtn.disabled = true;
|
|
2231
2261
|
saveForm()
|
|
@@ -2235,7 +2265,7 @@ export function initializeCreativeRowEditor() {
|
|
|
2235
2265
|
.json()
|
|
2236
2266
|
.catch(function () { return {}; })
|
|
2237
2267
|
.then(function (data) {
|
|
2238
|
-
|
|
2268
|
+
alertDialog(data && data.error ? data.error : errorMessage);
|
|
2239
2269
|
const error = new Error('Save failed');
|
|
2240
2270
|
error._handled = true;
|
|
2241
2271
|
throw error;
|
|
@@ -2252,12 +2282,12 @@ export function initializeCreativeRowEditor() {
|
|
|
2252
2282
|
.json()
|
|
2253
2283
|
.catch(function () { return {}; })
|
|
2254
2284
|
.then(function (data) {
|
|
2255
|
-
|
|
2285
|
+
alertDialog(data && data.error ? data.error : errorMessage);
|
|
2256
2286
|
});
|
|
2257
2287
|
})
|
|
2258
2288
|
.catch(function (error) {
|
|
2259
2289
|
if (error && error._handled) return;
|
|
2260
|
-
|
|
2290
|
+
alertDialog(errorMessage);
|
|
2261
2291
|
})
|
|
2262
2292
|
.finally(function () {
|
|
2263
2293
|
unconvertBtn.disabled = false;
|
|
@@ -2276,7 +2306,7 @@ export function initializeCreativeRowEditor() {
|
|
|
2276
2306
|
})
|
|
2277
2307
|
.catch(function (error) {
|
|
2278
2308
|
console.error('Failed to load metadata:', error);
|
|
2279
|
-
|
|
2309
|
+
alertDialog('Failed to load metadata');
|
|
2280
2310
|
});
|
|
2281
2311
|
}
|
|
2282
2312
|
|
|
@@ -2321,17 +2351,17 @@ export function initializeCreativeRowEditor() {
|
|
|
2321
2351
|
metadataPopup.style.display = 'none';
|
|
2322
2352
|
} else {
|
|
2323
2353
|
return response.json().then(function (data) {
|
|
2324
|
-
|
|
2354
|
+
alertDialog('Failed to save metadata: ' + (data.error || 'Unknown error'));
|
|
2325
2355
|
});
|
|
2326
2356
|
}
|
|
2327
2357
|
})
|
|
2328
2358
|
.catch(function (error) {
|
|
2329
2359
|
console.error('Failed to save metadata:', error);
|
|
2330
|
-
|
|
2360
|
+
alertDialog('Failed to save metadata');
|
|
2331
2361
|
});
|
|
2332
2362
|
} catch (error) {
|
|
2333
2363
|
console.error('YAML parse error:', error);
|
|
2334
|
-
|
|
2364
|
+
alertDialog('Invalid YAML format: ' + error.message);
|
|
2335
2365
|
}
|
|
2336
2366
|
});
|
|
2337
2367
|
}
|
|
@@ -2339,27 +2369,37 @@ export function initializeCreativeRowEditor() {
|
|
|
2339
2369
|
|
|
2340
2370
|
// Markdown toggle button
|
|
2341
2371
|
if (toggleMarkdownBtn) {
|
|
2342
|
-
toggleMarkdownBtn.addEventListener('click', function () {
|
|
2372
|
+
toggleMarkdownBtn.addEventListener('click', async function () {
|
|
2343
2373
|
if (markdownMode) {
|
|
2344
|
-
// Switching from Markdown → Rich Text
|
|
2374
|
+
// Switching from Markdown (advanced textarea) → Rich Text. The Markdown
|
|
2375
|
+
// stays canonical; we only flip the authoring surface to rich so it
|
|
2376
|
+
// reopens in Lexical, and render it to HTML for the editor view.
|
|
2345
2377
|
const confirmMsg = toggleMarkdownBtn.dataset.confirmToRichtext;
|
|
2346
|
-
if (confirmMsg && !
|
|
2378
|
+
if (confirmMsg && !(await confirmDialog(confirmMsg))) return;
|
|
2347
2379
|
const md = markdownTextarea?.value || '';
|
|
2348
2380
|
const html = md ? renderMarkdown(md) : '';
|
|
2349
2381
|
deactivateMarkdownMode();
|
|
2350
2382
|
descriptionInput.value = html;
|
|
2383
|
+
if (md) {
|
|
2384
|
+
if (contentTypeInput) contentTypeInput.value = 'markdown';
|
|
2385
|
+
if (markdownSourceInput) markdownSourceInput.value = md;
|
|
2386
|
+
if (markdownEditorInput) markdownEditorInput.value = 'rich';
|
|
2387
|
+
}
|
|
2351
2388
|
lexicalEditor.load(html, `creative-switch-${Date.now()}`);
|
|
2352
2389
|
lexicalEditor.focus();
|
|
2353
2390
|
isDirty = true;
|
|
2354
2391
|
scheduleSave();
|
|
2355
2392
|
} else {
|
|
2356
|
-
// Switching from Rich Text → Markdown
|
|
2393
|
+
// Switching from Rich Text → Markdown (advanced textarea). Preserve the
|
|
2394
|
+
// content by seeding the textarea with the rich surface's current
|
|
2395
|
+
// Markdown projection instead of discarding it.
|
|
2357
2396
|
const currentHtml = descriptionInput.value || '';
|
|
2358
2397
|
if (!isHtmlEmpty(currentHtml)) {
|
|
2359
2398
|
const confirmMsg = toggleMarkdownBtn.dataset.confirmToMarkdown;
|
|
2360
|
-
if (confirmMsg && !
|
|
2399
|
+
if (confirmMsg && !(await confirmDialog(confirmMsg))) return;
|
|
2361
2400
|
}
|
|
2362
|
-
|
|
2401
|
+
const existingMarkdown = markdownSourceInput ? markdownSourceInput.value : '';
|
|
2402
|
+
activateMarkdownMode(existingMarkdown);
|
|
2363
2403
|
isDirty = true;
|
|
2364
2404
|
scheduleSave();
|
|
2365
2405
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { alertDialog } from '../lib/utils/dialog';
|
|
1
2
|
|
|
2
3
|
if (!window.isExportMarkdownEnabled) {
|
|
3
4
|
window.isExportMarkdownEnabled = true;
|
|
@@ -27,7 +28,7 @@ if (!window.isExportMarkdownEnabled) {
|
|
|
27
28
|
URL.revokeObjectURL(url);
|
|
28
29
|
}, 0);
|
|
29
30
|
})
|
|
30
|
-
.catch(err =>
|
|
31
|
+
.catch(err => alertDialog(err));
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
34
|
});
|
|
@@ -52,7 +52,8 @@ export function createInlineEditor(container, {
|
|
|
52
52
|
suppressNextChange = false
|
|
53
53
|
return
|
|
54
54
|
}
|
|
55
|
-
currentHtml
|
|
55
|
+
// value is { html, markdown }; keep currentHtml for the render fallback.
|
|
56
|
+
currentHtml = value?.html ?? ""
|
|
56
57
|
onChange?.(value)
|
|
57
58
|
}}
|
|
58
59
|
onReady={(api) => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createSubscription } from '../services/cable'
|
|
2
|
-
import
|
|
2
|
+
import { highlightCodeBlocks } from '../lib/utils/markdown'
|
|
3
|
+
import { addTableDownloadButtons } from '../lib/utils/table_download'
|
|
4
|
+
import { sanitizeDescriptionHtml } from '../lib/utils/sanitize_description'
|
|
3
5
|
|
|
4
6
|
document.addEventListener('DOMContentLoaded', function() {
|
|
5
7
|
var container = document.getElementById('slide-view');
|
|
@@ -24,6 +26,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
24
26
|
load(index, false);
|
|
25
27
|
} else {
|
|
26
28
|
updateUrl(index);
|
|
29
|
+
// Initial slide is server-rendered (static ERB): re-tokenize its code blocks
|
|
30
|
+
// and attach table download buttons, same as the JS-loaded slides below.
|
|
31
|
+
highlightCodeBlocks(contentEl);
|
|
32
|
+
addTableDownloadButtons(contentEl);
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
function updateUrl(idx) {
|
|
@@ -60,8 +66,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
60
66
|
}
|
|
61
67
|
contentEl.innerHTML = '';
|
|
62
68
|
var el = document.createElement(tag);
|
|
63
|
-
el.
|
|
69
|
+
el.className = 'creative-content';
|
|
70
|
+
el.innerHTML = sanitizeDescriptionHtml(data.description_embedded_html || data.description);
|
|
64
71
|
contentEl.appendChild(el);
|
|
72
|
+
highlightCodeBlocks(el);
|
|
73
|
+
addTableDownloadButtons(el);
|
|
65
74
|
if (captionEl) {
|
|
66
75
|
captionEl.textContent = data.prompt || '';
|
|
67
76
|
}
|