Action

Add Lines to List

Posted by @JimS, Last update 2 months ago

Add Lines to List, v3.0


Attribution

This action was inspired by Add to list plus by @dchar. Add to List plus is based on Add to List by @agiletortoise.

In addition to the inspiration, the Add to List plus JavaScript code that presents a dialog to select an existing list, or creates a new one, is used in this action. The viewListAfterSuccess code has similarly been copied.

The action dialog is created and evaluated using MGCheckListPrompt, by @mattgemmell.

Help regarding the Preview (v1.1) was provided by @sylumer, @agiletortoise, and @FlohGro. For more information: End an action, yet still honor the AFTER SUCCESS configuration? - Actions - Help & Questions - Drafts Community

@sylumer suggested that this action be callable from another action. He also provided the necessary guidance.

Overview

This action can be used to add lines from a source draft to target list, i.e., another draft (formatted using markdown). The target list can be: 1) hard-coded, 2) selected from all current lists, or 3) created by this action.

Target lists are drafts that are archived and tagged with a configured value (by default the tag is list). Source drafts can be anywhere in the Drafts application.

Features

  • All lines from the source draft are added individually to the target list.

  • A time prefix (YYYY-MM-DD HH:MM: or HH:MM:) can be included with each added line.

  • Added lines can be appended or prepended.

  • A horizontal rule (—) can be added before each group of lines added to the target draft.

  • A date line (YYYY-MM-DD, DayOfWeek) can be included with each group of lines added to the target draft. If this option is selected, the added date and lines are enclosed by horizontal rules. (This action includes logic to prevent horizontal rules on two consecutive lines.)

  • After the target list is updated, it can be displayed in the Drafts editor and/or previewed (i.e., the markdown is rendered). Note: To preview a target list without changing it, run this action with an empty draft in the Drafts editor.

  • Blank lines above and below text in the source draft can be used or ignored. Similarly, interleaving blank lines can be used or ignored.

  • Several of the options mentioned above can be changed at runtime using an action dialog.

  • The default options can be superseded by using an action that calls this action via the Include Action step. The override options in the caller are the same as the options within this action except with an added leading underscore. For example _listTag can be used to override the option listTag. For an example caller, see: Add Lines to List (caller) | Drafts Directory.

Tested With

  • Drafts (iOS), Version 42.2.1 (415) // iOS Version 17.2.1/iPhone 15 Pro Max

  • Drafts (macOS), version 14.6.9 (14D100, Apple Silicon, sandboxed) // Sonoma 14.2.1 (23C71)/MacBookPro18,2

Version History

1.0 - initial version

1.1
a. If the source draft is empty no content is added to the target list but it can still be loaded and/or previewed.
b. The method used to to preview the target list has been changed such that the action ends nornally; thus the action After Success configuration is honored.

2.0
a. The default options can now be superseded by using an action that calls this action via the Include Action step. The override options in the caller are the same as the options within this action except with an added leading underscore. For example _listTag can be used to override the option listTag. For an example caller, see: Add Lines to List (caller) | Drafts Directory.
b. The list dialog prompts and logic were refined.
c. Changed option excludeBlankLines to excludeInnerBlankLines.
d. The icon color was changed from green to red. By default, the icons of calling actions are green.
e. Bug fix: When addCurrentDateToEachBlock = true and prependLines = false, the closing horizontal rule was prepended. It is now appended.

3.0
Changed option addCurrentDateToEachLine to addCurrentTimeToEachLine. If true, a YYYY-MM-DD HH:MM: or a HH:MM: prefix is added to each line before it is added to the list. If option addCurrentDateToEachBlock = true, it will be the latter, HH:MM:.

Drafts Community Post

ACTION: Add Lines to List, v3.0 - Actions - Share What You’ve Made - Drafts Community

Required Library

MGCheckListPrompt | Drafts Directory

