Automatically flag tasks in specific projects/contexts according to due/defer date


#21

Cool, those both work- i’m going to play around with these now. Thanks again for all of the help unlocked! Cant say how much i appreciate you taking the time to answer all my questions and make these awesome scripts!


#22

Awesome, Dan. You’re welcome! I’m glad it worked for you. I’m passionate about scripting, so I enjoyed it.


#23

Hi Unlocked,

Is there a script to that will flag any item in any project that is “due soon”?
I currently have my “due soon” set to today, but I know others have it set differently.

Thank you for your help!


#24

Of course, tell me if this works for you.

-- unlocked2412
-- This script toggles flag status of every task whose due date is today
-- based on blnFlagged property and is in the range specified in
-- dueSoon property.

property blnFlagged : true
property dueSoon : 1

on run
	set dateNow to (current date)
	set dateStart to calculateStartDate(dateNow)
	set dateEnd to dateStart + (dueSoon * days)
	toggleTaskFlag(dateStart, dateEnd)
end run

-----------------------------------------------------------------------

on toggleTaskFlag(dateStart, dateEnd)
	tell application "OmniFocus"
		set oDoc to front document
		set flagged of every flattened task of oDoc whose (due date > dateStart) and (due date < dateEnd) to blnFlagged
	end tell
end toggleTaskFlag

on calculateStartDate(theDate)
	set newDate to theDate
	tell newDate
		set its hours to 0
		set its minutes to 0
		set its seconds to 0
	end tell
	return newDate
end calculateStartDate



#25

@unlocked2412
I’m dying over this script and its potential to make my life a thousand times easier!!, but really struggling to adapt it to what I’m trying to do. Any chance you could offer a revision that would do the following:

Add “A” tag to tasks who are flagged AND have a due date within “X” days
Add “B” tag to tasks who have a due date within “X” days, but are not flagged
Add “C” tag to tasks who are flagged with no due date
Add “D” tag to tasks who have neither flagged nor have a due date

