Categories
PHP Programming Todoist Zenkit

Zenkit API to Todoist CSV

I’m of two minds about posting this. The code is ugly. I’m posting it anyway because there aren’t a lot of resource for this this and was kind of a bear. I hope to save somebody else some trouble.

As is well-documented, I’m a fan of productivity tools. Todoist added boards to support a kanban format and let me tell you, I am here for it.

My productivity software journey of the past couple years goes like this: Trello and Todoist -> Asana -> Trello alone -> Zenkit -> Todoist alone. I have data living in all four places. I’m trying to move as much as I can to Todoist (it has global tags which Trello lacks, but its missing some other features I’m pretty fond of). Definitely Todoist will last forever and I won’t come running back to Trello.

Don’t forget to put in your API key.

<?php
/*
 * Create a Todoist CSV file from the Zenkit API. Resulting file requires some
 * manual work.
 *
 * @license: GPL
 * @author KJ Coop
 *
 * Zenkit is more complicated then Todoist, so we kind of have to wedge things
 * in. For example, Zenkit only has groups of labels, and you decide which ones
 * you want to use a section heads at any given moment. Consequently, when
 * Zenkit hands off its data, it's not easy to see which labels Todoist should
 * use as section heads and which it should use as labels. It's probably
 * possible to detect which labels are grouped together in the Zenkit output,
 * but I haven't put any effort into it (yet?).
 *
 * In Todoist, to add a label, you append the title with @Desired_Label. Because
 * we don't know whether we want Todoist to show any given label as a label or
 * a section head, it appends all the label-like strings as labels.
 * Consequently, if you want to keep your kanban format and of course you do or
 * what is even the point of being alive, you have to open up the finished
 * CSV file an manually manipulate it.
 *
 * @todo Things I haven't put any work into:
 *    - Descriptions - Zenkit has them whereas Todoist does not
 *    - Attachments - in Zenkit, they can be on a file. In Todoist, they're
 *      necessarily in a comment
 *    - Comments
 *    - Users
 *
 *
 *
 * Links:
 *    - https://todoist.com/help/articles/how-to-format-your-csv-file-so-you-can-import-it-into-todoist
 *    - https://base.zenkit.com/docs/api/overview/introduction
 *    - https://packagist.org/packages/idoit/zenkit-api-client
 */

require_once(__DIR__ . "/vendor/autoload.php");

use idoit\zenkit\API;

function appendToCsv($filename, $type, $content) {

    // https://todoist.com/help/articles/how-to-format-your-csv-file-so-you-can-import-it-into-todoist
    $array = [
        $type,
        $content,
        '4', // Priority
        '1', // Indent
        '',  // Author
        '',  // Responsible
        '',  // Date
        '',  // Date lang
        '',  // Timezone

    ];


    $csv_file = __DIR__.'/'.$filename.'.csv';

    // https://www.geeksforgeeks.org/how-to-convert-an-array-to-csv-file-in-php/
    $fp = fopen($csv_file, 'a');

    // Loop through file pointer and a line
    fputcsv($fp, $array);

    fclose($fp);
}



function listWorkspacesAndBoards() {
    $apiKey = '';

    $api = new API($apiKey);

    $count = 0;

    // Some of the arrays Zenkit returns are like this and we can ignore them.
    $emptyArray = [
        'text' => '',
        'searchText' => '',
        'textType' => 'plain'
    ];

    // Output data as array - workspaces and their collections/lists.
    $workspaces = $api->getWorkspaceService()->getAllWorkspacesAndLists();
    foreach ($workspaces as $key => $workspace) {
        $filename = $workspace->id;

        foreach ($workspace->lists as $list_key => $list_item) {
            $elements = $api->getElementService()
                ->getElementsInList($list_item->shortId);

            $entries = $api->getEntryService()
                ->setElementConfiguration($elements)
                ->getEntriesForListView($list_item->shortId);
            //    ->getEntry($list_item->shortId, 1);

            $listEntries = $entries->listEntries;

            $distinctTitles = [];
            $distinctWholeStrings = [];

            if (!is_array($listEntries)) {
                continue;
            }

            foreach ($listEntries as $entry) {
                if (isset($entry->displayString)) {
                    $title = '';

                    foreach ($entry->elements as $element) {
                        /*
                            Don't reset the title at each iteration of this
                            loop. We append labels in subsequent iterations.
                            Fesetting the title resets the whole task. Consider:
                            In Zenkit, you have something like the following:
                                Task 1
                                    * Label A
                                    * Label B
                                Task 2

                            The best sense of the API I can get is that it gives
                            you:
                                Task 1
                                Label A
                                Label B
                                Task 2

                            The code can see that Task 1 is a task and that the
                            labels are a different kind of string that relates
                            to the last thing it encountered. If doesn't know
                            until Task 2 comes along that it's finished with
                            Task 1. Until it sees a task, it has to keep
                            appending tasks to the last title it encountered.
                        */

                        if (isset($element['text']) && $element['text'] != '' && !strpos($element['text'], 'http')) {
                            // Remember that this is a title
                            $title = $element['text'];
                            $distinctTitles[md5($element['text'])] = $element['text'];

                        } else if (isset($element['categoriesSort']) && isset($element['categoriesSort'])) {
                            foreach ($element['categoriesSort'] as $cat) {
                                if (isset($distinctTitles[md5($title)])) {
                                    /*
                                        Add categories as labels. This is an not
                                        ideal map - Zenkit sees no distinction
                                        between labels and column heads,
                                        wbereas Todoist does. We add them all as
                                        labels if you want to change it to a
                                        section head, you must alter the
                                        resulting CSV manually :(
                                    */
                                    $distinctTitles[md5($title)] .= ' @'.str_replace(' ', '_', $cat->name);
                                }
                            }
                        } else if ($element == $emptyArray) {
                            // Silence it
                        } else if (isset($element['persons'])) {
                            // Silence it - I'm the only person who matters, obviously
                        } else if (isset($element['files']) && isset($element['filesData'])) {
                            // Not worried about files right now.
                        } else {
                            // IDK LOL OMG
                        }

                        // Remember the whole string.
                        $distinctWholeStrings[md5($title)] = $title;

                    }
                }
            }
        }
    }

    echo "Distinct Titles: \n";
    print_r($distinctTitles);

    foreach ($distinctTitles as $title) {
        appendToCsv($filename, 'task', $title);
    }
}

