Our flexible Kanban board for managing tasks with drag drop


Post by suhas.kachare »

Hi Team ,

I am using the Bryutum Taskboard in React. In which I have customised the three-dot menu icon. I want to show a task menu item on clicking that icon. I tried achieving that, and I am able to show a menu item list, but the position of the menu item list is not where I wanted it to be.

Below is the code

import React, { Fragment, useRef, useEffect, useState } from "react";

import { BryntumTaskBoard, BryntumTaskBoardProps } from "@bryntum/taskboard-react";
import { TaskBoard, ProjectModel, DomHelper, DomClassList, TaskModel } from "@bryntum/taskboard";
import "./App.scss";
import { axiosWrapper } from "src/utils/axiosWrapper";
import { useDispatch } from "react-redux";
import { drawerActions } from "src/components/drawer/drawerReducers";
import Utils from "src/Utils";
import { Avatar, Dropdown } from "antd";


const menuItems = [
  {
    key: "1",
    label: (
      <a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
        1st menu item
      </a>
    ),
  },
  {
    key: "2",
    label: (
      <a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
        2nd menu item
      </a>
    ),
  },
  {
    key: "3",
    label: (
      <a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
        3rd menu item
      </a>
    ),
  },
];

const assigneeItems = [
  {
    key: "1",
    label: (
      <a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
        1st menu item
      </a>
    ),
  },
  {
    key: "2",
    label: (
      <a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
        2nd menu item
      </a>
    ),
  },
];


declare var Celoxis: any;

Celoxis.kanban = {};

const Kanban = (props: any) => {
  const { reportData } = props;
  const taskBoardRef = useRef<BryntumTaskBoard>(null);
  const taskBoardInstance = () => taskBoardRef.current?.instance as TaskBoard;

  const [kanbanId] = useState<any>("x" + Utils.randomId());
  const [position, setPosition] = useState<any>({});

  const dispatch = useDispatch();

  if (!Celoxis.kanban[kanbanId]) {
    Celoxis.kanban[kanbanId] = {};

Celoxis.kanban[kanbanId].addAssignee = (e) => {
  console.log(e);
  const rect = e.target.getBoundingClientRect();
  setPosition({ x: rect.left, y: rect.top, action: "assignee" });
};

Celoxis.kanban[kanbanId].taskActionMenu = async (e) => {
  console.log("abc", e);

  const response = await axiosWrapper.get("task.Actions.wm", { id: 17924 }, dispatch);
  console.log(response);
  setPosition({ x: e.clientX, y: e.clientY, action: "menu" });
};
  }

  useEffect(() => {
    return () => {
      delete Celoxis.kanban[kanbanId];
    };
  }, []);

  const dropDownItems = () => {
    if (position.action === "menu") {
      return menuItems;
    }
    if (position.action === "assignee") {
      return assigneeItems;
    }
    return [];
  };
 

  const kanbanConfig: BryntumTaskBoardProps = {
    // Url for resource avatar images
    resourceImagePath: "data/TaskBoard/images/users/",

// Experimental, transition moving cards using the editor
useDomTransition: true,

// Columns to display
columns: reportData.serverData.columns,

// Field used to pair a task to a column
columnField: "state",

tasksPerRow: 1,

features: {
  columnDrag: true,
  taskEdit: {
    disabled: true,
  },
  taskMenu: {
    items: {
      edit: {
        text: "Edit",
        icon: "b-fa-fw b-fa-flag",
        onItem({ taskRecord }) {
          taskRecord.flagged = true;
        },
      },
    },
  },

  columnToolbars: reportData.serverData.isAppKanban
    ? { disabled: true }
    : {
        bottomItems: {
          addTask: null,
          input: {
            type: "textfield",
            style: "flex: 1;",
            placeholder: "Name | Resources",
            onAction: async (event) => {
              // spliting the values to get name and resource

              console.log("abc", event);

              const addTask = await axiosWrapper.formPost(
                "task.AddMultiple.do",
                {
                  data: `${event.value}`,
                  defaults: JSON.stringify({ custom_kanban_state: `${event.source.columnRecord.data.id}` }),
                },
                { type: "rp", p_p_id: reportData.serverData.params.p_p_id },
                dispatch
              );

              if (addTask.data.success) {
              const values = event.value.split("|");
              const {
                source: {
                  columnRecord,
                  parent: { parent: taskBoard },
                },
                value,
              } = event;

              console.log(taskBoard);
              const textField = event.source;
              if (value.trim() === "") return;
              const resource = values[1].trim().toLowerCase();
              const res = taskBoard.project.resourceStore.find((item) => item.name.toLowerCase() === resource);
              if (Boolean(res)) {
                taskBoard.addTask(columnRecord, null, { name: values[0], resourceImages: {} });
                textField.value = "";
              }
              }
            },
          },
        },
      },
},

listeners: {
  async beforeTaskDrop({ taskRecords, targetColumn }) {
    // Show confirmation dialog
    if (reportData.serverData.isAppKanban) {
      dispatch(
        drawerActions.showContent({
          contentType: "TimeCodeList",
          contentProps: { url: "timeCode.List.wm" },
          boType: 51,
        })
      );
    }

    if (targetColumn.id === "Done") {
      return false;
    }
  },

  taskDrag({ taskRecords, targetColumn }) {
    return targetColumn.id !== "Backlog";
  },

  catchAll: ({ type }) => {
    if (!type.includes("mousemove") || type.includes("mouseover")) {
      // console.log("type", type);
    }
  },

  taskMenuBeforeShow: async ({ items }) => {
    let response = await axiosWrapper.get("task.Actions.wm", { id: 17354 }, dispatch);
    let menuItems = response.data.items;
    data.forEach((item) => {
      return (items[item.key] = item.text);
    });
  },

  taskMenuItem: (e) => {
    // console.log("abc", e.item.initialConfig.text);
    if (e.item.initialConfig.text === "Edit") {
      dispatch(
        drawerActions.showContent({
          contentType: "TimeCodeList",
          contentProps: { url: "timeCode.List.wm" },
          boType: 51,
        })
      );
    }
  },
},

headerItems: {
  text: {
    type: "template",
    template: ({ taskRecord }) =>
      // '<div class="card card-body" id="{tagId}" <tpl if="color">style="border-color:{color};"</tpl>>' +
      // '<tpl if="path"><div class="path pb-1 text-light large-only">{path}</span></div></tpl>' +
      '<div class="flexbox h-100 align-items-center py-2" style="display:flex ; align-items:center ;justify-content: center;flex-wrap: nowrap;">' +
      '<div style="flex: 1;min-width: 0px;overflow: hidden;text-overflow: ellipsis; max-height:99%;">' +
      `<a class="h6 mb-0 clickable" href={url} data-url={url} data-open="quickview" data-target="#qv-ajax">${taskRecord.data.name}</a>` +
      "</div>" +
      `<a style="justify-self:stretch;     width: 35px;
      height: 35px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      color:white;
      background: #4db7a9;"  class="large-only" 
    
       onclick="Celoxis.kanban['${kanbanId}'].addAssignee(event)">${taskRecord.data.resourceImages[0]?.initials}</a>` +
      `<div class="large-only action fa-lg far fa-ellipsis-v" onclick="Celoxis.kanban['${kanbanId}'].taskActionMenu(event)" style=" width: 16px; margin: 0; padding: 6px 0px; vertical-align: center; text-align: center; padding-left: 12px"></div>` +
      "</div>" +
      `<div class="large-only text-light kanban-attrs">${taskRecord.data.customAttrs}</div>` +
      "</div>",
    style: { width: "100%" },
  },
},

footerItems: {
  resourceAvatars: null,
},

// Project using inline data
project: {
  tasks: reportData.serverData.initialData,
  autoLoad: true,
  
},

taskRenderer({ taskRecord, cardConfig }) {
  if (taskRecord.data.prio) {
    cardConfig.class[taskRecord.data.prio] = true;
  }
},

tbar: [
  // Button for picking which columns to show
  { type: "columnpickerbutton" },
  { type: "zoomslider" },
  { type: "taskfilterfield" },
  {
    type: "textfield",
    ref: "highlight",
    placeholder: "Highlight tasks",
    clearable: true,
    keyStrokeChangeDelay: 100,
    triggers: {
      filter: {
        align: "start",
        cls: "b-fa b-fa-search",
      },
    },
    onChange(event) {
      const {
        source: {
          parent: { parent: taskBoard },
        },
        value,
      } = event;

      taskBoard.project.eventStore.forEach((task) => {
        const taskClassList = new DomClassList(task.cls);

        if (value !== "" && task.name.toLowerCase().includes(value.toLowerCase())) {
          taskClassList.add("b-match");
        } else {
          taskClassList.remove("b-match");
        }

        task.cls = taskClassList.value;
      });

      taskBoard.element.classList[value.length > 0 ? "add" : "remove"]("b-highlighting");
    },
  },
],
  };

  return (
    <Fragment>
      <BryntumTaskBoard ref={taskBoardRef} {...kanbanConfig} />
      {position.x ? (
        <Dropdown
          menu={{ items: dropDownItems() }}
          open={true}
          onOpenChange={(open) => {
            !open && setPosition({});
          }}
          trigger={["click"]}
        >
          <div
            style={{
              position: "fixed",
              display: "block",
              top: `${position.y}px` || "200px",
              left: `${position.x}px` || "400px",
              zIndex: 100,
            }}
          >
            &nbsp;
          </div>
        </Dropdown>
      ) : (
        ""
      )}

  {}
</Fragment>
  );
};

