APIs e Microsserviços

5 mai, 2015

Chrome Extension de “ToDo” Integrado ao Trello

Publicidade

To Do List

Uma extensão para controlar suas tarefas é muito útil, principalmente se você puder recuperar os dados mesmo se estiver em outro computador e sem muito trabalho.

O Trello é um site onde podemos criar boards para organizar as atividades e as tarefas. Neste artigo, eu mostro como criar uma chrome extension que utiliza a API do Trello para armazenar as tarefas que você poderá acessar em qualquer computador onde tenha a extensão instalada utilizando sua conta do Trello.

Como sempre, o código está disponível no meu GitHub.

Entendendo a API

A documentação da API do Trello está disponível no endereço https://trello.com/docs/index.html e está em fase beta no momento que escrevo este artigo. A API é RESTful e isso é uma boa notícia, pois já sabemos trabalhar com RESTful e mesmo se a documentação não for muito completa, conseguimos fazer os acessos apenas por deduções (como é o caso desta API).

Obtendo a chave de acesso

Para acessar o trello a partir da nossa aplicação, precisamos de uma chave específica. Esta chave de 32 caracteres é a nossa autorização para utilizar a API através da nossa aplicação.

Para gerar a chave, é necessário estar logado no Trello e depois acessar a URL https://trello.com/1/appKey/generate.

A imagem abaixo mostra a tela com a chave de acesso. Essa chave é a que foi gerada a partir da minha conta, portanto, é necessário que você gere uma com sua conta para que sua aplicação não seja impedida de acessar, caso eu renove as minhas chaves.

captura-de-tela-2015-04-13-c3a0s-21-35-39

Configurações da extensão

Toda extensão do chrome necessita um arquivo de manifesto, que contém algumas informações para o chrome iniciar a aplicação. Dentre as configurações contidas no arquivo de manifesto, as permissões são bastante comuns e igualmente importantes.

Para esse projeto, precisamos de permissões de acesso às abas e armazenamento. Essas permissões são “activeTab”, “tabs” e “storage”, além (é claro) de acesso à URL da API https://*.trello.com/*.

Abaixo, listo o arquivo manifest.json que fica localizado na raiz da app:

{
  "manifest_version": 2,
  "name": "ToDo Trello Chrome Extension",
  "description": "Controlador de tarefas integrado ao trello",
  "version": "1.0",
  "browser_action": {
    "default_icon": "images/icon128.png",
    "default_popup": "popup.html"
  },
  "permissions": [
    "activeTab",
    "storage",
    "tabs",
    "https://*.trello.com/*"
  ],
  "background": {
    "scripts": ["js/background.js"],
    "persistent": true
  }
}

Estrutura dos arquivos

Para este projeto, eu decidi incluir alguma coisa mais elaborada no visual.

Embora não seja meu ponto forte e nem o objetivo deste artigo, é sempre mais interessante quando trabalhamos com uma interface mais agradável.

Eu utilizei o bootstrap para a interface, pois é uma biblioteca muito fácil de usar e tem um resultado satisfatório. O objetivo é ter uma interface como a imagem abaixo.

captura-de-tela-2015-04-22-c3a0s-09-48-44

Os arquivos estão organizados por pastas de acordo com seu tipo, como podemos ver na imagem abaixo:

captura-de-tela-2015-04-22-c3a0s-09-49-32

Fluxo de autenticação

Para conseguir acessar os dados do usuário, precisamos obter o token de acesso. Este token é necessário para que a API saiba que o usuário autorizou a nossa aplicação para obter e modificar dados da conta dele no Trello.

Enquanto não temos o token de acesso, não conseguimos ler os dados do usuário e por isso não temos tarefas para apresentar na tela. Neste momento, devemos apresentar uma tela para que o usuário possa solicitar a autorização para a nossa aplicação.

Abaixo está a tela inicial da aplicação:

captura-de-tela-2015-04-22-c3a0s-09-57-49

Quando o usuário clicar no botão Autorizar, será aberta uma aba com uma tela informando que a nossa aplicação está solicitando uma autorização para acessar a conta dele.

A imagem abaixo mostra a tela de autorização.

captura-de-tela-2015-04-13-c3a0s-22-45-19

É importante lembrar que para o tipo de permissão que precisamos, o Trello gera um token de validade de apenas 1 dia. Quando o usuário for acessar a extensão após esse 1 dia, a extensão deverá solicitar nova autorização.

Quando o usuário clicar em Allow com a intenção de autorizar a aplicação, o Trello faz um redirecionamento para uma URL que a aplicação informou. No nosso caso, não temos uma hospedagem para redirecionar o acesso, portanto, eu defini no parâmetro return_url o nome da nossa app.