try {
    listWorkspacesAndBoards();
    echo "\n";
} catch (idoit\zenkit\BadResponseException $e) {
    echo 'Exception! The status was ' . $e->getCode() . ', response: ' . $e->getResponse()->getBody()->getContents();
} catch (Exception $e) {
    echo 'Exception! Something else went wrong: ' . $e->getMessage();
}

Categories
npm Trello

Tinkering with carlosazaustre / vue-trello

In my continuing effort to find a Trello replacement, I’ve been looking at carlosazaustre / vue-trello. My thanks to carlosazaustre for creating it. After some exploring, I realized it’s more reliant on Google Cloud Platform than I particularly want to go. However, I publish the notes I had so far in the hopes that they’ll help someone else.

`npm install`

`npm install` kept dying on Firebase. I had to uninstall then reinstall. I can’t find the Stack Overflow post that gave me the instructions, but according to my bash history, it looks something like the following:

Chuck GRPC’s data:
rm node_modules/grpc -r

Delete package-lock.json:
rm package-lock.json

Rebuild node-sass:
npm rebuild node-sass

Install the latest version of firebase:
npm i firebase@latest

Install firebase/app:
npm install --save @firebase/app

Settings File

The next error was:
ERROR Failed to compile with 1 errors 11:43:17 PM This relative module was not found: * ./settings in ./src/api/firebase.js

It needs a file called settings.js in src/api/. There’s an example file (settings.js.example) to grab from. You need to add your Firebase details. You can learn your API key at:
https://console.firebase.google.com/project/[project ID]/settings/general/

Next Steps

This is where I got off the train. Presumably it’s now just a matter of filling the correct details into the config file. Best of luck to you.

Categories
Asana CLI Linux Productivity Programming Ruby Trello

Working with TrelloToAsana

First off, many thanks to goborrega, the author of TrelloToAsana – a ruby package to help users port their data from 🌹💃 💕 Trello 💕 💃 🌹 to Asana. I love Trello (as you may have gathered from the emojis), but the boards are very siloed. I have like 3 dozen boards, I don’t want to have to dig into each one.

There are a few ways to get data between the two, but none were to my satisfaction until I found this package on Github.

I ran into a couple issues and I wanted to save some other folks the time. Many of these issues stem from not being a Ruby developer Thanks to Google University, I was able to develop a sufficient passing literacy.

Some background that may be helpful to non-Ruby devs may find useful:

  • Install Bundler and other dependencies. On Suse the command was: sudo zypper install ruby2.5-rubygem-bundler ruby-dev ruby2.5-dev ruby2.5-rubygem-rails-5_1
  • Install dependencies defined in the gemfile: bundle install
  • To run a Ruby script: ruby [filename]

There seems to be a version problem with the required versions of Ruby and the gem json. To fix that, I went opened Gemfile and replaced where it calls for json version 1.8.1 with 1.8.5. I haven’t seen any negative consequences of advancing it to a later version.

Between the time of the code’s writing and the time I ran it, Asana evidently changed their property id to gid on workspaces, projects and tasks. A find and replace on all instances of .id with .gid fixed the problem. The error was:

/usr/lib64/ruby/gems/2.5.0/gems/asana-0.10.0/lib/asana/resource_includes/resource.rb:34:in `method_missing': undefined method `id'

It worked like a dream after that.

Having gone to all that trouble, I don’t think I’m going to stick with Asana. There are too many features 💖 Trello 🌈 has that Asana doesn’t. My hunt for a 🌼 Trello 🥰 alternative that has global tags continues.

Categories
CLI Linux NPM Programming Vue.js

Pro-tip: `npm run dev` Relies on /etc/hostname

In my ongoing effort to play with Vue.js, I’ve been attempting to run the command npm run dev, which is required to turn the Vue magic into regular Javascript magic (all computers are magic). I couldn’t figure out why it kept insisting it couldn’t bind to the address.

I was looking for a config file with a hostname defined. There isn’t one. It uses the contents of /etc/hostname.

PS1 and hostname showed my hostname as kraken. But according to /etc/hostname, it was kraken.kjcoop.com. NPM was trying to bind to kjcoop.com’s IP address.

There’s no reason the hostname had to include kjcoop.com. I originally took to putting it in the hostname when I first registered the domain name in 1998 because I thought it was neat. I never got out of the habit because until [longer ago than I care to admit], it never caused me problems.