Reveal a list of all values used with a particular tag

Hi Jesse.

I use tag values a lot, but I find it challenging to always use them consistently for a tag and for knowing and browsing for items tagged with a particular value.

A possible solution would be to present an expandable list of the unique values that have been used with a given tag in the sidebar tags list

An example of a list of tags in the sidebar:

  • who
  • where
    Canada
    USA
    France
    urgent
    R&D
  • group
    Data
    Sales
    Programming
    Followup

So… I click on the tag “where”, TaskPaper will show me all items that use the @where. If I click on “USA” under the “where” tag, TaskPaper will show me all items that use @where with “USA” as one of the values.

Being able to see this list of values used in the sidebar, will also serve to show me a quick reference of all the values I have used with a particular tag.

If this solution is too involved, then I wish for a tag inspector that will show all the values that I have used with a tag.

Thank you for giving this your consideration.

Corey

To give you a sense of things, this is just the list of values I use with my @group tag:

  • Account Services
  • Board of Directors
  • Data
  • Financial
  • GDPR
  • HR
  • IT
  • Legal
  • Marketing
  • PRB
  • Programming
  • Project Management
  • Q&A
  • R&D
  • Sales

In the meanwhile, you could choose a tag from a list, and see the set of values attached to it in the active document, by using a script run from Script Editor or Keyboard Maestro etc

https://guide.taskpaper.com/using-taskpaper/using-scripts.html

To test the rough sketch below:

  • Make sure that you have copied all of the source text, right down to to the bottom of the scrolling code panel,
  • paste the copied code into Script Editor.
  • Choose JavaScript at top left of the Script Editor toolbar, and click Run
(() => {
    'use strict';

    // TASKPAPER 3 CONTEXT --------------------------------

    // allTags :: TP Editor -> Dict -> [String]
    const allTags = editor => {

        const main = () =>
            nub(concatMap(
                x => x.attributeNames.filter(
                    x => 'data-type' !== x && 'indent' !== x
                ),
                editor.outline.items
            )).map(x => x.slice(5));

        // REUSABLE GENERIC FUNCTIONS ---------------------

        // Just :: a -> Just a
        const Just = x => ({
            type: 'Maybe',
            Nothing: false,
            Just: x
        });

        // Nothing :: () -> Nothing
        const Nothing = () => ({
            type: 'Maybe',
            Nothing: true,
        });

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

        // nub :: [a] -> [a]
        const nub = xs => nubBy((a, b) => a === b, xs);

        // nubBy :: (a -> a -> Bool) -> [a] -> [a]
        const nubBy = (p, xs) => {
            const go = xs => xs.length > 0 ? (() => {
                const x = xs[0];
                return [x].concat(
                    go(xs.slice(1)
                        .filter(y => !p(x, y))
                    )
                )
            })() : [];
            return go(xs);
        };

        return main();
    };

    // tagValues :: TP Editor -> Dict ->
    //              { tagName :: String, values :: [String] }
    const tagValues = (editor, options) => {

        const main = () => {
            const strTag = options.tag;
            return nub(
                editor.outline
                .evaluateItemPath('//@' + strTag)
                .map(x => x.getAttribute('data-' + strTag))
            ).sort();
        };

        // REUSABLE GENERIC FUNCTIONS ---------------------

        // Just :: a -> Just a
        const Just = x => ({
            type: 'Maybe',
            Nothing: false,
            Just: x
        });

        // Nothing :: () -> Nothing
        const Nothing = () => ({
            type: 'Maybe',
            Nothing: true,
        });

        // nub :: [a] -> [a]
        const nub = xs => nubBy((a, b) => a === b, xs);


        // nubBy :: (a -> a -> Bool) -> [a] -> [a]
        const nubBy = (p, xs) => {
            const go = xs => xs.length > 0 ? (() => {
                const x = xs[0];
                return [x].concat(
                    go(xs.slice(1)
                        .filter(y => !p(x, y))
                    )
                )
            })() : [];
            return go(xs);
        };

        return main();
    };

    // JXA CONTEXT------------------------------------------------------------

    // 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.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

    // standardSEAdditions :: () -> Application
    const standardSEAdditions = () =>
        Object.assign(Application('System Events'), {
            includeStandardAdditions: true
        });

    // MAIN -----------------------------------------------
    const
        sa = standardSEAdditions(),
        tp3 = Application('TaskPaper'),
        ds = tp3.documents,
        lrValues = bindLR(
            ds.length > 0 ? (
                Right(ds.at(0))
            ) : Left('No documents open'),
            d => {
                const
                    tags = d.evaluate({
                        script: allTags.toString(),
                    });
                return bindLR(
                    tags.length > 0 ? (
                        Right(tags)
                    ) : Left('No tags found'),
                    ts => {
                        const choice = (
                            sa.activate(),
                            sa.chooseFromList(
                                ts, {
                                    withTitle: 'Tags used',
                                    withPrompt: 'Choose tag:',
                                    defaultItems: ts[0],
                                    multipleSelectionsAllowed: false
                                }
                            )
                        );
                        return Boolean(choice) ? (
                            Right({
                                tagName: choice[0],
                                values: d.evaluate({
                                    script: tagValues.toString(),
                                    withOptions: {
                                        tag: choice[0]
                                    }
                                })
                            })
                        ) : Left('User cancelled');
                    }
                )
            }
        );
    // RESULT ---------------------------------------------
    return lrValues.Left || (() => {
        const strList = unlines(lrValues.Right.values);
        return (
            sa.activate(),
            sa.displayDialog(
                strList, {
                    withTitle: 'Values for @' + lrValues.Right.tagName
                }
            ),
            strList
        );
    })();
})();

Wow! Thank you very much. I greatly appreciate the help.

Corey

1 Like

This seems like a good idea, not sure I’ve considered it before. Anyway its pretty easy to add and seems to fit in with the rest of the UI so I’ve added it to the latest preview release:

Try it out and let me know how it works for you.

2 Likes

Initial tests look good here—you rock Jesse!

2 Likes

That was fast !

(and a very pleasing and useful bit of UI)

2 Likes

Is there a way to exclude items in the archive from this feature. I have my archive tags items with @project so I end up with a ton of items in that sub-list. And when I click on a sub items
it doesn’t yield any results in the archive because of how the archive formats the text for example: @project = Work / EP 43 / SULLIVAN

I still often search my archive, which works great, but that the tag based selections don’t yield results. Does that make sense? I hope I am being clear.

There isn’t at the moment. I’m still considering this feature… it seems logical, but for my particular usage it just makes the app a bit more complex in the sidebar and “Go to” panels. It might be that it’s off by default and you can enable via a preference.

1 Like

Interesting. I think it is a super useful feature if the archive is an option, but I i hear you. It does busy up the sidebar.