Foi criado um listener, no arquivo background.js, para identificar que ocorreu a autorização e armazenar o token de acesso que é enviado pelo Trello através da URL de callback.

Abaixo, listo o arquivo background.js:

const CALLBACK_URL = "https://trello.com/1/token/ToDoChromeExtension#token=";

function setValue(key, value) {

    var obj = {};
    obj[key] = value;

    console.log("setj);

    chrome.storage.sync.set(obj, function () {
        console.log("ok   });
}

function getValue(key, callback) {
    chrome.storage.sync.get(key, function (value) {
        callback(value);
    });
}

chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
    getValue("tabId", function (value) {
        if (value.tabId === tabId) {
            if (changeInfo.status === "complete" && tab.url.indexOf(CALLBACK_URL) > -1) {

                var token = tab.url.replace(CALLBACK_URL, "");
                setValue("token", token);

                chrome.tabs.remove(tabId, function () {
                });
            }

        }
    });
});

Criando o quadro de tarefas

A extensão identifica que o usuário está autenticado e solicita os boards do usuário para a API.

O código abaixo mostra a função loadBoard que tenta encontrar o board cujo nome é o nome da nossa extensão, identificado aqui pela constante APP_NAME, que foi definida no início do arquivo popup.js.

Na primeira execução, não vamos encontrar o board com o nome da nossa app; quando isso ocorrer, faremos um post na API para criar um board com o nome que desejamos.

const BOARDS_URL = “https://trello.com/1/members/my/boards?key=%5BKEY%5D&token=%5BTOKEN%5D”;

const BOARD_CREATE_URL = "https://trello.com/1/boards?key=[KEY]&token=[TOKEN]&name=[NAME]";

...

function loadBoard() {
    var self = this;
    getJSON(BOARDS_URL.replace('[KEY]', API_KEY).replace('[TOKEN]', token), function (data) {
        if (data === "expired token") {
            logoff();
            return;
        }

        self.appBoard = null;

        data.some(function (board) {
            if (board.name === APP_NAME && !board.closed) {
                console.log(board);
                self.appBoard = board;
                return true;
            }
            return false;
        });

        if (!appBoard) {
            post(BOARD_CREATE_URL
                .replace('[KEY]', API_KEY)
                .replace('[TOKEN]', self.token)
                .replace('[NAME]', APP_NAME), function (data) {
                self.appBoard = data;

                loadLists();
            });
        } else {
            loadLists();
        }
    }, function (jqxhr, textStatus, error) {
        if (jqxhr.status === 401) {
            logoff();
        }
    });
}

Após encontrar ou criar um board, fazemos uma busca nas listas, que são as colunas onde os cards ficam agrupados no Trello. Por padrão, quando o board é criado, existem 3 listas: To Do, Doing e Done. 

O que faremos agora é identificar as listas para obter os cards (que são as nossas tarefas) e também para poder mover o card para a lista Done quando a tarefa for concluída.

const LISTS_URL = "https://api.trello.com/1/boards/[BOARD_ID]/lists?cards=open&card_fields=name&fields=name&key=[KEY]&token=[TOKEN]";

...

function loadLists() {
    var self = this;

    getJSON(LISTS_URL
        .replace('[KEY]', API_KEY)
        .replace('[TOKEN]', self.token)
        .replace('[BOARD_ID]', self.appBoard.id), function (data) {

        clearList();

        data.forEach(function (list) {
            var done = false;

            if (list.name === "To Do") {
                self.toDoList = list;
            } else if (list.name === "Done") {
                self.doneList = list;
                done = true;
            }

            if (list.cards) {
                list.cards.forEach(function (card) {
                    showCard(card, done);
                });
            }
        });

        showCard({id: "new", name: ""}, false);
    });
}

Listando as tarefas

Os cards são encontrados no JSON de retorno da lista, como podemos ver abaixo.

[
    {
        "id": "552e5c1ed5d312eefbce7ba4",
        "name": "To Do",
        "cards": [
            {
                "id": "552fa15a89d33bc16d0f8c57",
                "name": "Finish the ToDo chrome extension"
            },
            {
                "id": "552fb535d6a799698c93960b",
                "name": "English class"
            },
            {
                "id": "552fb53ac338f55f0b6ec2ad",
                "name": "Blog Post"
            },
            {
                "id": "552fb55af1b07169347244fa",
                "name": "Prototype"
            },
            {
                "id": "552fb566a2c694b19ab7b9b2",
                "name": "Meeting"
            }
        ]
    },
    {
        "id": "552e5c1ed5d312eefbce7ba6",
        "name": "Done",
        "cards": []
    }
]

Para cada card encontrado, será chamada a função showCard, que será responsável por incluir ele na interface de forma dinâmica.

Também será chamada mais uma vez para incluir uma linha de tarefa vazia, que será apresentada ao final da lista para que possamos digitar a tarefa que desejamos criar.

Para a função, são passados 2 parâmetros, sendo o primeiro o card e o segundo true/false para indicar se a tarefa já está concluída.

function showCard(card, done) {
    var template = $("#template").clone().attr('id', 'card_' + card.id).removeClass("hide    $("input[type=text]", template)
        .val(card.name)
        .on('change', function (e) {
            saveCard($(this));
        });

    $("input[type=checkbox]", template)
        .on('change', function (e) {
            moveCard(this);
        });

    if (card.id === 'new') {
        $("input[type=checkbox]", template).attr('disabled', 'disabled');
    }

    if (done) {
        $("input[type=checkbox]", template).attr('checked', 'checked');
    }

    $("#todo-container").append(template);
}

Criar, alterar e excluir uma tarefa

Para criar uma tarefa (ou card), o usuário deve digitar um texto no último campo que está vazio e quando sair do campo, apertar tab ou enter, assim, a extensão enviará um POST para a API para criar o card.

Para alterar uma tarefa, o usuário deve alterar o texto da tarefa que deseja modificar e sair do campo; a extensão irá realizar um PUT na API alterando o texto do card.

Para excluir, o usuário deverá limpar o campo e sair do mesmo. Quando isso ocorrer, será enviado um DELETE para a API solicitando a exclusão.

Abaixo segue a função saveCard que é responsável por identificar qual tipo de ação será realizada.

const CARD_CREATE_URL = "https://trello.com/1/cards?key=[KEY]&token=[TOKEN]&idList=[LIST_ID]&name=";
const CARD_UPDATE_URL = "https://trello.com/1/cards/[CARD_ID]?key=[KEY]&token=[TOKEN]&name=";
const CARD_DELETE_URL = "https://trello.com/1/cards/[CARD_ID]?key=[KEY]&token=[TOKEN]";
...
function saveCard(input) {
    var cardId = input.parent().parent().parent().attr('id').replace('card_', '');

    if (input.val().length === 0) {
        if (cardId !== 'new') {
            sendDelete(CARD_DELETE_URL
                .replace('[KEY]', API_KEY)
                .replace('[TOKEN]', self.token)
                .replace('[CARD_ID]', cardId), function () {
                console.log('deleted                loadLists();
            });
        }
    } else {
        if (cardId === 'new') {
            post(CARD_CREATE_URL
                .replace('[KEY]', API_KEY)
                .replace('[TOKEN]', self.token)
                .replace('[LIST_ID]', self.toDoList.id) + encodeURIComponent(input.val()), function () {
                console.log('created
                loadLists();
            });
        } else {
            put(CARD_UPDATE_URL
                .replace('[KEY]', API_KEY)
                .replace('[TOKEN]', self.token)
                .replace('[CARD_ID]', cardId) + encodeURIComponent(input.val()), function () {
                console.log('updated            });
        }

    }
}

Marcando uma tarefa como concluída

A tarefa que for concluída, deverá ter o checkbox localizado à esquerda do texto marcado. Para identificar a tarefa como concluída, devemos mover o card para a lista “Done”. Isso pode ser feito enviando um PUT para a API informando o card e o id da nova lista.

Uma tarefa pode ser enviada novamente para a lista de “To Do”, indicando que ela não está concluída. Este processo é o mesmo que marcar como concluída, porém o a id da lista deverá ser o da lista “To Do”.

Abaixo está a função que move a lista:

const CARD_MOVE_URL = "https://trello.com/1/cards/[CARD_ID]?idList=[LIST_ID]&key=[KEY]&token=[TOKEN]";
...
function moveCard(checkbox) {
    var cardId = $(checkbox).parent().parent().parent().parent().attr('id').replace('card_', '');

    put(CARD_MOVE_URL
        .replace('[KEY]', API_KEY)
        .replace('[TOKEN]', self.token)
        .replace('[CARD_ID]', cardId)
        .replace('[LIST_ID]', (checkbox.checked ? self.doneList.id : self.toDoList.id)), function () {
        console.log('moved        loadLists();
    });

}

Conclusão

Esta aplicação dá um certo trabalho, pois temos que tomar cuidado em armazenar o token do usuário para utilizar durante os requests. Além disso, temos uma tela diferente para quando o usuário não está autenticado.

Não esqueça de pegar o projeto no meu GitHub e caso encontre alguma dificuldade em refazer o projeto pode deixar sua mensagem que eu terei prazer em responder.