Steps

  • includeAction

    name
    MGCheckListPrompt Library
  • script

    //
    // The tag used for target lists.
    //
    // Note: The target lists must also be archived.
    //
    let listTag = 'list';
    
    //
    // The target list that will be updated. If set to '', the action
    // will search for all archived drafts that include a tag=listTag.
    // A dialog will appear that includes these drafts and an
    // additional option to specify a new list.
    // 
    let theListTitle = '';
    
    //
    // The markdown heading level for target lists.
    //
    let newListHeading = '##';
    
    // -------------------------------------------------------
    // Boolean options below can be displayed (and changed) at
    // runtime if promptOptions = true
    // -------------------------------------------------------
    
    //
    // If `true`, leading and trailing blank lines are ignored
    // as the displayed draft is processed for addition to the list.
    //
    let trimDraft = true;
    
    //
    // If `true`, blank lines after the top text line and before 
    // the bottom text line are ignored when the displayed draft
    // is processed for addition to the list.
    //
    let excludeInnerBlankLines = true;
    
    //
    // If `true`, appends/prepends `---` if the existing adjacent 
    // line in the list is not `---`.
    //
    let addHorizontalRule = false;
    
    //
    // If `true`, appends/prepends `---` (if the existing adjacent 
    // line in the list is not `---`), appends/prepends 
    // YYYY-MM-DD DayOfWeek, appends/prepends `---`.
    //
    let addCurrentDateToEachBlock = true;
    
    //
    // If `true`, lines from the displayed draft are prepended
    // to the list; otherwise lines are appended.
    //
    let prependLines = true;
    
    //
    // If `true`, a `YYYY-MM-DD HH:MM: ` or a `HH:MM: ` prefix is 
    // added to each line before it is added to the list. It will
    // be the latter if addCurrentDateToEachBlock = `true`.
    //
    let addCurrentTimeToEachLine = false;
    
    //
    // If `true`, the target list is loaded into the Drafts Editor
    // after it is updated.
    //
    let viewListAfterSuccess = false;
    
    //
    // If `true`, the target list is loaded into the Drafts Editor 
    // and the list is previewed using 'HTML Preview'.
    //
    let previewListAfterSuccess = true;
    
    //
    // If `true`, the action displays a dialog that includes boolean 
    // options: trimDraft, excludeBlankLines, addHorizontalRule,
    // addCurrentDateToEachBlock, prependLines, addCurrentDateToEachLine,
    // viewListAfterSuccess, previewListAfterSuccess.
    //
    let promptOptions = true;
    
  • script

    // If this action is "called" by another action, check if any of the
    // options have been specified by the calling action.
    
    if (typeof _listTag !== 'undefined') listTag = _listTag;
    
    if (typeof _theListTitle !== 'undefined') theListTitle = _theListTitle;
    
    if (typeof _trimDraft !== 'undefined') trimDraft = _trimDraft;
    
    if (typeof _excludeInnerBlankLines !== 'undefined') excludeInnerBlankLines = _excludeInnerBlankLines;
    
    if (typeof _addHorizontalRule !== 'undefined') addHorizontalRule = _addHorizontalRule;
    
    if (typeof _addCurrentDateToEachBlock !== 'undefined') addCurrentDateToEachBlock = _addCurrentDateToEachBlock;
    
    if (typeof _prependLines !== 'undefined') prependLines = _prependLines;
    
    if (typeof _addCurrentTimeToEachLine !== 'undefined') addCurrentTimeToEachLine = _addCurrentTimeToEachLine;
    
    if (typeof _viewListAfterSuccess !== 'undefined') viewListAfterSuccess = _viewListAfterSuccess;
    
    if (typeof _previewListAfterSuccess !== 'undefined') previewListAfterSuccess = _previewListAfterSuccess;
    
    if (typeof _promptOptions !== 'undefined') promptOptions = _promptOptions;
  • script

    // Refer to the action Description.
    
    (() => {
    
    	const HORIZONTAL_RULE = "---";
    
    	const msgTitleInfo = "💡 Information 💡";
    	const msgTitleWarn = "⚠️ Warning ⚠️";
    	const msgTitleError = "❌ Error ❌";
    
    	const msgbox = (strTitle, strMsg) => {
    
    		let promptMsg = Prompt.create();
    		promptMsg.title = strTitle;
    		promptMsg.message = strMsg;
    		promptMsg.addButton("OK");
    		promptMsg.isCancellable = false;
    		promptMsg.show();
    		return;
    
    	};
    
    	const msgInfo = (strMsg) => msgbox(msgTitleInfo, strMsg);
    	const msgWarn = (strMsg) => msgbox(msgTitleWarn, strMsg);
    	const msgError = (strMsg) => msgbox(msgTitleError, strMsg);
    
    	// const listWorkspaceTabs = ["inbox", "flagged", "archive"];
    	const listWorkspaceTabs = ["archive"];
    
    	const getLists = () => {
    
    		return listWorkspaceTabs
    			.map(s => Draft.query("", s, [listTag]))
    			.filter(arr => arr !== undefined)
    			.reduce((acc, v) => acc.concat(v), [])
    			.reduce((o, d) => {
    				const m = d.content.match(/#+ ([^\n]+)/);
    				if (m) { o[m[1].trim()] = d; }
    				return o;
    			}, {});
    
    	};
    
    	const sortcase = (xs) => {
    
    		return xs.sort((a,b) => {
    			return a.toLowerCase().localeCompare(b.toLowerCase());
    		})
    
    	};
    
    	const split = (s, delim) => {
    
    		const i = s.indexOf(delim);
    		return [
    			s.substr(0, (i>=0) ? i : s.length),
    			(i>=0) ? s.substr(i) : ''
    		]
    
    	};
    	
    	const countLines = str => {
    	
    		// str = '', returns 0
    		// str = '\n' returns 1
    		// str = ' ' returns 1
    		// str = ' \n' return 1
    		// str = ' \n ' returns 2
    		// etc.
    
    		if (str === '') {
    			return 0;
    		}
    		let lineCount = str.split(/\r\n|\r|\n/).length;
    		if (str.endsWith('\n')) {
    			lineCount--;
    		}
    		return lineCount;
    
    	};
    
    	const hhmm = () => {
        	const currentDate = new Date();
        	const timeString = currentDate.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit' });
        	return timeString;
    	};
    
    	const yyyymmdd_dow = () => {
    
    		const currentDate = new Date();
    		const dateString = currentDate.toISOString().split('T')[0];
    		const dayOfWeek = new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(currentDate);
    		return(`${dateString}; ${dayOfWeek}`);
    
    	};
    
    	const yyyymmdd = () => {
    
    		const currentDate = new Date();
    		const dateString = currentDate.toISOString().split('T')[0];
    		return(`${dateString}`);
    
    	};
    	
    	const yyyymmddhhmm = () => {
    	
        	const currentDate = new Date();
        	const dateString = currentDate.toISOString().split('T')[0];
        	const timeString = currentDate.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit' });
        	return `${dateString} ${timeString}`;
    
    	};
    
    
    	const append = (txtBlock, txtLineToAppend) => {
    
    		return txtBlock + "\n" + txtLineToAppend;
    
    	};
    
    	const prepend = (txtBlock, txtLineToPrepend) => {
    
    		const lines = txtBlock.split('\n');
    
    		if (lines.length == 1) {
    			// Case 1: If the text block contains only one line
    			lines.push('', txtLineToPrepend);
    
    	  	} else if (lines[1].trim() !== '') {
    			// Case 2: If the second line is not blank
    			lines.splice(1, 0, txtLineToPrepend);
    
    	  	} else {
    			// Case 3: If there are contiguous blank lines after the first line
    			let index = 1;
    			while (index < lines.length && lines[index].trim() === '') {
    		  		index++;
    			}
    			lines.splice(index, 0, txtLineToPrepend);
    	  	}
    
    	  return lines.join('\n');
    
    	};
    	
    	const trimInnerBlankLines = str => {
    	
        	let lines = str.split('\n');
        	
        	let firstNonBlankLineIndex = lines.findIndex(line => line.trim() !== '');
        	
        	let lastNonBlankLineIndex = lines.slice().reverse().findIndex(line => line.trim() !== '');
       	 	
       	 	lastNonBlankLineIndex = lastNonBlankLineIndex >= 0 ? lines.length - 1 - lastNonBlankLineIndex : lastNonBlankLineIndex;
    
    		return lines.filter((line, index) => index < firstNonBlankLineIndex || index > lastNonBlankLineIndex || line.trim() !== '').join('\n');
    	
    	};
    
    	const lineMatchTop = (txtBlock, txtLineSearch) => {
    
    		const lines = txtBlock.split('\n');
    
    		if (lines.length == 1) {
    			// Case 1: If the text block contains only one line
    			return false;
    
    	  	} else if (lines[1].trim() !== '') {
    			// Case 2: If the second line is not blank
    			lineToCheck = lines[1];
    
    	  	} else {
    			// Case 3: If there are contiguous blank lines after the first line
    			let index = 1;
    			while (index < lines.length && lines[index].trim() === '') {
    		  		index++;
    			}
    			lineToCheck = lines[index];
    	  	}
    		
    		return lineToCheck === txtLineSearch ? true : false;		
    
    	};
    	
    	const lineMatchBottom = (textBlock, txtLineSearch) => {
    
        		const lines = textBlock.split('\n');
        		return lines[lines.length - 1] === txtLineSearch ? true : false;
    
    	};
    	
    	const getList = (theListTitle) => {	
    
    		// If the specified list is found, includes a list tag, and
    		// is archived, return the list 'draft object'.
    	
    		const draftTitle = newListHeading + " " + theListTitle;
    	
    		const dTitleMatch = Draft.queryByTitle(draftTitle);
    
    		const dArchiveMatch = dTitleMatch.filter(obj => obj.isArchived === true);
    		
    		const dFullMatch = dArchiveMatch.filter(obj => obj.hasTag(listTag) === true);
    
    		switch(dFullMatch.length) {
    
    		  case 0:
    
    			var sError = "Specified list (" + draftTitle + ") not found, does not include tag '" + listTag + "', or is not archived.";
    			msgError(sError);
    			break;
    
    		  case 1:
    
    			return dFullMatch[0];
    			break;
    
    		  default:
    
    			var sError = dFullMatch.length + " list matches '" + draftTitle + "', tag '" + listTag + "', and archived.";
    			msgError(sError);
    			return;
    
    		}        	
    
    	};
    
    	const pickOrCreateList = () => {
    
    		// Generate an dialog to pick an existing list or create
    		// a new one.
    		
    		const lists = getLists();
    
    		let d = {};	
    
    		if (Object.getOwnPropertyNames(lists).length !== 0) {
    
    			// prompt to select a list
    			let p = Prompt.create();
    	
    			p.title = 'Pick or Create a New List';
    
    			sortcase(Object.keys(lists)).map(c => p.addButton(c));
    
    			// Give the user an option to create a new list
    			p.addButton('Create a New List');
    
    			if (!p.show()) {
    				context.fail();
    				return;
    			}
    
    			d = lists[p.buttonPressed];
    		}
    
    		if (typeof d === 'undefined') {
    
    			// Generate an dialog to specify the new list name
    
    			p = Prompt.create();
    			p.title = 'Create New List';
    			p.addTextField('listName', 'List Name:', '');
    		 	p.addButton('OK');
    
    			if (!p.show()) {
    				context.fail();
    				return;
    			}
    
    			const name = p.fieldValues['listName'].trim();
    			if (name.length == 0) {
    				app.displayErrorMessage("Invalid list name");
    				context.fail();
    				return;
    			}
    
    			// Create the new list.
    
    			d = Draft.create();
    			d.content = newListHeading + ` ${name} \n`;
    			d.addTag(listTag);
    			d.isArchived = true;
    			d.update();
    
    		}
    			
    		return d;
    
    	};
    	
    	const optionsDialog = () => {
    
    		// Create an MGCheckListPrompt.
    		var prompt = new MGCheckListPrompt();
    		prompt.message = ""; // Removes header.
    		prompt.addItems([
    
    			{separator: true,
    			 title: "Source Options"},
    
    			{title: "Trim Draft",
    			 description: "Exclude leading and trailing blank lines",
    			 selected: trimDraft},
    
    			{title: "Exclude Inner Blank Lines",
    			 description: "Exclude blank lines between the first and last text lines",
    			 selected: excludeInnerBlankLines},
    
    			{separator: true,
    			 title: "List Options"},
    
    			{title: "Separator",
    			 description: "Add a Horizontal Rule (---) before adding the draft lines",
    			 selected: addHorizontalRule},
    
    			{title: "Add Today's Date to Each Block",
    			 description: "Add the date (YYYY-MM-DD) to the beginning to each block",
    			 selected: addCurrentDateToEachBlock},
    
    			{title: "Prepend Lines",
    			 description: "Prepend draft lines to list; otherwise append",
    			 selected: prependLines},
    
    			{title: "Add Today's Date/Time to Each Line",
    			 description: "Add the date/time (YYYY-MM-DD HH:MM) to the beginning to each line",
    			 selected: addCurrentTimeToEachLine},
    
    			{title: "View List",
    			 description: "If line(s) were successfully added, display the list in the editor",
    			 selected: viewListAfterSuccess},
    
    			{title: "Preview List",
    			 description: "If line(s) were successfully added, preview the list",
    			 selected: previewListAfterSuccess},
    
    		]);
    
    		var selectedItems = prompt.show();
    
    		if (prompt.didShow) {
    
    			if (selectedItems != null) {
    
    				trimDraft = selectedItems.includes(0);
    				excludeInnerBlankLines = selectedItems.includes(1);
    				addHorizontalRule = selectedItems.includes(2);
    				addCurrentDateToEachBlock = selectedItems.includes(3);
    				prependLines = selectedItems.includes(4);
    				addCurrentTimeToEachLine = selectedItems.includes(5);
    				viewListAfterSuccess = selectedItems.includes(6);
    				previewListAfterSuccess = selectedItems.includes(7);
    
    			} else {
    
    				return null;
    
    			}
    
    		} else {
    
    			app.displayErrorMessage("Something went wrong.");
    			return null;
    
    		}
    
    	};
    
    	const previewList = (theList) => {
    
    		// Per 'Dark-Light Preview II' by @dmetzcher
    		// [Dark-Light Preview II | Drafts Directory](https://directory.getdrafts.com/a/1H6)
    		// create theme sensitive css for preview
    		if (app.themeMode == 'dark') {
    			var css = "body { background: #222; color: #ddd; }";
    		}
    		else {
    			var css = "body { background: #fff; color: #444; }";
    		}
    		draft.setTemplateTag("bodystyle", css);
    
    		const theMarkdownList = theList.content.replace(/\~\~(.*)\~\~/gm, '\<del\>$1\<\/del\>');
    		draft.setTemplateTag("list_markdown", theMarkdownList);
    
    	};
    
    // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    	
    	const main = () => {
    
    		// draft == the source draft with the new item(s)
    		if (draft.hasTag(listTag)) {
    		
    			msgError('This draft will not be processed because it is already a list.');
    			context.fail();
    			return;
    			
    		}
    
    		if (theListTitle !== '') { var d = getList(theListTitle); }
    		else { var d = pickOrCreateList(); }
    
    		if (d === undefined) {
    			context.fail();
    			return;
    		}
    
    		draftText = draft.content;
    		
    		if (countLines(draftText) < 1) {
    		
    			viewListAfterSuccess = false;
    			previewListAfterSuccess = true;
    
    		} else {
    		
    			if (promptOptions) { 
    				if (optionsDialog() === null) { 
    					context.cancel();
    					return;
    				}
    			}
    
    			if (trimDraft) { draftText = draft.content.trim(); }
    
    			if (draftText !== '') {
    			
    				draftText = draftText.replace(/\n$/, '');
    				
    				if (excludeInnerBlankLines) { draftText = trimInnerBlankLines(draftText); }
    
    				let lines = draftText.split('\n');
    
    				if (addCurrentTimeToEachLine && addCurrentDateToEachBlock) { 
    					prefix = `- [ ] ` + hhmm() + `: `;
    				} else if (addCurrentTimeToEachLine) {
    					prefix = `- [ ] ` + yyyymmddhhmm() + `: `;
    				} else { 
    					prefix = `- [ ] `; 
    				}
    
    				if (prependLines) {
    
    					if ( (addHorizontalRule || addCurrentDateToEachBlock) && !lineMatchTop(d.content, HORIZONTAL_RULE) ) {
    
    						 new_line = HORIZONTAL_RULE;
    						 d.content = prepend(d.content, new_line);
    						 d.update();
    
    					}
    
    					let reversedLines = lines.reverse();
    
    					reversedLines.forEach(line => {
    						new_line = `${prefix}${line}`;
    						d.content = prepend(d.content, new_line);
    						d.update();
    					});
    
    					if (addCurrentDateToEachBlock) {
    
    							 d.content = prepend(d.content, yyyymmdd_dow());
    							 d.update();
    
    							 new_line = HORIZONTAL_RULE;
    							 d.content = prepend(d.content, new_line);
    							 d.update();
    
    					}
    
    				} else {
    
    					if ((addHorizontalRule || addCurrentDateToEachBlock) && !lineMatchBottom(d.content, HORIZONTAL_RULE)) {
    
    						new_line = HORIZONTAL_RULE;
    						d.content = append(d.content, new_line);
    						d.update();
    
    					}
    
    					if (addCurrentDateToEachBlock) {
    
    							 d.content = append(d.content, yyyymmdd_dow());
    							 d.update();
    
    					}
    
    					lines.forEach(line => {
    						new_line = `${prefix}${line}`;
    						d.content = append(d.content, new_line);
    						d.update();
    					});
    
    					if (addCurrentDateToEachBlock) {
    
    							 new_line = HORIZONTAL_RULE;
    							 d.content = append(d.content, new_line);
    							 d.update();
    
    					}
    	
    				}
    				
    			}			
    
    		} 
    					
    		if (viewListAfterSuccess) { 
    			editor.load(d); 
    		}
    		else { 
    			if (!previewListAfterSuccess && draftText.length > 0) {
    				app.displaySuccessMessage("List updated"); 
    			}
    		}
    
    		if (previewListAfterSuccess) {
    			previewList(d);
    		}
    					
    	};
    
    	main();
    
    })();
  • defineTemplateTag

    name
    preview_css
    template
    @charset "utf-8";
    
    html { 
    	font-size:100%;
    	font-family: "Helvetica Neue", "Helvetica", sans-serif;
    }
    body {
    	margin:0;
    	padding:1em;
    }
    [[bodystyle]]
    @media (max-device-width: 480px) { 
    
    } 
    @media (min-device-width: 481px) { 
    	body {
    		margin:auto;
    		max-width:600px;
    	} 
    }
    
    blockquote {
    	font-style: italic;
    }
    
    code, pre {
        border-radius: 3px;
        padding: .5em;
       color: inherit;
    }
    
    table {
      margin: 1em 0;
      border: 1px solid #aaa;
      border-collapse: collapse;
    }
    
    th {
      padding:.25em .5em;
      border: 1px solid #ccc;  
    }
    
    td {
      padding:.25em .5em;
      border: 1px solid #ccc;
    }
  • defineTemplateTag

    name
    htmlpreview
    template
    
    
    	
    		Preview
    		
    		
    	
    	
    
    %%[[list_markdown]]%%
    
    	
    
  • script

    if (previewListAfterSuccess) {
    	const htmlpreview = draft.getTemplateTag("htmlpreview")
    	const theHtml = draft.processTemplate(htmlpreview);
    
    	const hpObj = HTMLPreview.create();
    	hpObj.show(theHtml);
    }

Options

  • After Success Nothing
    Notification Info
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.