OP4 OmniJS 'Link to Task'

Apologies if this is a stupid question, I’ve been searching for hours for a solution but have been unable to find an answer.

I have some OmniJS scripts to link OP with OF. Part of the initial copy process is to append a link to the corresponding task in the other application, within the note of the task in each application.

For the link to the omniplan task I haven’t found a method of generating a link which includes the path to the file so that when clicked it opens the omniplan document.
My current method taken from the omni-automation.com site produces links which look like:

omniplan:///task/41

What i am looking for is a way of doing the equivalent of the menu option ‘Edit -> Copy Link To Task’ does when a task is selected in OmniPlan, in essence to build something which looks like:

omniplan://localhost/Users/username/Desktop/Test.oplx/task/41

The problem is i cant seem to get a handle on the open document file path /localhost/Users/username/Desktop/Test.oplx through any of the OmniJS methods exposed in the API on any of the obvious classes (document, application, project.

I fear I’m missing something obvious, can anyone point me in the right direction? Or is this simply not possible to do with the JS API?

For anyone else looking to do this, it is not currently possible - I reached out to support and they came back with some really good suggestions which I’m going to look into. They have also added a request to add a method to get the task link within the automation API. Full response is below:

I’m afraid it is not possible to get the path to an open file in OmniPlan via Omni Automation. The best you can do is get just the filename. This is done using document.name. Now, if your OmniPlan files are all stored in a particular directory, then you could just hard code the path into your script. So for instance:

let directory = “/Users/home_folder/Documents/”
let filename = document.name
let link = “omniplan://localhost” + directory + filename

Or even if you have files in different places, you could potentially keep a list of directory paths, and pick which one to use based on the filename of the current document.

Alternatively, you could have your script present a dialog where you choose a file to open, and that would allow you to get the path to that file, which could then be used by the rest of the script. Like this:

new FilePicker().show().then(function(urlArray) {
let url = urlArrary[0]
console.log(url.string) // result should be something like “file:///Users/home_folder/Documents/filename.oplx”
app.openDocument(null, urlArray[0], function() {
// use url here
})
})

Doing that, you would need to replace “file:///” with “omniplan://localhost/” to construct the links to tasks.

I hope one of those suggestions might work for you right now. Then, I have filed a request to add a [Task.link](http://task.link/) property to our API, so you can much more easily get an OmniPlan task link without having to find the path to a file and manually construct a url yourself.

For macOS, you can, of course, do it with JavaScript for Automation.

The Copy as Markdown Link Keyboard Maestro macro copies task-specific OP4 URLs this way:

(KM-specific JS source code below)

JS Source
(() => {
    'use strict';

    // Rob Trew @2020

    // main :: IO ()
    const main = () => {
        const
            op = Application('OmniPlan'),
            wins = op.windows,
            version = last(op.id());

        return either(
            // A user message,
            alert('MD link')
        )(
            // or a Markdown link.
            x => x
        )(
            bindLR(
                0 < wins.length ? (
                    Right(wins.at(0))
                ) : Left(
                    `No documents open in OmniPlan ${version}.`
                )
            )(window => {
                const
                    doc = window.document,
                    fp = Path(doc.file()).toString(),
                    opURL = encodeURI(`omniplan://localhost${fp}`),
                    fileURL = encodeURI('file://' + fp),
                    selns = window.selectedTasks(),
                    taskNames = selns.map(x => x.name())
                    .join(','),
                    taskIds = selns.map(x => x.id())
                    .join(',%2520');
                return Right(
                    0 < selns.length ? (
                        `[${taskNames}](${opURL}/task/${taskIds})`
                    ) : `[${doc.name()}](${fileURL})`
                );
            })
        );
    };

    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application('System Events'), {
                    includeStandardAdditions: true
                });
            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ['OK'],
                    defaultButton: 'OK'
                }),
                s
            );
        };

    // --------------------- 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;

    // last :: [a] -> a
    const last = xs => (
        // The last item of a list.
        ys => 0 < ys.length ? (
            ys.slice(-1)[0]
        ) : undefined
    )(list(xs));

    // list :: StringOrArrayLike b => b -> [a]
    const list = xs =>
        // xs itself, if it is an Array,
        // or an Array derived from xs.
        Array.isArray(xs) ? (
            xs
        ) : Array.from(xs || []);

    return main();
})();

1 Like

Thank you! I will take a look at this.