Looking for a working move project to folder Applescript

I want to create an apple script to use via Alfred.app and Keyboard Maestro which takes the currently selected action, converts it to a project and moves the project into a particular folder.

Goole resulted in several scripts but I did not manage to get any of them to work.

I was able to hack some pieces together but I can not make it work as a whole.

Below are the pieces I have:

Getting the selected project:

   tell application "OmniFocus"
	tell front document
		tell front document window
			repeat with oPanel in {sidebar, content}
				set refProj to (a reference to (value of selected trees of oPanel where class of its value is project))
				set lstProj to refProj as list
				if lstProj ≠ {} then exit repeat
			end repeat
		end tell
		if lstProj = {} then return
		-- refProj
	end tell
   end tell

Getting a list of folders:

    tell application "OmniFocus"
	tell front document
		set Folderlist to name of every flattened folder
	end tell
    end tell

Selecting the folder:

   tell application "OmniFocus"
	tell the default document
		set folderList to (every flattened folder whose name is "My folder name")
		tell the front document window
		
		end tell
	end tell
   end tell

I would appreciate it if somebody can help me put the pieces together

You could also look at using the javascript options described here:

https://omni-automation.com/omnifocus/actions.html

These two scripts should be easy to adapt to work for you

  • Move Selected Tasks into New Project
  • Move Selected Projects into New Folder

Thanks Janov,

That is a great resource but I don’t have the skills to put the different parts together.

With Applescript I can manage bits and pieces but at the moment I’m stuck, therefore I am looking for somebody who might be able to help me put the pieces together.

Thanks for your help, it’s much appreciated. 😃

How is the target folder chosen or identified ?

  • by name ?
  • from a menu ?

Hi draft8,

My idea was to select an action, invoke the script, the script lets me choose from a list of folders, select a folder, script converts the action to project and then moves it to the selected folder.

Thanks for taking the time to look at this

I had a quick look at the omniJS interface to see what issues came up.

The first thing ( @unlocked2412 might have an answer to this, but I believe they are out of town ) is whether there is a simple way to upgrade a task to a project, conserving:

  • its subtree
  • its various date and other properties

(as in the GUI’s Edit > Convert to Project)

All I can immediately see in the omniJS API is the route of:

  1. Creating a fresh project in the chosen folder
  2. Copying relevant properties across
  3. Reparenting any child tasks and their descendants.

A first sketch of that latter route, leaving questions in place of implementations, might have the rough shape of:

JS Source
/*{
    "author": "Draft 8",
    "targets": ["omnifocus"],
    "type": "action",
    "identifier": "com.etc.toFolderAsNewProject",
    "version": "0.1",
    "description": "Selected task to chosen folder as project",
    "label": "toFolderAsProject",
    "mediumLabel": "toFolderAsProject",
    "paletteLabel": "toFolderAsProject",
}*/
(() => Object.assign(
    new PlugIn.Action(selection => {

        // main :: IO ()
        const main = () => {
            const tasks = selection.tasks;
            return bindLR(
                0 < tasks.length ? (
                    Right(tasks[0])
                ) : Left('No task selected in OmniFocus.')
            )(
                task => Right(
                    asProjectInChosenFolder(
                        'Target folder'
                    )(x => x.name)(flattenedFolders)(
                        task
                    )
                )
            );
        };


        // ------------------- OMNIJS --------------------

        // asProjectInChosenFolder :: String -> 
        // (a -> String) -> String -> 
        // [Folder] -> Task -> Promise ()
        const asProjectInChosenFolder = title =>
            labelFromItem => folders => task =>
            0 < folders.length ? (
                userDialog([
                    new Form.Field.Option(
                        'choice', // key
                        title, // displayName
                        folders, // options
                        folders.map(labelFromItem), // menu strings
                        folders[0], // default item
                        labelFromItem(folders[0]) // default item menu name
                    )
                ])('Choose:')(
                    dialog => {
                        const newProject = new Project(
                            task.name,
                            // In chosen folder:
                            dialog.values.choice
                        );

                        // Task properties copied to project ?

                        // Tasks descending from selected tasks
                        // reparented to new project ?

                        // Original task deleted ?

                        showLog(
                            'newProject',
                            Object.getOwnPropertyNames(
                                newProject
                            )
                        );
                    })
            ) : null;


        // userDialog :: [Form.Field] ->
        // String -> values -> () -> Promise
        const userDialog = fields =>
            // General constructor for omniJS dialogs,
            // where f is a continuation function 
            // in which a dialog result
            // is bound to the function argument.
            prompt => f => fields.reduce(
                (form, field) => (
                    form.addField(field),
                    form
                ), new Form()
            )
            .show(prompt, 'OK')
            .then(f);


        // ------------------- GENERIC -------------------

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


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


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


        // either :: (a -> c) -> (b -> c) -> Either a b -> c
        const either = fl =>
            // Application of the function fl to the
            // contents of any Left value in e, or
            // the application of fr to its Right value.
            fr => e => 'Either' === e.type ? (
                undefined !== e.Left ? (
                    fl(e.Left)
                ) : fr(e.Right)
            ) : undefined;


        // showLog :: a -> IO ()
        const showLog = (...args) =>
            console.log(
                args
                .map(JSON.stringify)
                .join(' -> ')
            );

        return main();
    }), {
        validate: selection => true
    }
))();

