collavre 0.23.0 → 0.24.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/images/collavre/landing/interface-en-dark.png +0 -0
- data/app/assets/images/collavre/landing/interface-en.png +0 -0
- data/app/assets/images/collavre/landing/interface-ko-dark.png +0 -0
- data/app/assets/images/collavre/landing/interface-ko.png +0 -0
- data/app/assets/stylesheets/collavre/comments_popup.css +40 -0
- data/app/assets/stylesheets/collavre/landing.css +92 -0
- data/app/assets/stylesheets/collavre/popup.css +4 -2
- data/app/controllers/collavre/admin/settings_controller.rb +52 -67
- data/app/controllers/collavre/api/v1/agents_controller.rb +17 -188
- data/app/controllers/collavre/api/v1/base_controller.rb +9 -27
- data/app/controllers/collavre/attachments_controller.rb +13 -3
- data/app/controllers/collavre/concerns/slide_viewable.rb +8 -32
- data/app/controllers/collavre/creatives_controller.rb +32 -99
- data/app/controllers/collavre/inbox_items_controller.rb +10 -4
- data/app/controllers/collavre/landing_controller.rb +5 -2
- data/app/controllers/collavre/topics_controller.rb +5 -0
- data/app/controllers/concerns/collavre/api/authenticatable.rb +40 -0
- data/app/errors/collavre/api_error.rb +20 -0
- data/app/helpers/collavre/creatives_helper.rb +30 -8
- data/app/javascript/components/InlineLexicalEditor.jsx +2 -34
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +87 -0
- data/app/javascript/components/creative_tree_row.js +8 -2
- data/app/javascript/components/plugins/markdown_shortcuts_plugin.jsx +15 -4
- data/app/javascript/controllers/__tests__/search_popup_controller.test.js +104 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +82 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +61 -0
- data/app/javascript/controllers/comments/topics_controller.js +47 -0
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- data/app/javascript/controllers/index.js +3 -0
- data/app/javascript/controllers/search_popup_controller.js +17 -1
- data/app/javascript/controllers/topic_list_controller.js +78 -0
- data/app/javascript/lib/__tests__/common_popup_bounds.test.js +58 -0
- data/app/javascript/lib/common_popup.js +24 -5
- data/app/javascript/lib/lexical/__tests__/code_block_toggle.test.js +326 -0
- data/app/javascript/lib/lexical/__tests__/code_fence_shortcut.test.js +179 -0
- data/app/javascript/lib/lexical/code_block_toggle.js +130 -0
- data/app/javascript/lib/lexical/code_fence_shortcut.js +60 -0
- data/app/javascript/modules/__tests__/creative_inline_payload.test.js +231 -0
- data/app/javascript/modules/__tests__/creative_save_queue.test.js +228 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +425 -0
- data/app/javascript/modules/creative_inline_payload.js +86 -0
- data/app/javascript/modules/creative_row_editor.js +121 -401
- data/app/javascript/modules/creative_row_editor_helpers.js +76 -0
- data/app/javascript/modules/creative_save_queue.js +95 -0
- data/app/javascript/modules/creative_tree_dom.js +260 -0
- data/app/jobs/collavre/permission_cache_job.rb +20 -2
- data/app/jobs/collavre/trigger_loop_check_job.rb +20 -3
- data/app/jobs/collavre/update_mcp_tools_job.rb +17 -0
- data/app/models/collavre/channel.rb +16 -0
- data/app/models/collavre/comment/approvable.rb +16 -0
- data/app/models/collavre/comment/notifiable.rb +1 -1
- data/app/models/collavre/comment.rb +8 -1
- data/app/models/collavre/comment_version.rb +1 -1
- data/app/models/collavre/creative/describable.rb +15 -9
- data/app/models/collavre/creative/linkable.rb +9 -0
- data/app/models/collavre/creative/permissible.rb +97 -49
- data/app/models/collavre/creative.rb +73 -5
- data/app/models/collavre/creative_share.rb +78 -26
- data/app/models/collavre/inbox_item.rb +2 -2
- data/app/models/collavre/task.rb +12 -0
- data/app/models/collavre/user.rb +33 -9
- data/app/models/concerns/collavre/indexed_json_columns.rb +58 -0
- data/app/services/collavre/agent_type_classifier.rb +23 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +4 -0
- data/app/services/collavre/ai_agent/message_builder.rb +1 -1
- data/app/services/collavre/ai_agent/session_provisioner.rb +126 -0
- data/app/services/collavre/ai_agent/task_claim_service.rb +96 -0
- data/app/services/collavre/ai_client.rb +53 -2
- data/app/services/collavre/auto_theme_generator.rb +14 -10
- data/app/services/collavre/creatives/children_index.rb +86 -0
- data/app/services/collavre/creatives/comment_badge_index.rb +73 -0
- data/app/services/collavre/creatives/creative_tree_serializer.rb +122 -0
- data/app/services/collavre/creatives/effective_creative_resolution.rb +37 -0
- data/app/services/collavre/creatives/permission_checker.rb +1 -1
- data/app/services/collavre/creatives/permission_filter.rb +144 -17
- data/app/services/collavre/creatives/tree_builder.rb +105 -77
- data/app/services/collavre/http_client.rb +119 -0
- data/app/services/collavre/link_preview_fetcher.rb +129 -38
- data/app/services/collavre/markdown_importer.rb +30 -6
- data/app/services/collavre/mobile/event_summarizer.rb +1 -1
- data/app/services/collavre/orchestration/agent_context_builder.rb +1 -15
- data/app/services/collavre/orchestration/agent_orchestrator.rb +1 -1
- data/app/services/collavre/system_events/context_builder.rb +1 -14
- data/app/services/collavre/tools/preview_attach_service.rb +5 -7
- data/app/views/collavre/comments/_comment.html.erb +4 -4
- data/app/views/collavre/comments/_comments_popup.html.erb +11 -3
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +9 -0
- data/app/views/collavre/landing/show.html.erb +52 -10
- data/config/locales/comments.en.yml +1 -0
- data/config/locales/comments.ko.yml +1 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +5 -0
- data/config/locales/landing.en.yml +22 -10
- data/config/locales/landing.ko.yml +22 -10
- data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +13 -4
- data/db/migrate/20260702000001_enforce_system_settings_key_not_null.rb +9 -0
- data/db/migrate/20260702000002_add_creative_index_and_fk_to_tags.rb +11 -0
- data/db/migrate/20260702000003_add_creative_type_index_to_labels.rb +10 -0
- data/db/migrate/20260702000004_add_creative_created_at_index_to_comments.rb +10 -0
- data/db/migrate/20260702000005_promote_channel_config_index_columns.rb +76 -0
- data/db/migrate/20260713000001_add_comments_count_to_creatives.rb +20 -0
- data/db/migrate/20260713000002_add_comment_versions_count_to_comments.rb +20 -0
- data/db/migrate/20260715000000_change_creatives_description_to_plain_text.rb +16 -0
- data/lib/collavre/version.rb +1 -1
- metadata +42 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Characterization tests for the tree-DOM navigation/geometry cluster extracted
|
|
3
|
+
* from creative_row_editor.js (slice 2 of the god-file decomposition).
|
|
4
|
+
*
|
|
5
|
+
* These pin the CURRENT behavior of the sibling/level/parent computations that
|
|
6
|
+
* feed the structural-move paths (levelUp/levelDown/addNew/move) — the inputs to
|
|
7
|
+
* the recurring "parentId loss / wrong ordering" bug class. They are written
|
|
8
|
+
* against a jsdom fixture that mirrors the real creative-tree render: a
|
|
9
|
+
* `creative-tree-row` custom element wrapping a `.creative-tree` node, with
|
|
10
|
+
* `.creative-children` containers as siblings of the row.
|
|
11
|
+
*
|
|
12
|
+
* Intentionally behavior-preserving: any surprising result below (e.g. the
|
|
13
|
+
* before/after id inversion in siblingOrderingForRow) documents existing
|
|
14
|
+
* behavior, not an endorsement of it.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
creativeTreeElement,
|
|
18
|
+
creativeIdFrom,
|
|
19
|
+
siblingTreeRow,
|
|
20
|
+
siblingOrderingForRow,
|
|
21
|
+
treeContainerElement,
|
|
22
|
+
nodeAfterTreeBlock,
|
|
23
|
+
normalizeRowNode,
|
|
24
|
+
childrenContainerForTree,
|
|
25
|
+
ensureChildrenContainer,
|
|
26
|
+
expandChildrenContainer,
|
|
27
|
+
moveTreeBlock,
|
|
28
|
+
listAllTreeNodes,
|
|
29
|
+
findPreviousTree,
|
|
30
|
+
getTreeLevel,
|
|
31
|
+
updateTreeLevels,
|
|
32
|
+
setTreeLevel,
|
|
33
|
+
removeTreeElement,
|
|
34
|
+
} from '../creative_tree_dom';
|
|
35
|
+
|
|
36
|
+
// Build one row: <creative-tree-row><div.creative-tree/></creative-tree-row>
|
|
37
|
+
function makeRow(id, { parentId = '', level = 1 } = {}) {
|
|
38
|
+
const row = document.createElement('creative-tree-row');
|
|
39
|
+
row.setAttribute('creative-id', String(id));
|
|
40
|
+
row.setAttribute('level', String(level));
|
|
41
|
+
const tree = document.createElement('div');
|
|
42
|
+
tree.className = 'creative-tree';
|
|
43
|
+
tree.id = `creative-${id}`;
|
|
44
|
+
tree.dataset.id = String(id);
|
|
45
|
+
tree.dataset.parentId = parentId;
|
|
46
|
+
tree.dataset.level = String(level);
|
|
47
|
+
row.appendChild(tree);
|
|
48
|
+
return { row, tree };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeChildrenContainer(parentId) {
|
|
52
|
+
const c = document.createElement('div');
|
|
53
|
+
c.className = 'creative-children';
|
|
54
|
+
c.id = `creative-children-${parentId}`;
|
|
55
|
+
return c;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function treeOf(row) {
|
|
59
|
+
return row.querySelector('.creative-tree');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let root;
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
document.body.innerHTML = '';
|
|
65
|
+
root = document.createElement('div');
|
|
66
|
+
root.id = 'creatives';
|
|
67
|
+
document.body.appendChild(root);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('creativeTreeElement / creativeIdFrom', () => {
|
|
71
|
+
test('creativeTreeElement returns the node itself when it is a .creative-tree', () => {
|
|
72
|
+
const { tree } = makeRow(1);
|
|
73
|
+
expect(creativeTreeElement(tree)).toBe(tree);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('creativeTreeElement descends into a wrapping row', () => {
|
|
77
|
+
const { row, tree } = makeRow(1);
|
|
78
|
+
expect(creativeTreeElement(row)).toBe(tree);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('creativeTreeElement returns null for a nullish node', () => {
|
|
82
|
+
expect(creativeTreeElement(null)).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('creativeIdFrom reads the inner .creative-tree data-id from a row', () => {
|
|
86
|
+
const { row } = makeRow(42);
|
|
87
|
+
expect(creativeIdFrom(row)).toBe('42');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('creativeIdFrom falls back to creative-id / data-id attributes', () => {
|
|
91
|
+
const el = document.createElement('div');
|
|
92
|
+
el.setAttribute('creative-id', '7');
|
|
93
|
+
expect(creativeIdFrom(el)).toBe('7');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('creativeIdFrom returns empty string when nothing identifies the node', () => {
|
|
97
|
+
expect(creativeIdFrom(null)).toBe('');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('siblingTreeRow', () => {
|
|
102
|
+
test('finds the next/previous row, skipping text nodes', () => {
|
|
103
|
+
const a = makeRow(1);
|
|
104
|
+
const b = makeRow(2);
|
|
105
|
+
root.appendChild(a.row);
|
|
106
|
+
root.appendChild(document.createTextNode('\n '));
|
|
107
|
+
root.appendChild(b.row);
|
|
108
|
+
|
|
109
|
+
expect(siblingTreeRow(a.row, 'next')).toBe(b.row);
|
|
110
|
+
expect(siblingTreeRow(b.row, 'previous')).toBe(a.row);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('skips over intervening .creative-children containers', () => {
|
|
114
|
+
const a = makeRow(1);
|
|
115
|
+
const b = makeRow(2);
|
|
116
|
+
root.appendChild(a.row);
|
|
117
|
+
root.appendChild(makeChildrenContainer(1)); // between the two rows
|
|
118
|
+
root.appendChild(b.row);
|
|
119
|
+
|
|
120
|
+
expect(siblingTreeRow(a.row, 'next')).toBe(b.row);
|
|
121
|
+
expect(siblingTreeRow(b.row, 'previous')).toBe(a.row);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('returns null at the ends', () => {
|
|
125
|
+
const a = makeRow(1);
|
|
126
|
+
root.appendChild(a.row);
|
|
127
|
+
expect(siblingTreeRow(a.row, 'next')).toBeNull();
|
|
128
|
+
expect(siblingTreeRow(a.row, 'previous')).toBeNull();
|
|
129
|
+
expect(siblingTreeRow(null, 'next')).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('siblingOrderingForRow', () => {
|
|
134
|
+
// Pins the (surprising) mapping: beforeId := NEXT sibling, afterId := PREVIOUS
|
|
135
|
+
// sibling. This is existing behavior consumed by persistStructureChange.
|
|
136
|
+
test('beforeId comes from the next sibling, afterId from the previous', () => {
|
|
137
|
+
const a = makeRow(1);
|
|
138
|
+
const b = makeRow(2);
|
|
139
|
+
const c = makeRow(3);
|
|
140
|
+
root.appendChild(a.row);
|
|
141
|
+
root.appendChild(b.row);
|
|
142
|
+
root.appendChild(c.row);
|
|
143
|
+
|
|
144
|
+
expect(siblingOrderingForRow(b.row)).toEqual({ beforeId: '3', afterId: '1' });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('empty ids at the boundaries', () => {
|
|
148
|
+
const a = makeRow(1);
|
|
149
|
+
root.appendChild(a.row);
|
|
150
|
+
expect(siblingOrderingForRow(a.row)).toEqual({ beforeId: '', afterId: '' });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('treeContainerElement', () => {
|
|
155
|
+
test('returns the row parent when the tree has a wrapping row', () => {
|
|
156
|
+
const { row, tree } = makeRow(1);
|
|
157
|
+
root.appendChild(row);
|
|
158
|
+
expect(treeContainerElement(tree)).toBe(root);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('returns null for nullish tree', () => {
|
|
162
|
+
expect(treeContainerElement(null)).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('nodeAfterTreeBlock', () => {
|
|
167
|
+
test('returns the next row when there is no children container', () => {
|
|
168
|
+
const a = makeRow(1);
|
|
169
|
+
const b = makeRow(2);
|
|
170
|
+
root.appendChild(a.row);
|
|
171
|
+
root.appendChild(b.row);
|
|
172
|
+
expect(nodeAfterTreeBlock(a.tree)).toBe(b.row);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('skips the tree\'s own children container to the node after it', () => {
|
|
176
|
+
const a = makeRow(1);
|
|
177
|
+
const b = makeRow(2);
|
|
178
|
+
const children = makeChildrenContainer(1);
|
|
179
|
+
root.appendChild(a.row);
|
|
180
|
+
root.appendChild(children);
|
|
181
|
+
root.appendChild(b.row);
|
|
182
|
+
expect(nodeAfterTreeBlock(a.tree)).toBe(b.row);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('normalizeRowNode', () => {
|
|
187
|
+
test('returns the row unchanged', () => {
|
|
188
|
+
const { row } = makeRow(1);
|
|
189
|
+
expect(normalizeRowNode(row)).toBe(row);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('maps a .creative-tree node to its wrapping row', () => {
|
|
193
|
+
const { row, tree } = makeRow(1);
|
|
194
|
+
expect(normalizeRowNode(tree)).toBe(row);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('returns nullish input as null', () => {
|
|
198
|
+
expect(normalizeRowNode(null)).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('childrenContainerForTree', () => {
|
|
203
|
+
test('finds the container by id', () => {
|
|
204
|
+
const { row, tree } = makeRow(1);
|
|
205
|
+
const children = makeChildrenContainer(1);
|
|
206
|
+
root.appendChild(row);
|
|
207
|
+
root.appendChild(children);
|
|
208
|
+
expect(childrenContainerForTree(tree)).toBe(children);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('finds the container as a following sibling of the row', () => {
|
|
212
|
+
const { row, tree } = makeRow(5);
|
|
213
|
+
const children = document.createElement('div');
|
|
214
|
+
children.className = 'creative-children'; // no matching id
|
|
215
|
+
root.appendChild(row);
|
|
216
|
+
root.appendChild(children);
|
|
217
|
+
expect(childrenContainerForTree(tree)).toBe(children);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('returns null when no container exists', () => {
|
|
221
|
+
const { row, tree } = makeRow(9);
|
|
222
|
+
root.appendChild(row);
|
|
223
|
+
expect(childrenContainerForTree(tree)).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('ensureChildrenContainer', () => {
|
|
228
|
+
test('reuses an existing container', () => {
|
|
229
|
+
const { row, tree } = makeRow(1);
|
|
230
|
+
const existing = makeChildrenContainer(1);
|
|
231
|
+
root.appendChild(row);
|
|
232
|
+
root.appendChild(existing);
|
|
233
|
+
expect(ensureChildrenContainer(tree)).toBe(existing);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('creates a container with child level (+1) load url and inserts after the row', () => {
|
|
237
|
+
const { row, tree } = makeRow(3, { level: 2 });
|
|
238
|
+
root.appendChild(row);
|
|
239
|
+
|
|
240
|
+
const created = ensureChildrenContainer(tree);
|
|
241
|
+
|
|
242
|
+
expect(created).not.toBeNull();
|
|
243
|
+
expect(created.id).toBe('creative-children-3');
|
|
244
|
+
expect(created.classList.contains('creative-children')).toBe(true);
|
|
245
|
+
expect(created.dataset.expanded).toBe('true');
|
|
246
|
+
// level+1 = 3, select_mode 0
|
|
247
|
+
expect(created.dataset.loadUrl).toBe('/creatives/3/children?level=3&select_mode=0');
|
|
248
|
+
// inserted into the row's parent, right after the row
|
|
249
|
+
expect(created.previousElementSibling).toBe(row);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('returns null when the tree has no id', () => {
|
|
253
|
+
const tree = document.createElement('div');
|
|
254
|
+
tree.className = 'creative-tree';
|
|
255
|
+
root.appendChild(tree);
|
|
256
|
+
expect(ensureChildrenContainer(tree)).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('expandChildrenContainer', () => {
|
|
261
|
+
test('clears display and marks expanded', () => {
|
|
262
|
+
const c = makeChildrenContainer(1);
|
|
263
|
+
c.style.display = 'none';
|
|
264
|
+
expandChildrenContainer(c);
|
|
265
|
+
expect(c.style.display).toBe('');
|
|
266
|
+
expect(c.dataset.expanded).toBe('true');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('no-ops on nullish container', () => {
|
|
270
|
+
expect(() => expandChildrenContainer(null)).not.toThrow();
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('moveTreeBlock', () => {
|
|
275
|
+
test('moves the row and its children container into the target (append)', () => {
|
|
276
|
+
const src = makeRow(1);
|
|
277
|
+
const children = makeChildrenContainer(1);
|
|
278
|
+
const targetParent = makeChildrenContainer(99);
|
|
279
|
+
root.appendChild(src.row);
|
|
280
|
+
root.appendChild(children);
|
|
281
|
+
root.appendChild(targetParent);
|
|
282
|
+
|
|
283
|
+
moveTreeBlock(src.tree, targetParent);
|
|
284
|
+
|
|
285
|
+
expect(src.row.parentNode).toBe(targetParent);
|
|
286
|
+
expect(children.parentNode).toBe(targetParent);
|
|
287
|
+
// row moved before its children container (append order preserved)
|
|
288
|
+
expect([...targetParent.children]).toEqual([src.row, children]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('inserts before a reference node', () => {
|
|
292
|
+
const src = makeRow(1);
|
|
293
|
+
const ref = makeRow(2);
|
|
294
|
+
const targetParent = makeChildrenContainer(99);
|
|
295
|
+
// Keep src.row's next sibling from being a .creative-children, otherwise
|
|
296
|
+
// childrenContainerForTree's sibling fallback would treat targetParent as
|
|
297
|
+
// src's own children container (covered separately above).
|
|
298
|
+
const srcHolder = document.createElement('div');
|
|
299
|
+
srcHolder.appendChild(src.row);
|
|
300
|
+
root.appendChild(srcHolder);
|
|
301
|
+
targetParent.appendChild(ref.row);
|
|
302
|
+
root.appendChild(targetParent);
|
|
303
|
+
|
|
304
|
+
moveTreeBlock(src.tree, targetParent, ref.row);
|
|
305
|
+
|
|
306
|
+
expect([...targetParent.children]).toEqual([src.row, ref.row]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('no-ops without a target', () => {
|
|
310
|
+
const src = makeRow(1);
|
|
311
|
+
root.appendChild(src.row);
|
|
312
|
+
expect(() => moveTreeBlock(src.tree, null)).not.toThrow();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('listAllTreeNodes', () => {
|
|
317
|
+
test('returns all .creative-tree under #creatives in document order', () => {
|
|
318
|
+
const a = makeRow(1);
|
|
319
|
+
const b = makeRow(2);
|
|
320
|
+
root.appendChild(a.row);
|
|
321
|
+
root.appendChild(b.row);
|
|
322
|
+
expect(listAllTreeNodes()).toEqual([a.tree, b.tree]);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('getTreeLevel', () => {
|
|
327
|
+
test('reads data-level when present and positive', () => {
|
|
328
|
+
const { tree } = makeRow(1, { level: 4 });
|
|
329
|
+
expect(getTreeLevel(tree)).toBe(4);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('falls back to the wrapping row level attribute when data-level absent', () => {
|
|
333
|
+
const { row, tree } = makeRow(1, { level: 3 });
|
|
334
|
+
delete tree.dataset.level;
|
|
335
|
+
root.appendChild(row);
|
|
336
|
+
expect(getTreeLevel(tree)).toBe(3);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('defaults to 1 for nullish tree', () => {
|
|
340
|
+
expect(getTreeLevel(null)).toBe(1);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('findPreviousTree', () => {
|
|
345
|
+
// #creatives: row1(L1), [children-1: row11(L2)], row2(L1)
|
|
346
|
+
function buildNested() {
|
|
347
|
+
const one = makeRow(1, { level: 1 });
|
|
348
|
+
const children1 = makeChildrenContainer(1);
|
|
349
|
+
const eleven = makeRow(11, { parentId: '1', level: 2 });
|
|
350
|
+
children1.appendChild(eleven.row);
|
|
351
|
+
const two = makeRow(2, { level: 1 });
|
|
352
|
+
root.appendChild(one.row);
|
|
353
|
+
root.appendChild(children1);
|
|
354
|
+
root.appendChild(two.row);
|
|
355
|
+
return { one, eleven, two };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
test('returns the nearest previous tree at the same level', () => {
|
|
359
|
+
const { one, two } = buildNested();
|
|
360
|
+
expect(findPreviousTree(two.tree)).toBe(one.tree);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('returns null when the previous tree is at a shallower level', () => {
|
|
364
|
+
const { eleven } = buildNested();
|
|
365
|
+
// eleven (L2) is preceded only by one (L1) which is shallower -> null
|
|
366
|
+
expect(findPreviousTree(eleven.tree)).toBeNull();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('returns null for the first tree', () => {
|
|
370
|
+
const { one } = buildNested();
|
|
371
|
+
expect(findPreviousTree(one.tree)).toBeNull();
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe('updateTreeLevels / setTreeLevel', () => {
|
|
376
|
+
test('updateTreeLevels applies delta to the tree and its child rows recursively', () => {
|
|
377
|
+
const parent = makeRow(1, { level: 1 });
|
|
378
|
+
const children = makeChildrenContainer(1);
|
|
379
|
+
const child = makeRow(11, { parentId: '1', level: 2 });
|
|
380
|
+
children.appendChild(child.row);
|
|
381
|
+
root.appendChild(parent.row);
|
|
382
|
+
root.appendChild(children);
|
|
383
|
+
|
|
384
|
+
updateTreeLevels(parent.tree, 1);
|
|
385
|
+
|
|
386
|
+
expect(parent.tree.dataset.level).toBe('2');
|
|
387
|
+
expect(parent.row.getAttribute('level')).toBe('2');
|
|
388
|
+
expect(child.tree.dataset.level).toBe('3');
|
|
389
|
+
expect(child.row.getAttribute('level')).toBe('3');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('updateTreeLevels floors the level at 1', () => {
|
|
393
|
+
const { tree, row } = makeRow(1, { level: 1 });
|
|
394
|
+
root.appendChild(row);
|
|
395
|
+
updateTreeLevels(tree, -5);
|
|
396
|
+
expect(tree.dataset.level).toBe('1');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('setTreeLevel computes the delta to reach the target level', () => {
|
|
400
|
+
const { tree, row } = makeRow(1, { level: 2 });
|
|
401
|
+
root.appendChild(row);
|
|
402
|
+
setTreeLevel(tree, 4);
|
|
403
|
+
expect(tree.dataset.level).toBe('4');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('setTreeLevel is a no-op when already at the target', () => {
|
|
407
|
+
const { tree, row } = makeRow(1, { level: 2 });
|
|
408
|
+
root.appendChild(row);
|
|
409
|
+
setTreeLevel(tree, 2);
|
|
410
|
+
expect(tree.dataset.level).toBe('2');
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe('removeTreeElement', () => {
|
|
415
|
+
test('removes the wrapping row', () => {
|
|
416
|
+
const { row, tree } = makeRow(1);
|
|
417
|
+
root.appendChild(row);
|
|
418
|
+
removeTreeElement(tree);
|
|
419
|
+
expect(root.querySelector('creative-tree-row')).toBeNull();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('no-ops on nullish tree', () => {
|
|
423
|
+
expect(() => removeTreeElement(null)).not.toThrow();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Inline creative payload mapping cluster extracted from creative_row_editor.js
|
|
2
|
+
// (slice 3 of the god-file decomposition).
|
|
3
|
+
//
|
|
4
|
+
// These two functions are the inverse pair over the "inline creative payload"
|
|
5
|
+
// shape that drives instant/optimistic UI: `inlinePayloadFromTree` reads a
|
|
6
|
+
// payload OUT of a rendered tree row's dataset, and `updateRowFromData` writes a
|
|
7
|
+
// server/API payload INTO a row's dataset + attributes. Both depend only on their
|
|
8
|
+
// arguments (plus the pure DOM helpers below and browser globals) — neither
|
|
9
|
+
// captures editor closure state, and neither uses timers or `this`. Behavior is
|
|
10
|
+
// identical to the original inline versions.
|
|
11
|
+
import {
|
|
12
|
+
treeRowElement,
|
|
13
|
+
hasDatasetValue,
|
|
14
|
+
setRowDatasetValue,
|
|
15
|
+
} from './creative_row_editor_helpers'
|
|
16
|
+
|
|
17
|
+
export function updateRowFromData(row, data) {
|
|
18
|
+
if (!row || !data) return;
|
|
19
|
+
const descriptionHtml = data.description || '';
|
|
20
|
+
const rawHtml = data.description_raw_html || descriptionHtml;
|
|
21
|
+
row.descriptionHtml = descriptionHtml;
|
|
22
|
+
setRowDatasetValue(row, 'descriptionHtml', descriptionHtml);
|
|
23
|
+
setRowDatasetValue(row, 'descriptionRawHtml', rawHtml);
|
|
24
|
+
if (data.progress_html != null) {
|
|
25
|
+
row.progressHtml = data.progress_html;
|
|
26
|
+
setRowDatasetValue(row, 'progressHtml', data.progress_html);
|
|
27
|
+
}
|
|
28
|
+
if (Object.prototype.hasOwnProperty.call(data, 'progress')) {
|
|
29
|
+
setRowDatasetValue(row, 'progressValue', data.progress ?? '');
|
|
30
|
+
}
|
|
31
|
+
if (Object.prototype.hasOwnProperty.call(data, 'origin_id')) {
|
|
32
|
+
setRowDatasetValue(row, 'originId', data.origin_id ?? '');
|
|
33
|
+
}
|
|
34
|
+
if (Object.prototype.hasOwnProperty.call(data, 'content_type')) {
|
|
35
|
+
setRowDatasetValue(row, 'contentType', data.content_type ?? '');
|
|
36
|
+
}
|
|
37
|
+
if (Object.prototype.hasOwnProperty.call(data, 'markdown_source')) {
|
|
38
|
+
setRowDatasetValue(row, 'markdownSource', data.markdown_source ?? '');
|
|
39
|
+
}
|
|
40
|
+
if (Object.prototype.hasOwnProperty.call(data, 'markdown_editor')) {
|
|
41
|
+
setRowDatasetValue(row, 'markdownEditor', data.markdown_editor ?? '');
|
|
42
|
+
}
|
|
43
|
+
if (Object.prototype.hasOwnProperty.call(data, 'has_children')) {
|
|
44
|
+
if (data.has_children) {
|
|
45
|
+
row.setAttribute('has-children', '');
|
|
46
|
+
row.hasChildren = true;
|
|
47
|
+
} else {
|
|
48
|
+
row.removeAttribute('has-children');
|
|
49
|
+
row.hasChildren = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (typeof row.requestUpdate === 'function') {
|
|
53
|
+
row.requestUpdate();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function inlinePayloadFromTree(tree) {
|
|
58
|
+
if (!tree) return null;
|
|
59
|
+
const row = treeRowElement(tree);
|
|
60
|
+
if (!row) return null;
|
|
61
|
+
|
|
62
|
+
// Relax validation - allow loading with partial data for instant UI
|
|
63
|
+
const hasDescription = hasDatasetValue(row, 'descriptionRawHtml') || hasDatasetValue(row, 'descriptionHtml');
|
|
64
|
+
const hasProgress = hasDatasetValue(row, 'progressValue');
|
|
65
|
+
|
|
66
|
+
// Only require ID to be present
|
|
67
|
+
const id = tree.dataset?.id;
|
|
68
|
+
if (!id) return null;
|
|
69
|
+
|
|
70
|
+
const rawHtml = hasDatasetValue(row, 'descriptionRawHtml') ? row.dataset.descriptionRawHtml : row.dataset.descriptionHtml || '';
|
|
71
|
+
const description = row.dataset.descriptionHtml || rawHtml || '';
|
|
72
|
+
const progressValue = hasProgress ? Number(row.dataset.progressValue ?? 0) : 0;
|
|
73
|
+
const parentId = tree.dataset?.parentId || '';
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
id: id,
|
|
77
|
+
description,
|
|
78
|
+
description_raw_html: rawHtml,
|
|
79
|
+
origin_id: row.dataset?.originId || '',
|
|
80
|
+
parent_id: parentId,
|
|
81
|
+
progress: Number.isNaN(progressValue) ? 0 : progressValue,
|
|
82
|
+
content_type: row.dataset?.contentType || null,
|
|
83
|
+
markdown_editor: row.dataset?.markdownEditor || null,
|
|
84
|
+
markdown_source: row.dataset?.markdownSource || null
|
|
85
|
+
};
|
|
86
|
+
}
|