Action
Find Draft by UUID
Posted by givens,
Last update
6 days ago
Paste a UUID, a partial, or a drafts:// link — it finds the draft and opens it.
Steps
-
script
// ============================================================================ // Drafts action: "Find Draft by UUID" // Paste a UUID, a partial, or a drafts:// link — it finds the draft and opens it. // ---------------------------------------------------------------------------- // Motivation: a UUID handed over in conversation is unusable inside Drafts. There // is no built-in "go to this UUID". This closes that gap. // // Accepts, interchangeably: // DB99B636-7E98-4249-9D7A-E45C39501E1B full UUID (any case) // drafts://open?uuid=DB99B636-7E98-... a link — cropped to the UUID // x-callback-url://drafts/open?uuid=FC5E.. ditto, params after it ignored // DB99B6 a partial — matched as a substring // "the spec is DB99B6" prose around it is ignored // // Partial search covers EVERY folder including trash and archive, because the // point is to find a draft you have lost track of. Full-UUID lookup uses // Draft.find(), which is folder-agnostic already. // // Bind it to a keyboard shortcut. Seeds itself from the clipboard, so the usual // flow is: copy the UUID -> run -> Enter. // JavaScriptCore-safe: no require / fs / npm / HTTP. // ============================================================================ const MAX_BUTTONS = 20; // Prompt gets unusable past ~20; longer lists go to text // ---- key extraction ------------------------------------------------------- // Returns { key, partial } or null. `partial:false` means an exact UUID lookup. function extractKey(raw){ if(raw===null||raw===undefined) return null; const s=String(raw).trim(); if(!s) return null; // A bare UUID, a drafts:// link, an x-callback URL, or a UUID pasted mid-sentence // all reduce to the same thing: the first UUID-shaped run. Try that first. const full=s.match(/[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/); if(full) return {key:full[0].toUpperCase(), partial:false}; // No full UUID: treat the input as a fragment. An explicit uuid= wins; otherwise // take the longest hex-ish run, which survives surrounding prose and punctuation. // (Stripping non-hex characters globally would NOT work — "drafts://open" is // itself mostly hex letters and would corrupt the fragment.) let frag=null; const m=s.match(/uuid=([0-9A-Fa-f-]+)/i); if(m) frag=m[1]; else { const runs=s.match(/[0-9A-Fa-f-]{4,}/g)||[]; for(const r of runs) if(!frag||r.length>frag.length) frag=r; } if(!frag) return null; frag=frag.replace(/^-+|-+$/g,""); if(!frag) return null; return {key:frag.toUpperCase(), partial:true}; } // ---- search --------------------------------------------------------------- function findAll(key, partial){ if(!partial){ const d=Draft.find(key); return d?[d]:[]; } const out=[], seen={}; // "all" omits trash in Drafts' query filter, so trash is queried separately — // a draft you cannot find is exactly the one that might be sitting in it. for(const folder of ["all","trash"]){ let batch=[]; try { batch=Draft.query("", folder, [], [], "modified", true, false); } catch(e){ batch=[]; } for(const d of batch){ const u=String(d.uuid).toUpperCase(); if(seen[u]) continue; if(u.indexOf(key)>-1){ seen[u]=true; out.push(d); } } } // Prefix matches first — a fragment is nearly always the start of a UUID. out.sort((a,b)=>{ const pa=String(a.uuid).toUpperCase().indexOf(key)===0?0:1; const pb=String(b.uuid).toUpperCase().indexOf(key)===0?0:1; return pa-pb; }); return out; } // ---- display -------------------------------------------------------------- function where(d){ if(d.isTrashed) return "trash"; if(d.isArchived) return "archive"; if(d.isFlagged) return "flagged"; return "inbox"; } function titleOf(d){ let t=String(d.title||"").trim(); if(!t) t="(untitled)"; return t.length>52 ? t.slice(0,49)+"..." : t; } function label(d,i){ return (i+1)+". "+titleOf(d)+" ["+String(d.uuid).slice(0,8)+" · "+where(d)+"]"; } function detail(d){ return titleOf(d)+"\n"+ " uuid "+d.uuid+"\n"+ " link drafts://open?uuid="+d.uuid+"\n"+ " folder "+where(d)+ (d.tags&&d.tags.length?"\n tags "+d.tags.join(", "):"")+ "\n changed "+String(d.modifiedAt).slice(0,24); } // ============================================================================ // MAIN // ============================================================================ (function main(){ // Seed from the clipboard only if it actually holds something UUID-shaped — // otherwise the field opens prefilled with junk you have to clear first. let seed=""; try { const c=app.getClipboard(); if(c && extractKey(c)) seed=String(c).trim(); } catch(e){} const p=Prompt.create(); p.title="Find Draft by UUID"; p.message="Full UUID, a partial, or a drafts:// link. Surrounding text is ignored."; p.addTextField("q","UUID / partial / link", seed); p.addButton("Find"); p.addButton("Cancel"); if(!p.show() || p.buttonPressed==="Cancel"){ context.cancel("cancelled"); return; } const parsed=extractKey(p.fieldValues["q"]); if(!parsed){ alert("Nothing UUID-shaped in:\n\n"+String(p.fieldValues["q"]||"(empty)")); context.cancel("no key"); return; } const hits=findAll(parsed.key, parsed.partial); if(hits.length===0){ alert((parsed.partial?"No draft whose UUID contains:":"No draft with UUID:")+ "\n\n"+parsed.key+ "\n\nSearched every folder, trash included."+ (parsed.partial?"":"\n\nIf you meant a partial, pass fewer characters — a full-length\nUUID is looked up exactly and will not fuzzy-match.")); context.cancel("no match"); return; } if(hits.length===1){ const d=hits[0]; editor.load(d); app.displaySuccessMessage(titleOf(d)); alert(detail(d)); return; } if(hits.length>MAX_BUTTONS){ alert(hits.length+" drafts match \""+parsed.key+"\" — too many to pick from.\n"+ "Add a few more characters.\n\nFirst "+MAX_BUTTONS+":\n\n"+ hits.slice(0,MAX_BUTTONS).map((d,i)=>label(d,i)).join("\n")); context.cancel("too many"); return; } const pick=Prompt.create(); pick.title=hits.length+" matches"; pick.message="\""+parsed.key+"\" matches "+hits.length+" drafts."; // Index-prefixed so labels stay unique even when two drafts share a title — // the button string is the only thing Drafts hands back. const labels=hits.map((d,i)=>label(d,i)); for(const l of labels) pick.addButton(l); pick.addButton("Cancel"); if(!pick.show() || pick.buttonPressed==="Cancel"){ context.cancel("cancelled"); return; } const idx=labels.indexOf(pick.buttonPressed); if(idx<0){ context.cancel("no selection"); return; } editor.load(hits[idx]); app.displaySuccessMessage(titleOf(hits[idx])); alert(detail(hits[idx])); })();
Options
-
After Success Default Notification Info Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.