Skip to main content

DHTMLX Kanban. Backend with comments and votes example

Comments and votes are collaborative features that require user identity. This demo extends the basic backend integration with authentication, allowing each user to post comments and cast votes under their own identity.

Live example

const { Kanban, Toolbar, RestDataProvider, defaultEditorShape, defaultCardShape } = kanban;

// Check Go backend repository here -  https://github.com/web-widgets/kanban-go
const url = "https://docs.dhtmlx.com/kanban-backend";
const restProvider = new RestDataProvider(url);

loginWithUser(initKanban, restProvider, url, "auth-container");

function initKanban(restProvider, url, token, userId) {
    Promise.all([
        restProvider.getCards(),
        restProvider.getColumns(),
        restProvider.getRows(),
    ]).then(([cards, columns, rows]) => {
        restProvider.setHeaders({
            "Remote-Token": token,
        });
        const cardShape = {
            ...defaultCardShape,
            label: true,
            description: true,
            progress: true,
            start_date: true,
            end_date: true,
            priority: true,
            color: true,
            cover: true,
            attached: true,
            users: {
                show: true,
                values: users
            },
            votes: {
                show: true,
                clickable: true,
            },
            comments: true,
        };
        const editorShape = [
            ...defaultEditorShape,
            {
                key: "attached",
                type: "files",
                label: "Files",
                uploadURL: url + "/uploads",
            },
            {
                type: "comments",
                key: "comments",
                label: "Comments",
                config: {
                    placement: "editor",
                },
            },
        ];

        const board = new Kanban("#root", {
            cards,
            columns,
            rows,
            rowKey: "row",
            cardShape,
            editorShape,
            currentUser: parseInt(userId),
        });
        board.api.setNext(restProvider);
        new Toolbar("#toolbar", { api: board.api });
    });
}
<!-- component containers -->
<div id="auth-container" style="display: flex; justify-content: center; align-items: center; height: 100%">
    <div id="auth-content" class="auth-content"></div>
</div>

<div id="toolbar"></div>
<div id="root" style="height: calc(100% - 56px);"></div>

<!-- dataset -->

<style>
.auth-select {
		padding: 4px;
		border: 1px solid #d2cfcf;
		font-size: 14px;
		height: 32px;
		width: 200px;
		margin: 5px 0 10px;
	}

	.auth-btn {
		display: inline-block;
		vertical-align: middle;
		max-width: 100%;
		min-width: 200px;
		font-weight: 500;
		color: #fff;
		cursor: pointer;
		font-size: 16px;
		padding: 6px 16px;
		border: none;
		background: #027ABC;
	}

	.auth-content {
		width: 150px;
        height: 200px;
	}
</style>

<script>
const users = [
        {
            id: 1,
            label: "Steve Smith",
            avatar: "https://snippet.dhtmlx.com/codebase/data/kanban/01/img/user-1.jpg",
        },
        {
            id: 2,
            label: "Aaron Long",
            avatar: "https://snippet.dhtmlx.com/codebase/data/kanban/01/img/user-2.jpg",
        },
        {
            id: 3,
            label: "Angela Allen",
            avatar: "https://snippet.dhtmlx.com/codebase/data/kanban/01/img/user-3.jpg",
        },
        {
            id: 4,
            label: "Angela Long",
            avatar: "https://snippet.dhtmlx.com/codebase/data/kanban/01/img/user-4.jpg",
        },
        {
            id: 5,
            label: "John Doe",
        },
    ];

	const login = (url, id) => {
        return fetch(`${url}/login?id=${id}`)
            .then(raw => raw.text())
            .then(token => {
                sessionStorage.setItem("login-token", token);
                sessionStorage.setItem("user-id", id);
                return token;
            });
    }

    const loginWithUser = (initKanban, restProvider, url, authContainer) => {
        // 1) try to get credentials from previous session
        const token = sessionStorage.getItem("login-token");
        const userId = parseInt(sessionStorage.getItem("user-id"));
        if (token && userId) {
            if (authContainer)
                document.getElementById(authContainer).style.display = "none";
            initKanban(restProvider, url, token, userId);
            return;
        }
        // 2) initialize an auth panel and login with exact user
        const onAuth = userId => {
            login(url, userId).then(token => {
                document.getElementById(authContainer).style.display = "none";
                initKanban(restProvider, url, token, userId);
            });
        };

        sessionStorage.clear();
        restProvider
            .getUsers()
            .then(data => {
                _initAuthContainer(authContainer, data, onAuth);
            })
            .catch(err => {
                alert(err);
                throw new Error(err);
            });
    }

    const _initAuthContainer = (containerId, users, onAuth) => {
        const container = document.getElementById("auth-content");
        if (!container) {
            throw new Error("auth container not defined");
        }
        container.innerHTML = `
            <label for="users">Choose user:</label>
            <select name="users" id="users-select" class="auth-select"></select>
            <button id="login-button" class="auth-btn">Login</button>
        `;

        const selectNode = document.getElementById("users-select");
        for (let i = 0; i < users.length; i++) {
            const option = document.createElement("option");
            option.text = users[i].label;
            option.value = users[i].id;
            selectNode.appendChild(option);
        }

        document.getElementById("login-button").onclick = () => {
            const userId = selectNode.value;
            if (!userId) {
                alert("choose user");
                return;
            }
            onAuth(userId);
        };
    }
</script>

The code adds a login flow before initializing the board. After authentication, the RestDataProvider gets a "Remote-Token" header via setHeaders, and currentUser is set to the logged-in user's ID. The card shape enables votes: { show: true, clickable: true } and comments: true, while the editor includes a type: "comments" field with placement: "editor". The login credentials are stored in sessionStorage to persist across page refreshes within the same session.

Solution overview

  1. Authenticate the user and obtain a token
  2. Set the token on the provider: restProvider.setHeaders({ "Remote-Token": token })
  3. Initialize the board with currentUser: parseInt(userId)
  4. Enable votes and comments in cardShape and editorShape
  5. Connect to the backend with board.api.setNext(restProvider)

Key points

  • currentUser ties actions to identity: Without it, the board can't track who commented or voted - it's required for both features
  • Token-based auth: The Remote-Token header carries a JSON Web Token that the backend decodes on every request to extract the user ID - this is what ties votes and comments to the correct user
  • Comment placement in editor: config: { placement: "editor" } renders comments inline in the editor panel

API reference

Additional resources