collavre_notion 0.1.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 +7 -0
- data/README.md +58 -0
- data/Rakefile +3 -0
- data/app/controllers/collavre_notion/application_controller.rb +12 -0
- data/app/controllers/collavre_notion/creatives/notion_integrations_controller.rb +196 -0
- data/app/controllers/collavre_notion/notion_auth_controller.rb +48 -0
- data/app/javascript/collavre_notion.js +506 -0
- data/app/jobs/collavre_notion/notion_export_job.rb +29 -0
- data/app/jobs/collavre_notion/notion_sync_job.rb +47 -0
- data/app/models/collavre_notion/notion_account.rb +17 -0
- data/app/models/collavre_notion/notion_block_link.rb +10 -0
- data/app/models/collavre_notion/notion_page_link.rb +19 -0
- data/app/services/collavre_notion/notion_client.rb +231 -0
- data/app/services/collavre_notion/notion_creative_exporter.rb +296 -0
- data/app/services/collavre_notion/notion_service.rb +249 -0
- data/app/views/collavre_notion/integrations/_modal.html.erb +91 -0
- data/config/locales/en.yml +36 -0
- data/config/locales/ko.yml +36 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20241201000000_create_notion_integrations.rb +29 -0
- data/db/migrate/20250312000000_create_notion_block_links.rb +16 -0
- data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +5 -0
- data/lib/collavre_notion/engine.rb +89 -0
- data/lib/collavre_notion/version.rb +3 -0
- data/lib/collavre_notion.rb +5 -0
- metadata +109 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
let notionIntegrationInitialized = false;
|
|
2
|
+
|
|
3
|
+
if (!notionIntegrationInitialized) {
|
|
4
|
+
notionIntegrationInitialized = true;
|
|
5
|
+
|
|
6
|
+
document.addEventListener('turbo:load', function () {
|
|
7
|
+
const openBtn = document.getElementById('notion-integration-btn');
|
|
8
|
+
const modal = document.getElementById('notion-integration-modal');
|
|
9
|
+
if (!openBtn || !modal) return;
|
|
10
|
+
|
|
11
|
+
const statusEl = document.getElementById('notion-integration-status');
|
|
12
|
+
const loginBtn = document.getElementById('notion-login-btn');
|
|
13
|
+
const loginForm = document.getElementById('notion-login-form');
|
|
14
|
+
const closeBtn = document.getElementById('close-notion-modal');
|
|
15
|
+
const prevBtn = document.getElementById('notion-prev-btn');
|
|
16
|
+
const nextBtn = document.getElementById('notion-next-btn');
|
|
17
|
+
const exportBtn = document.getElementById('notion-export-btn');
|
|
18
|
+
const syncBtn = document.getElementById('notion-sync-btn');
|
|
19
|
+
const deleteBtn = document.getElementById('notion-delete-btn');
|
|
20
|
+
const errorEl = document.getElementById('notion-wizard-error');
|
|
21
|
+
const existingContainer = document.getElementById('notion-existing-connections');
|
|
22
|
+
const existingList = document.getElementById('notion-existing-page-list');
|
|
23
|
+
const connectMessage = document.getElementById('notion-connect-message');
|
|
24
|
+
const workspaceNameEl = document.getElementById('notion-workspace-name');
|
|
25
|
+
const parentPageSection = document.getElementById('notion-parent-page-section');
|
|
26
|
+
const parentPageSelect = document.getElementById('notion-parent-page-select');
|
|
27
|
+
const creativeTitleEl = document.getElementById('notion-creative-title');
|
|
28
|
+
const workspaceSummaryEl = document.getElementById('notion-workspace-summary');
|
|
29
|
+
const exportTypeSummaryEl = document.getElementById('notion-export-type-summary');
|
|
30
|
+
const parentSummaryEl = document.getElementById('notion-parent-summary');
|
|
31
|
+
const parentPageSummaryEl = document.getElementById('notion-parent-page-summary');
|
|
32
|
+
|
|
33
|
+
let creativeId = null;
|
|
34
|
+
let currentStep = 'connect';
|
|
35
|
+
let hasExistingIntegration = false;
|
|
36
|
+
let workspaceInfo = null;
|
|
37
|
+
let availablePages = [];
|
|
38
|
+
let exportType = 'new-page';
|
|
39
|
+
let selectedParentPage = null;
|
|
40
|
+
|
|
41
|
+
function csrfToken() {
|
|
42
|
+
return document.querySelector('meta[name="csrf-token"]')?.content;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resetWizard() {
|
|
46
|
+
currentStep = 'connect';
|
|
47
|
+
hasExistingIntegration = false;
|
|
48
|
+
workspaceInfo = null;
|
|
49
|
+
availablePages = [];
|
|
50
|
+
exportType = 'new-page';
|
|
51
|
+
selectedParentPage = null;
|
|
52
|
+
statusEl.textContent = '';
|
|
53
|
+
errorEl.style.display = 'none';
|
|
54
|
+
errorEl.textContent = '';
|
|
55
|
+
if (existingContainer) {
|
|
56
|
+
existingContainer.style.display = 'none';
|
|
57
|
+
}
|
|
58
|
+
if (existingList) {
|
|
59
|
+
existingList.innerHTML = '';
|
|
60
|
+
}
|
|
61
|
+
if (connectMessage) {
|
|
62
|
+
connectMessage.style.display = '';
|
|
63
|
+
}
|
|
64
|
+
if (deleteBtn) deleteBtn.style.display = 'none';
|
|
65
|
+
if (syncBtn) syncBtn.style.display = 'none';
|
|
66
|
+
if (loginBtn) loginBtn.style.display = 'inline-block';
|
|
67
|
+
if (parentPageSection) parentPageSection.style.display = 'none';
|
|
68
|
+
updateStep();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function updateStep() {
|
|
72
|
+
['connect', 'workspace', 'summary']
|
|
73
|
+
.forEach(function (step) {
|
|
74
|
+
const el = document.getElementById(`notion-step-${step}`);
|
|
75
|
+
if (!el) return;
|
|
76
|
+
el.style.display = (step === currentStep) ? 'block' : 'none';
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (currentStep === 'connect') {
|
|
80
|
+
prevBtn.style.display = 'none';
|
|
81
|
+
if (hasExistingIntegration) {
|
|
82
|
+
nextBtn.style.display = 'block';
|
|
83
|
+
nextBtn.disabled = false;
|
|
84
|
+
} else {
|
|
85
|
+
nextBtn.style.display = 'none';
|
|
86
|
+
}
|
|
87
|
+
exportBtn.style.display = 'none';
|
|
88
|
+
} else if (currentStep === 'workspace') {
|
|
89
|
+
prevBtn.style.display = 'block';
|
|
90
|
+
prevBtn.disabled = false;
|
|
91
|
+
nextBtn.style.display = 'block';
|
|
92
|
+
nextBtn.disabled = false;
|
|
93
|
+
exportBtn.style.display = 'none';
|
|
94
|
+
} else if (currentStep === 'summary') {
|
|
95
|
+
prevBtn.style.display = 'block';
|
|
96
|
+
prevBtn.disabled = false;
|
|
97
|
+
nextBtn.style.display = 'none';
|
|
98
|
+
exportBtn.style.display = 'block';
|
|
99
|
+
exportBtn.disabled = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function showError(message) {
|
|
104
|
+
errorEl.textContent = message;
|
|
105
|
+
errorEl.style.display = 'block';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function clearError() {
|
|
109
|
+
errorEl.style.display = 'none';
|
|
110
|
+
errorEl.textContent = '';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function loadIntegrationStatus() {
|
|
114
|
+
if (!creativeId) return;
|
|
115
|
+
|
|
116
|
+
statusEl.textContent = 'Loading...';
|
|
117
|
+
clearError();
|
|
118
|
+
|
|
119
|
+
fetch(`/notion/creatives/${creativeId}/notion_integration`, {
|
|
120
|
+
method: 'GET',
|
|
121
|
+
headers: {
|
|
122
|
+
'X-CSRF-Token': csrfToken(),
|
|
123
|
+
'Content-Type': 'application/json'
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
.then(response => response.json())
|
|
127
|
+
.then(data => {
|
|
128
|
+
console.log('Notion integration status:', data);
|
|
129
|
+
statusEl.textContent = '';
|
|
130
|
+
|
|
131
|
+
// Store the creative title from the API response
|
|
132
|
+
if (data.creative_title) {
|
|
133
|
+
window.notionCreativeTitle = data.creative_title;
|
|
134
|
+
console.log('Creative title from API:', data.creative_title);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (data.connected) {
|
|
138
|
+
workspaceInfo = data.account;
|
|
139
|
+
availablePages = data.available_pages || [];
|
|
140
|
+
|
|
141
|
+
console.log('Available pages:', availablePages);
|
|
142
|
+
|
|
143
|
+
if (workspaceNameEl) {
|
|
144
|
+
workspaceNameEl.textContent = data.account.workspace_name || 'Notion Workspace';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (data.linked_pages && data.linked_pages.length > 0) {
|
|
148
|
+
hasExistingIntegration = true;
|
|
149
|
+
showExistingIntegration(data.linked_pages);
|
|
150
|
+
} else {
|
|
151
|
+
hasExistingIntegration = true;
|
|
152
|
+
if (connectMessage) connectMessage.style.display = 'none';
|
|
153
|
+
if (loginBtn) loginBtn.style.display = 'none';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
updateParentPageSelect(); // Update the select with available pages
|
|
157
|
+
} else {
|
|
158
|
+
hasExistingIntegration = false;
|
|
159
|
+
if (connectMessage) connectMessage.textContent = modal.dataset.loginRequired;
|
|
160
|
+
}
|
|
161
|
+
updateStep();
|
|
162
|
+
})
|
|
163
|
+
.catch(error => {
|
|
164
|
+
console.error('Error loading integration status:', error);
|
|
165
|
+
statusEl.textContent = '';
|
|
166
|
+
showError('Failed to load integration status');
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function showExistingIntegration(linkedPages) {
|
|
171
|
+
if (!existingContainer || !existingList) return;
|
|
172
|
+
|
|
173
|
+
existingList.innerHTML = '';
|
|
174
|
+
linkedPages.forEach(function (page) {
|
|
175
|
+
const li = document.createElement('li');
|
|
176
|
+
const link = document.createElement('a');
|
|
177
|
+
link.href = page.page_url;
|
|
178
|
+
link.target = '_blank';
|
|
179
|
+
link.textContent = page.page_title || 'Untitled Page';
|
|
180
|
+
li.appendChild(link);
|
|
181
|
+
|
|
182
|
+
if (page.last_synced_at) {
|
|
183
|
+
const syncInfo = document.createElement('span');
|
|
184
|
+
syncInfo.textContent = ` (synced ${new Date(page.last_synced_at).toLocaleDateString()})`;
|
|
185
|
+
syncInfo.style.color = 'var(--color-text-secondary)';
|
|
186
|
+
li.appendChild(syncInfo);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
existingList.appendChild(li);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (connectMessage) connectMessage.style.display = 'none';
|
|
193
|
+
if (loginBtn) loginBtn.style.display = 'none';
|
|
194
|
+
if (syncBtn) syncBtn.style.display = 'inline-block';
|
|
195
|
+
if (deleteBtn) deleteBtn.style.display = 'inline-block';
|
|
196
|
+
existingContainer.style.display = 'block';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function loadAvailablePages() {
|
|
200
|
+
// Pages are now loaded with the initial status call
|
|
201
|
+
return Promise.resolve(availablePages);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function updateParentPageOptions() {
|
|
205
|
+
const showParentSelect = document.querySelector('input[name="notion-export-type"]:checked')?.value === 'select-parent';
|
|
206
|
+
|
|
207
|
+
if (parentPageSection) {
|
|
208
|
+
parentPageSection.style.display = showParentSelect ? 'block' : 'none';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (showParentSelect && availablePages.length === 0) {
|
|
212
|
+
loadAvailablePages().then(pages => {
|
|
213
|
+
availablePages = pages;
|
|
214
|
+
updateParentPageSelect();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function updateParentPageSelect() {
|
|
220
|
+
if (!parentPageSelect) return;
|
|
221
|
+
|
|
222
|
+
console.log('Updating parent page select with', availablePages.length, 'pages');
|
|
223
|
+
parentPageSelect.innerHTML = '';
|
|
224
|
+
|
|
225
|
+
if (availablePages.length === 0) {
|
|
226
|
+
const option = document.createElement('option');
|
|
227
|
+
option.value = '';
|
|
228
|
+
option.textContent = 'No pages available or loading...';
|
|
229
|
+
parentPageSelect.appendChild(option);
|
|
230
|
+
} else {
|
|
231
|
+
const defaultOption = document.createElement('option');
|
|
232
|
+
defaultOption.value = '';
|
|
233
|
+
defaultOption.textContent = 'Select a parent page';
|
|
234
|
+
parentPageSelect.appendChild(defaultOption);
|
|
235
|
+
|
|
236
|
+
availablePages.forEach(page => {
|
|
237
|
+
console.log('Adding page option:', page);
|
|
238
|
+
const option = document.createElement('option');
|
|
239
|
+
option.value = page.id;
|
|
240
|
+
option.textContent = page.title || 'Untitled';
|
|
241
|
+
parentPageSelect.appendChild(option);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function updateSummary() {
|
|
247
|
+
if (creativeTitleEl) {
|
|
248
|
+
// Use the creative title from the API response
|
|
249
|
+
const title = window.notionCreativeTitle || 'Current Creative';
|
|
250
|
+
console.log(`Creative title used: "${title}" for ID: ${creativeId}`);
|
|
251
|
+
creativeTitleEl.textContent = title;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (workspaceSummaryEl && workspaceInfo) {
|
|
255
|
+
workspaceSummaryEl.textContent = workspaceInfo.workspace_name || 'Notion Workspace';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const selectedExportType = document.querySelector('input[name="notion-export-type"]:checked')?.value || 'new-page';
|
|
259
|
+
exportType = selectedExportType;
|
|
260
|
+
|
|
261
|
+
if (exportTypeSummaryEl) {
|
|
262
|
+
exportTypeSummaryEl.textContent = selectedExportType === 'new-page' ? 'New page' : 'Subpage';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (selectedExportType === 'select-parent') {
|
|
266
|
+
selectedParentPage = parentPageSelect?.value || null;
|
|
267
|
+
if (parentSummaryEl && parentPageSummaryEl) {
|
|
268
|
+
if (selectedParentPage) {
|
|
269
|
+
const selectedPage = availablePages.find(p => p.id === selectedParentPage);
|
|
270
|
+
parentPageSummaryEl.textContent = selectedPage?.title || 'Selected page';
|
|
271
|
+
parentSummaryEl.style.display = 'block';
|
|
272
|
+
} else {
|
|
273
|
+
parentSummaryEl.style.display = 'none';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
if (parentSummaryEl) parentSummaryEl.style.display = 'none';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function performExport() {
|
|
282
|
+
if (!creativeId) {
|
|
283
|
+
showError(modal.dataset.noCreative);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
exportBtn.disabled = true;
|
|
288
|
+
exportBtn.textContent = 'Exporting...';
|
|
289
|
+
clearError();
|
|
290
|
+
|
|
291
|
+
const requestData = {
|
|
292
|
+
action: 'export'
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (exportType === 'select-parent' && selectedParentPage) {
|
|
296
|
+
requestData.parent_page_id = selectedParentPage;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
console.log('Sending export request:', requestData);
|
|
300
|
+
console.log('Export type:', exportType, 'Selected parent page:', selectedParentPage);
|
|
301
|
+
|
|
302
|
+
fetch(`/notion/creatives/${creativeId}/notion_integration`, {
|
|
303
|
+
method: 'PATCH',
|
|
304
|
+
headers: {
|
|
305
|
+
'X-CSRF-Token': csrfToken(),
|
|
306
|
+
'Content-Type': 'application/json'
|
|
307
|
+
},
|
|
308
|
+
body: JSON.stringify(requestData)
|
|
309
|
+
})
|
|
310
|
+
.then(response => response.json())
|
|
311
|
+
.then(data => {
|
|
312
|
+
if (data.success) {
|
|
313
|
+
statusEl.textContent = modal.dataset.exportSuccess || 'Export started successfully';
|
|
314
|
+
statusEl.style.color = 'green';
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
modal.style.display = 'none';
|
|
317
|
+
resetWizard();
|
|
318
|
+
}, 2000);
|
|
319
|
+
} else {
|
|
320
|
+
showError(data.message || 'Export failed');
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
.catch(error => {
|
|
324
|
+
console.error('Export error:', error);
|
|
325
|
+
showError('Export failed');
|
|
326
|
+
})
|
|
327
|
+
.finally(() => {
|
|
328
|
+
exportBtn.disabled = false;
|
|
329
|
+
exportBtn.textContent = 'Export to Notion';
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function performSync() {
|
|
334
|
+
if (!creativeId) return;
|
|
335
|
+
|
|
336
|
+
syncBtn.disabled = true;
|
|
337
|
+
syncBtn.textContent = 'Syncing...';
|
|
338
|
+
clearError();
|
|
339
|
+
|
|
340
|
+
fetch(`/notion/creatives/${creativeId}/notion_integration`, {
|
|
341
|
+
method: 'PATCH',
|
|
342
|
+
headers: {
|
|
343
|
+
'X-CSRF-Token': csrfToken(),
|
|
344
|
+
'Content-Type': 'application/json'
|
|
345
|
+
},
|
|
346
|
+
body: JSON.stringify({ action: 'sync' })
|
|
347
|
+
})
|
|
348
|
+
.then(response => response.json())
|
|
349
|
+
.then(data => {
|
|
350
|
+
if (data.success) {
|
|
351
|
+
statusEl.textContent = modal.dataset.syncSuccess || 'Sync completed successfully';
|
|
352
|
+
statusEl.style.color = 'green';
|
|
353
|
+
} else {
|
|
354
|
+
showError(data.message || 'Sync failed');
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
.catch(error => {
|
|
358
|
+
console.error('Sync error:', error);
|
|
359
|
+
showError('Sync failed');
|
|
360
|
+
})
|
|
361
|
+
.finally(() => {
|
|
362
|
+
syncBtn.disabled = false;
|
|
363
|
+
syncBtn.textContent = 'Sync to Notion';
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function performDelete() {
|
|
368
|
+
if (!confirm(modal.dataset.deleteConfirm)) return;
|
|
369
|
+
|
|
370
|
+
deleteBtn.disabled = true;
|
|
371
|
+
deleteBtn.textContent = 'Removing...';
|
|
372
|
+
clearError();
|
|
373
|
+
|
|
374
|
+
fetch(`/notion/creatives/${creativeId}/notion_integration`, {
|
|
375
|
+
method: 'DELETE',
|
|
376
|
+
headers: {
|
|
377
|
+
'X-CSRF-Token': csrfToken(),
|
|
378
|
+
'Content-Type': 'application/json'
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
.then(response => response.json())
|
|
382
|
+
.then(data => {
|
|
383
|
+
if (data.success) {
|
|
384
|
+
statusEl.textContent = modal.dataset.deleteSuccess || 'Integration removed successfully';
|
|
385
|
+
statusEl.style.color = 'green';
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
modal.style.display = 'none';
|
|
388
|
+
resetWizard();
|
|
389
|
+
}, 2000);
|
|
390
|
+
} else {
|
|
391
|
+
showError(data.message || 'Deletion failed');
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
.catch(error => {
|
|
395
|
+
console.error('Delete error:', error);
|
|
396
|
+
showError('Deletion failed');
|
|
397
|
+
})
|
|
398
|
+
.finally(() => {
|
|
399
|
+
deleteBtn.disabled = false;
|
|
400
|
+
deleteBtn.textContent = 'Remove link';
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Event listeners
|
|
405
|
+
openBtn.addEventListener('click', function () {
|
|
406
|
+
creativeId = this.dataset.creativeId;
|
|
407
|
+
if (!creativeId) {
|
|
408
|
+
alert(modal.dataset.noCreative);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
modal.style.display = 'flex';
|
|
412
|
+
resetWizard();
|
|
413
|
+
loadIntegrationStatus();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
closeBtn.addEventListener('click', function () {
|
|
417
|
+
modal.style.display = 'none';
|
|
418
|
+
resetWizard();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Listen for OAuth success message from popup window
|
|
422
|
+
window.addEventListener('message', function(event) {
|
|
423
|
+
// Verify origin for security
|
|
424
|
+
if (event.origin !== window.location.origin) return;
|
|
425
|
+
|
|
426
|
+
if (event.data && event.data.type === 'notion_oauth_success') {
|
|
427
|
+
console.log('Received Notion OAuth success message');
|
|
428
|
+
loadIntegrationStatus();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
loginBtn.addEventListener('click', function () {
|
|
433
|
+
console.log('Notion login button clicked');
|
|
434
|
+
const width = parseInt(this.dataset.windowWidth) || 600;
|
|
435
|
+
const height = parseInt(this.dataset.windowHeight) || 700;
|
|
436
|
+
const left = (screen.width - width) / 2;
|
|
437
|
+
const top = (screen.height - height) / 2;
|
|
438
|
+
|
|
439
|
+
const authWindow = window.open('', 'notion-auth-window',
|
|
440
|
+
`width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`);
|
|
441
|
+
|
|
442
|
+
if (authWindow) {
|
|
443
|
+
loginForm.target = 'notion-auth-window';
|
|
444
|
+
loginForm.submit();
|
|
445
|
+
console.log('Auth form submitted to popup window');
|
|
446
|
+
|
|
447
|
+
const checkClosed = setInterval(() => {
|
|
448
|
+
if (authWindow.closed) {
|
|
449
|
+
clearInterval(checkClosed);
|
|
450
|
+
console.log('Auth window closed, reloading integration status');
|
|
451
|
+
setTimeout(() => loadIntegrationStatus(), 1000);
|
|
452
|
+
}
|
|
453
|
+
}, 1000);
|
|
454
|
+
} else {
|
|
455
|
+
loginForm.target = '_blank';
|
|
456
|
+
loginForm.submit();
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
prevBtn.addEventListener('click', function () {
|
|
461
|
+
if (currentStep === 'workspace') {
|
|
462
|
+
currentStep = 'connect';
|
|
463
|
+
} else if (currentStep === 'summary') {
|
|
464
|
+
currentStep = 'workspace';
|
|
465
|
+
}
|
|
466
|
+
updateStep();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
nextBtn.addEventListener('click', function () {
|
|
470
|
+
clearError();
|
|
471
|
+
if (currentStep === 'connect') {
|
|
472
|
+
currentStep = 'workspace';
|
|
473
|
+
updateParentPageOptions();
|
|
474
|
+
} else if (currentStep === 'workspace') {
|
|
475
|
+
updateSummary();
|
|
476
|
+
currentStep = 'summary';
|
|
477
|
+
}
|
|
478
|
+
updateStep();
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
exportBtn.addEventListener('click', performExport);
|
|
482
|
+
if (syncBtn) syncBtn.addEventListener('click', performSync);
|
|
483
|
+
if (deleteBtn) deleteBtn.addEventListener('click', performDelete);
|
|
484
|
+
|
|
485
|
+
// Listen for export type changes
|
|
486
|
+
document.addEventListener('change', function (e) {
|
|
487
|
+
if (e.target.name === 'notion-export-type') {
|
|
488
|
+
updateParentPageOptions();
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Listen for parent page selection
|
|
493
|
+
if (parentPageSelect) {
|
|
494
|
+
parentPageSelect.addEventListener('change', function () {
|
|
495
|
+
selectedParentPage = this.value;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
modal.addEventListener('click', function (e) {
|
|
500
|
+
if (e.target === modal) {
|
|
501
|
+
modal.style.display = 'none';
|
|
502
|
+
resetWizard();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module CollavreNotion
|
|
2
|
+
class NotionExportJob < ApplicationJob
|
|
3
|
+
queue_as :default
|
|
4
|
+
|
|
5
|
+
def perform(creative, notion_account, parent_page_id = nil)
|
|
6
|
+
service = CollavreNotion::NotionService.new(user: notion_account.user)
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
# Export the creative tree to Notion
|
|
10
|
+
link = service.sync_creative(creative, parent_page_id: parent_page_id)
|
|
11
|
+
|
|
12
|
+
Rails.logger.info("Successfully exported creative #{creative.id} to Notion page #{link.page_id}")
|
|
13
|
+
|
|
14
|
+
# You could add broadcast/notification logic here if needed
|
|
15
|
+
# ActionCable.server.broadcast("user_#{notion_account.user.id}", {
|
|
16
|
+
# type: 'notion_export_complete',
|
|
17
|
+
# creative_id: creative.id,
|
|
18
|
+
# page_url: link.page_url
|
|
19
|
+
# })
|
|
20
|
+
rescue NotionError => e
|
|
21
|
+
Rails.logger.error("Notion export failed for creative #{creative.id}: #{e.message}")
|
|
22
|
+
raise e
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
Rails.logger.error("Unexpected error during Notion export for creative #{creative.id}: #{e.message}")
|
|
25
|
+
raise e
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module CollavreNotion
|
|
2
|
+
class NotionSyncJob < ApplicationJob
|
|
3
|
+
queue_as :default
|
|
4
|
+
|
|
5
|
+
def perform(creative, notion_account, page_id)
|
|
6
|
+
service = CollavreNotion::NotionService.new(user: notion_account.user)
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
# Find the existing link
|
|
10
|
+
link = CollavreNotion::NotionPageLink.find_by(
|
|
11
|
+
creative: creative,
|
|
12
|
+
notion_account: notion_account,
|
|
13
|
+
page_id: page_id
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
unless link
|
|
17
|
+
Rails.logger.error("No Notion page link found for creative #{creative.id} and page #{page_id}")
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Update the existing Notion page with children as blocks
|
|
22
|
+
title = ActionController::Base.helpers.strip_tags(creative.description).strip.presence || "Untitled Creative"
|
|
23
|
+
children = creative.children.to_a
|
|
24
|
+
Rails.logger.info("NotionSyncJob: Syncing creative #{creative.id} as page title with #{children.count} children as blocks")
|
|
25
|
+
blocks = children.any? ? NotionCreativeExporter.new(creative).export_tree_blocks(children, 1, 0) : []
|
|
26
|
+
|
|
27
|
+
properties = {
|
|
28
|
+
title: {
|
|
29
|
+
title: [ { text: { content: title } } ]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
service.update_page(page_id, properties: properties, blocks: blocks)
|
|
34
|
+
link.update!(page_title: title)
|
|
35
|
+
link.mark_synced!
|
|
36
|
+
|
|
37
|
+
Rails.logger.info("Successfully synced creative #{creative.id} to Notion page #{page_id}")
|
|
38
|
+
rescue NotionError => e
|
|
39
|
+
Rails.logger.error("Notion sync failed for creative #{creative.id}: #{e.message}")
|
|
40
|
+
raise e
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
Rails.logger.error("Unexpected error during Notion sync for creative #{creative.id}: #{e.message}")
|
|
43
|
+
raise e
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module CollavreNotion
|
|
2
|
+
class NotionAccount < ApplicationRecord
|
|
3
|
+
self.table_name = "notion_accounts"
|
|
4
|
+
|
|
5
|
+
belongs_to :user, class_name: "::User"
|
|
6
|
+
has_many :notion_page_links, class_name: "CollavreNotion::NotionPageLink", dependent: :destroy
|
|
7
|
+
|
|
8
|
+
encrypts :token, deterministic: false
|
|
9
|
+
|
|
10
|
+
validates :notion_uid, :token, presence: true
|
|
11
|
+
validates :notion_uid, uniqueness: true
|
|
12
|
+
|
|
13
|
+
def expired?
|
|
14
|
+
token_expires_at.present? && token_expires_at < Time.current
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module CollavreNotion
|
|
2
|
+
class NotionBlockLink < ApplicationRecord
|
|
3
|
+
self.table_name = "notion_block_links"
|
|
4
|
+
|
|
5
|
+
belongs_to :notion_page_link, class_name: "CollavreNotion::NotionPageLink"
|
|
6
|
+
belongs_to :creative, class_name: "Collavre::Creative"
|
|
7
|
+
|
|
8
|
+
validates :block_id, presence: true, uniqueness: { scope: :notion_page_link_id }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module CollavreNotion
|
|
2
|
+
class NotionPageLink < ApplicationRecord
|
|
3
|
+
self.table_name = "notion_page_links"
|
|
4
|
+
|
|
5
|
+
belongs_to :creative, class_name: "Collavre::Creative"
|
|
6
|
+
belongs_to :notion_account, class_name: "CollavreNotion::NotionAccount"
|
|
7
|
+
has_many :notion_block_links, class_name: "CollavreNotion::NotionBlockLink", dependent: :destroy
|
|
8
|
+
|
|
9
|
+
validates :page_id, :page_title, presence: true
|
|
10
|
+
validates :page_id, uniqueness: true
|
|
11
|
+
|
|
12
|
+
scope :recent, -> { order(last_synced_at: :desc) }
|
|
13
|
+
scope :synced, -> { where.not(last_synced_at: nil) }
|
|
14
|
+
|
|
15
|
+
def mark_synced!
|
|
16
|
+
touch(:last_synced_at)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|