I'd love a "move row to" button

I’ve realized I’d love something like cmd + p, except it takes the currently selected row + all children and turns it into a child of the one you searched for

Sounds like something that could be scripted,
but the mechanics are not yet quite clear to me.

You want to select the source first,
and then choose a target from some search results ?


If, at any point, outline paths were to become accessible to the scripting interface, it might be possible to script a filtered move selected sub-tree(s) to chosen parent dialog with a flexibility similar to that of the built-in ⌘P Focus Heading dialog.

I think that is what I would expect.

I also think that I will add a native version of this eventually. It will be very similar/same as TaskPaper’s Item > Move to Project command. I don’t know when exactly I will add this, but it’s on my short list. Shouldn’t be too hard I think.

2 Likes

To test, with dummy data, a very rough draft (not for use with real data) of an interim script:

Expand disclosure triangle to view JS source
(() => {
    "use strict";

    // NB ROUGH pre-draft sketch,
    // ONLY for experimenting with DUMMY DATA:

    // Move selected rows, with their descendants,
    // to target sections chosen from a menu.

    // In this draft, the targets menu shows
    // 1. All rows at levels 1 and 2 (except where selected)
    // 2. All rows with type "heading" (except where selected)

    // Rob Trew @2023
    // Ver 0.01

    const deepestNonHeadingTargetLevel = 2;

    // main :: IO ()
    const main = () => {
        const doc = Application("Bike").documents.at(0);

        return either(
            alert("Move selected row(s) to end of chosen section.")
        )(
            message => message
        )(
            bindLR(
                selectedForestLR(doc)
            )(
                selectionRows => fmapLR(target => {
                    const
                        n = selectionRows.length,
                        k = target.name();

                    // EFFECT AND REPORT
                    return (
                        selectionRows().forEach(
                            row => row.move({to: target})
                        ),
                        `${n} row(s) moved (with descendants) to end of ${k}.`
                    );
                })(
                    targetSectionsMenuChoiceLR(doc)(
                        deepestNonHeadingTargetLevel
                    )
                )
            )
        );
    };

    // ------------------------ BIKE ---------------------

    // selectedForestLR :: Bike Document ->
    // Either String Bike Rows
    const selectedForestLR = doc =>
        // Either a message string, or a reference to
        // the outer level selected rows.
        doc.exists()
            ? (() => {
                const
                    rows = doc.rows.where({
                        selected: true
                    }),
                    outerLevel = Math.min(...rows.level()),
                    selectionForest = rows.where({
                        level: outerLevel,
                        selected: true
                    });

                return 0 < selectionForest.length
                    ? Right(selectionForest)
                    : Left(
                        `No rows selected in ${doc.name()}`
                    );
            })()
            : Left("No open Bike document found.");


    // targetSectionsMenuChoiceLR :: Bike Document ->
    // Int -> Either String -> IO Bike Row
    const targetSectionsMenuChoiceLR = doc =>
        // Either a message or a row chosen by the user.
        levelLimit => doc.exists()
            ? (() => {
                const
                    rows = doc.rows,
                    targetRows = rows.where({
                        _or: [
                            {level: {"<=": levelLimit}},
                            {_match: [ObjectSpecifier().type, "heading"]}
                        ],
                        _not: [{name: ""}]
                    }),
                    nTargets = targetRows.length;

                return 0 < nTargets
                    ? 1 < nTargets
                        ? targetChoiceLR(rows)(targetRows)(
                            new Set(
                                rows.where({selected: true})
                                .id()
                            )
                        )
                        : Right(targetRows.at(0))
                    : Left("No target rows found.");
            })()
            : Left("Bike document not found.");


    // targetChoiceLR :: Bike Rows ->
    // Bike Rows -> Either String IO Bike Row
    const targetChoiceLR = allRows =>
        // Either a message, or a Bike Row chosen
        // by the user from a menu of targetRows.
        // (Rows with selected ids are excluded).
        targetRows => exceptionSet => {
            const
                menuKeys = zipWith3(k => n => s =>
                    exceptionSet.has(k)
                        ? []
                        : [[
                            k,
                            `${"    ".repeat(n - 1)}${s}`
                        ]])(
                    targetRows.id()
                )(
                    targetRows.level()
                )(
                    targetRows.name()
                )
                .flat(),
                menuLines = menuKeys.map(snd),
                sa = Object.assign(
                    Application("System Events"),
                    {includeStandardAdditions: true}
                ),
                result = (
                    sa.activate(),
                    sa.chooseFromList(
                        menuLines, {
                            withTitle: "Move Selected Rows",
                            withPrompt: (
                                "Move selection to children of:"
                            ),
                            defaultItems: [menuLines[0]],
                            okButtonName: "Move"
                        }
                    )
                );

            return Array.isArray(result)
                ? 0 < result.length
                    ? fmapLR(
                        rowID => allRows.byId(rowID)
                    )(
                        chosenIDLR(menuKeys)(result[0])
                    )
                    : Left("No target chosen.")
                : Left("User cancelled.");
        };


    // chosenIDLR :: [(ID, String)] -> Either ID String
    const chosenIDLR = menuKeys =>
        // Either a message or the ID of the chosen line.
        chosenLine => {
            const
                match = menuKeys.find(
                    ([, s]) => chosenLine === s
                );

            return Boolean(match)
                ? Right(match[0])
                : Left(`Menu entry not found: ${chosenLine}`);
        };


    // ----------------------- 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 => 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 => e.Left ? (
            fl(e.Left)
        ) : fr(e.Right);


    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
        // Either f mapped into the contents of any Right
        // value in e, or e unchanged if is a Left value.
        e => "Left" in e ? (
            e
        ) : Right(f(e.Right));


    // snd :: (a, b) -> b
    const snd = tpl =>
    // Second member of a pair.
        tpl[1];


    // zipWith3 :: (a -> b -> c -> d) ->
    // [a] -> [b] -> [c] -> [d]
    const zipWith3 = f =>
        xs => ys => zs => Array.from({
            length: Math.min(
                ...[xs, ys, zs].map(x => x.length)
            )
        }, (_, i) => f(xs[i])(ys[i])(zs[i]));


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

To test in Script Editor, change language selector at top left to JavaScript (rather than AppleScript).

See: Using Scripts - Bike

1 Like

I used task paper a year and a half ago so that might be where the functionality intention came from in the bottom of my mind while I was using bike.

My use case is that I have one inbox list, and then a dozen other buckets that I’ll sort items from the inbox list into. At the moment I’m having to move the rows manually, or use copy and paste, but since each list is beginning to have hundreds of items I’m wishing I could just use a shortcut like cmd p. I had the same set up and task paper, and I believe I just used that move command you mentioned.

2 Likes

Latest preview adds move to heading:

1 Like