Here is an approach which might work:

  • Creating a new project, in the chosen folder, with the name of the selected task
  • Moving the selected task, with its subtrees, into the new project.

The installation would be to:

  • Save all the source code below (open the disclosure triangle) in a text file with the extension .omnijs
  • in the OmniFocus menu choose Automation > Configure
  • drag the .omnijs file into the Configure window which appears.

To use the plugin, you can either:

  1. choose its name from the Automation menu in OF
  2. Use Customize Toolbar in the main OF window, or Automation > Console to find and drag a button for it onto the toolbar.

JS Source
/*{
    "author": "Draft 8",
    "targets": ["omnifocus"],
    "type": "action",
    "identifier": "com.etc.toFolderAsNewProject",
    "version": "0.1",
    "description": "Selected task to chosen folder as project",
    "label": "toFolderAsProject",
    "mediumLabel": "toFolderAsProject",
    "paletteLabel": "toFolderAsProject",
}*/
(() => Object.assign(
    new PlugIn.Action(selection => {

        // main :: IO ()
        const main = () => {
            const tasks = selection.tasks;
            return bindLR(
                0 < tasks.length ? (
                    Right(tasks[0])
                ) : Left('No task selected in OmniFocus.')
            )(
                task => Right(
                    asProjectInChosenFolder(
                        'Choose'
                    )(x => x.name)(flattenedFolders)(
                        task
                    )
                )
            );
        };


        // ------------------- OMNIJS --------------------

        // asProjectInChosenFolder :: String -> 
        // (a -> String) -> String -> 
        // [Folder] -> Task -> Promise ()
        const asProjectInChosenFolder = title =>
            labelFromItem => folders => task =>
            0 < folders.length ? (
                userDialog([
                    new Form.Field.Option(
                        'choice', // key
                        title, // displayName
                        folders, // options
                        folders.map(labelFromItem), // menu strings
                        folders[0], // default item
                        labelFromItem(folders[0]) // default item menu name
                    )
                ])('Target folder:')(
                    dialog => {
                        const 
                            folder = dialog.values.choice,
                            taskName = task.name;
                        selection.database.moveTasks(
                            [task],
                            new Project(
                                taskName,
                                folder
                            )
                        );
                        console.log(
                            `${taskName} moved to folder :: ${folder.name}.`
                        );
                    })
            ) : null;


        // userDialog :: [Form.Field] ->
        // String -> values -> () -> Promise
        const userDialog = fields =>
            // General constructor for omniJS dialogs,
            // where f is a continuation function 
            // in which a dialog result
            // is bound to the function argument.
            prompt => f => fields.reduce(
                (form, field) => (
                    form.addField(field),
                    form
                ), new Form()
            )
            .show(prompt, 'OK')
            .then(f);


        // ------------------- GENERIC -------------------

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


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


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


        // either :: (a -> c) -> (b -> c) -> Either a b -> c
        const either = fl =>
            // Application of the function fl to the
            // contents of any Left value in e, or
            // the application of fr to its Right value.
            fr => e => 'Either' === e.type ? (
                undefined !== e.Left ? (
                    fl(e.Left)
                ) : fr(e.Right)
            ) : undefined;


        // showLog :: a -> IO ()
        const showLog = (...args) =>
            console.log(
                args
                .map(JSON.stringify)
                .join(' -> ')
            );

        return main();
    }), {
        validate: selection => true
    }
))();

Thanks Draft8! 😃

I appreciate all the effort, I saved the code and will definitly use it!

Meanwhile I also got the Applescript to work:

set the_list to {}

tell application "OmniFocus"
	tell front document
		set the_list to name of every flattened folder
		set t to (my sort_list(the_list))
		set ChosenFolder to choose from list t with prompt "Select the destination folder:" default items {""}
		set MyFolder to first folder whose name is ChosenFolder
		tell content of document window 1
			set theSelectedItems to value of every selected tree
			
		end tell
		repeat with anItem in theSelectedItems
			--This last bit gives an error
			set theProject to make new project with properties {name:(get name of anItem), note:(get note of anItem), defer date:(get defer date of anItem), due date:(get due date of anItem), flagged:(get flagged of anItem)}
			move project (get name of anItem) to end of projects of MyFolder
			delete anItem
		end repeat
	end tell
end tell


on sort_list(the_list)
	set nl to (ASCII character 10)
	tell (a reference to my text item delimiters)
		set {old_delim, contents} to {contents, nl}
		set {the_list, contents} to {"" & the_list, old_delim}
	end tell
	return paragraphs of (do shell script "echo " & (quoted form of the_list) & " | sort")
end sort_list

What happens, in your draft, if any of the selected items have descendants ?

Are you happy to discard them with their parent once its main properties have been transcribed to a new project ?

(It looks as if there may be a gap in these APIs which has the shape of the GUI’s Edit > Convert to Project , which takes care of conserving the selected task’s sub-tree, as well as all of its properties, attachments, and notifications)

I understand (from Omni Slack discussions) that a future build of OF is going to contain an omniJS method which simplifies automatic conversion of a task to a Project (in scripts like this), retaining all of its properties, subtrees, attachments, and notifications etc

In my usage scenario’s I don’t often have dependants.

To me the script does what it needs to do.

Thanks for the help.

1 Like