I attempted, but I couldn’t even get it to add a tag for any of the criteria ><. But I’m a complete noob =-(


#26

I have a deadline approaching, will look into it in a couple of days.


#27

Thank you!!!


#28

Here is a first draft of a script (JXA) that accomplishes what you want on selected tasks.
If you run it in Script Editor, set language tab to JavaScript.

// @unlocked2412
(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        // dateDifferenceInDays :: Date -> Date -> Int
        const dateDifferenceInDays = (a, b) => {
            const _MS_PER_DAY = 1000 * 60 * 60 * 24
            // Discard the time and time-zone information.
            const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
            const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());

            return Math.floor((utc2 - utc1) / _MS_PER_DAY);
        }

        // isFlagged :: OF Task -> Bool
        const isFlagged = oTask => oTask.flagged()

        // isDueIn2Days :: Int -> OF Task -> Bool
        const isDueIn = intDays => oTask => {
            const now = new Date();
            const dueDate = oTask.dueDate()
            return dueDate === null ? (
                false
            ) : dateDifferenceInDays(now, dueDate) <= intDays
        }

        const isDueIn2Days = isDueIn(2)

        // priority :: OF Task -> String
        const priority = oTask =>
            isFlagged(oTask) ? (
                isDueIn2Days(oTask) ? (
                    'A'
                ) : 'C'
            ) : isDueIn2Days(oTask) ? (
                'B'
            ) : 'D'

        // tagFoundOrCreated :: Tag String -> OF Task
        const tagFoundOrCreated = charTag => {
            const
                ofApp = Application('OmniFocus'),
                oDoc = ofApp.defaultDocument,
                tags = oDoc.flattenedTags.whose({
                    name: charTag
                }),
                oTag = ofApp.Tag({
                    name: charTag
                });
            return tags.length === 0 ? (
                (
                    oDoc.tags.push(oTag),
                    oTag
                )
            ) : tags()[0]
        }

        // applyTag :: OF Tag -> OF Task -> OF Task (Side Effects)
        const applyTag = oTag => oTask => {
            const ofApp = Application('OmniFocus')
            return ofApp.add(oTag, {
                to: oTask.tags
            })
        }
        
        const priorityTag = compose(priority, tagFoundOrCreated)

        const lrChoice = dialogChoiceLR(
            'Assign priority tags to selected OmniFocus Tags',
            'Do you want to apply this modification?',
            Left(),
            ['Cancel', 'OK'], 'OK', 'Cancel', 190
        )

        const ofApp = Application('OmniFocus')
        const oDoc = ofApp.defaultDocument
        const xs = filter(
            x => ObjectSpecifier.classOf(x) === 'task',
            ofSelectionLR().Right
        )

        return isLeft(lrChoice) ? (
            lrChoice.Left
        ) : map(x => applyTag(priorityTag(x))(x), xs)
    };

    // GENERIC -----------------------------------------------------------------
    // JS - Apps

    // ofSelectionLR :: () -> Either [OF Task]
    const ofSelectionLR = () => {
        const
            appOF = Application('OmniFocus'),
            ds = appOF.documents;

        return bindLR(
            bindLR(

                // Documents ?
                ds.length === 0 ? (
                    Left('No documents open')
                ) : Right(ds[0].documentWindows),

                // Windows ?
                ws => ws.length === 0 ? (
                    Left('No windows open')
                ) : Right(ws[0].content.selectedTrees)
            ),

            // Selection ?
            seln => seln.length === 0 ? (
                Left('No selection')
            ) : Right(seln.value())
        )
    }

    // JS - Automation

    // dialogChoiceLR :: String -> String -> Either String String ->
    //      [String] -> String -> String -> Int -> FilePath
    //          -> Either Dict String
    const dialogChoiceLR = (
        strTitle, strMsg, lrDefault, lstButtons, strDefaultButton,
        strCancelButton, intMaxSeconds, strIconPath
    ) => {
        const
            sa = standardAdditions();
        try {
            sa.activate;
            return (() => {
                // sa :: standardAdditions
                const dct = sa.displayDialog(strMsg, Object.assign({
                    buttons: lstButtons || ['Cancel', 'OK'],
                    defaultButton: strDefaultButton || 'OK',
                    cancelButton: strCancelButton || 'Cancel',
                    withTitle: strTitle,
                    givingUpAfter: intMaxSeconds || 120
                }, isRight(lrDefault) ? {
                    defaultAnswer: lrDefault.Right
                } : {}, typeof strIconPath === 'string' ? {
                    withIcon: Path(strIconPath)
                } : {}));
                return dct.gaveUp ? (
                    Left(dct)
                ) : Right(dct);
            })();
        } catch (e) {
            return Left(e);
        }
    };

    // standardAdditions :: () -> Application
    const standardAdditions = () =>
        Object.assign(Application.currentApplication(), {
            includeStandardAdditions: true
        });

    // JS - Prelude
    // https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // isLeft :: Either a b -> Bool
    const isLeft = lr =>
        lr.type === 'Either' && lr.Left !== undefined;

    // apply ($) :: (a -> b) -> a -> b
    const apply = (f, x) => f(x);

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        m.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // compose :: (a -> b) -> (b -> c) -> a -> c
    const compose = (f, g) => x => g(f(x));

    // isRight :: Either a b -> Bool
    const isRight = lr =>
        lr.type === 'Either' && lr.Right !== undefined;

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) => xs.map(f);

    // filter :: (a -> Bool) -> [a] -> [a]
    const filter = (f, xs) => xs.filter(f);

    // MAIN ----------------------------------------------------------------
    return main();
})();

#29

Just saw this ! Can’t wait to try it out tonight. Thank you so much! When you say selected items, does that mean it will scan the entire database like the other ones or do I need to select all items first?

In my opinion this is a huge update to OF workflows. Thank you very very much.


#30

You are welcome.

In this first version, you need to explicitly select items.


#31

If I copy paste the code into the middle part of the previous iterations will it work to scan the database ? I’m worried I’m going to have trouble keeping it up to date if selecting things manually.


#32

As the script is going to modify the entire database, I wanted to be sure it does what you expect. Does it work as you imagine ? I can make a revised version.


#33

I think I’m doing something wrong. I get an error when I run it, it says
“Error on line 18: Can’t get object”

Also says in the bottom of the script editor:
“Error -1728: Can’t get object”

Edit: I just realized !! It only does this when my selection includes the header from the grouping selection. So having it scan the database similarly to the script that auto flags tasks according to due/defer date, would help it function better I think. Another issue it runs into is when the priority changes on an item. Say it suddenly becomes due, it doesn’t update the letter assigned to it, it just adds a new one. Not sure how complicated these changes are though


#34

Thanks for the feedback. Will look into it and come back.


#35

Fixed

Good catch. An interesting problem to solve. Let me think what I can do.