Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'feat: document duplicate button' (#103) from feat/doc-duplicate into main

scott 70d4b311 5feda519

+53
+2
src/css/app.css
··· 492 492 } 493 493 494 494 .doc-item-delete, 495 + .doc-item-duplicate, 495 496 .doc-item-move { 496 497 opacity: 0; 497 498 transition: opacity var(--transition-fast); 498 499 } 499 500 .doc-item:hover .doc-item-delete, 501 + .doc-item:hover .doc-item-duplicate, 500 502 .doc-item:hover .doc-item-move { opacity: 1; } 501 503 502 504 /* Star button */
+51
src/landing.ts
··· 386 386 <span class="doc-item-type">${doc.type}</span> 387 387 <span class="doc-item-date">${date}</span> 388 388 <button class="btn-icon doc-item-move" data-id="${doc.id}" title="Move to folder">&#128193;</button> 389 + <button class="btn-icon doc-item-duplicate" data-id="${doc.id}" title="Duplicate">&#10697;</button> 389 390 <button class="btn-icon doc-item-delete" data-id="${doc.id}" title="Move to trash">&#10005;</button> 390 391 </a>`; 391 392 } ··· 436 437 e.preventDefault(); 437 438 e.stopPropagation(); 438 439 showMoveModal(btn.dataset.id); 440 + }); 441 + }); 442 + 443 + // Duplicate handlers 444 + docListEl.querySelectorAll('.doc-item-duplicate').forEach(btn => { 445 + btn.addEventListener('click', async (e) => { 446 + e.preventDefault(); 447 + e.stopPropagation(); 448 + const id = btn.dataset.id; 449 + const originalDoc = allDocs.find(d => d.id === id); 450 + if (!originalDoc) return; 451 + 452 + try { 453 + // Create new document of same type (copies encrypted name so duplicate has same title) 454 + const res = await fetch('/api/documents', { 455 + method: 'POST', 456 + headers: { 'Content-Type': 'application/json' }, 457 + body: JSON.stringify({ type: originalDoc.type, name_encrypted: originalDoc.name_encrypted }), 458 + }); 459 + const { id: newId } = await res.json(); 460 + 461 + // Copy the snapshot (encrypted blob — no need to decrypt/re-encrypt) 462 + const snapRes = await fetch(`/api/documents/${id}/snapshot`); 463 + if (snapRes.ok) { 464 + const blob = await snapRes.blob(); 465 + await fetch(`/api/documents/${newId}/snapshot`, { 466 + method: 'PUT', 467 + body: blob, 468 + }); 469 + } 470 + 471 + // Copy the encryption key so the user can decrypt the copy 472 + const keys = JSON.parse(localStorage.getItem('tools-keys') || '{}'); 473 + if (keys[id]) { 474 + keys[newId] = keys[id]; 475 + localStorage.setItem('tools-keys', JSON.stringify(keys)); 476 + } 477 + 478 + // If we're inside a folder, assign the duplicate to the same folder 479 + if (currentFolderId) { 480 + folderAssignments = moveToFolder(folderAssignments, newId, currentFolderId); 481 + localStorage.setItem('tools-folder-assignments', JSON.stringify(folderAssignments)); 482 + } 483 + 484 + // Reload document list from server to pick up the new doc 485 + await loadDocuments(); 486 + showToast('Document duplicated'); 487 + } catch { 488 + showToast('Failed to duplicate document', 4000, true); 489 + } 439 490 }); 440 491 }); 441 492 }