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
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.
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.
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:
Creating a fresh project in the chosen folder
Copying relevant properties across
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
}
))();
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:
choose its name from the Automation menu in OF
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
}
))();
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