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.