export default Kanban;

Please find below attach video for your reference

Attachments
Screen Recording 2023-03-09 at 4.14.17 PM.mov
(4.34 MiB) Downloaded 51 times

Post by tasnim »

Hi,

Can you please upload your app here (by wrapping it in a .zip file) so we can run it and debug it?


Post by Animal »

Why not just use the taskMenu?

Screenshot 2023-03-09 at 12.47.09.png
Screenshot 2023-03-09 at 12.47.09.png (289.44 KiB) Viewed 732 times

Post by suhas.kachare »

Please find below attached .zip file

Attachments
BryntumTrial Kanban 2.zip
(416.15 KiB) Downloaded 43 times

Post by Animal »

I don't really understand all that. The very simplest thing you can do is just use the taskMenu feature as shown above.


Post by suhas.kachare »

We want to show our task menu items dynamically. So how can we achieve that since the TaskMenu feature doesn't support async operations?


Post by Animal »

Async will be supported in the next version, 5.3.1 which will be out soon: https://github.com/bryntum/support/issues/6249

But from what I can understand in the code you posted it doesn't need to be async. You have

const menuItems = [
  {
    key: "1",
    label: (
      <a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
        1st menu item
      </a>
    ),
  },
  {
    key: "2",
    label: (
      <a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
        2nd menu item
      </a>
    ),
  },
  {
    key: "3",
    label: (
      <a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
        3rd menu item
      </a>
    ),
  },
]

Post by suhas.kachare »

The above menuItem code is just a dummy dataset for testing, but at the end, it will be fetched from the server.


Post by marcio »

Got it suhas.kachare,

As Animal already shared, it'll be released and available soon on 5.3.1!

Best regards,
Márcio


Post Reply