Our state of the art Gantt chart


Post by avallete »

Hi there,

I'm currently trying to use the "nonWorkingTime" feature with the React wrapper. I did set it to "true" into the "features" configuration as recommended in the API documentation here:

But it doesn't seems to be taken into account, or maybe some other option I use enter in conflict with it. Here is the code I use:

/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import "@bryntum/gantt/gantt.material.css";
import type { GanttBase, GanttConfig } from "@bryntum/gantt";
import { BryntumGantt } from "@bryntum/gantt-react";
import * as React from "react";

const serverData = [
  {
    children: [
      {
        children: false,
        draggable: false,
        endDate: "2021-10-30T00:00:00.000Z",
        id: "5a33440c-c1b5-4945-a999-edb80c774ab4",
        index: 0,
        manuallyScheduled: true,
        name: "test",
        parentId: "357fb463-97aa-40b9-8ef3-f8ec1dc88f88",
        percentDone: 0,
        resizable: false,
        startDate: "2021-10-01T00:00:00.000Z",
        taskNumber: "1.1",
        type: "subtask",
      },
    ],
    draggable: true,
    endDate: "2021-10-30T00:00:00.000Z",
    expanded: true,
    id: "357fb463-97aa-40b9-8ef3-f8ec1dc88f88",
    manuallyScheduled: true,
    name: "build a wall (Link to Tour Eiffel project)",
    percentDone: 100,
    resizable: true,
    startDate: "2021-10-01T00:00:00.000Z",
    taskNumber: "1",
    type: "task",
  },
  {
    children: [],
    draggable: true,
    endDate: "2021-10-26T00:00:00.000Z",
    expanded: false,
    id: "af098e9c-0ae4-4821-9d29-bf12ce30e823",
    manuallyScheduled: true,
    name: "talk to contractor",
    resizable: true,
    startDate: "2021-10-24T00:00:00.000Z",
    taskNumber: "2",
    type: "task",
  },
  {
    children: [],
    draggable: true,
    endDate: undefined,
    expanded: false,
    id: "38b491a4-012d-4201-bcdd-4539a8cf5bad",
    manuallyScheduled: true,
    name: "Task without dates",
    resizable: true,
    startDate: undefined,
    taskNumber: "3",
    type: "task",
  },
];

type Config = Partial<GanttConfig>;

function Gantt() {
  const ganttRef = React.useRef<{ instance: GanttBase }>();
  const [tasks, setTasks] = React.useState(serverData);
  const [taskSelected, setTaskSelected] = React.useState(null);

  const config: Config = {
    barMargin: 10,
    columns: [
      { field: "taskNumber", text: "N°", type: "number", width: 10 },
      { field: "name", type: "name", width: 50 },
    ],
    features: {
      // We don't want to use the gantt default edit form
      cellEdit: false,
      columnAutoWidth: true,
      columnLines: false,
      // We don't need the task/link dependencies
      dependencies: false,
      dependencyEdit: false,
      // We want to have the "weekends" greyed on the calendar
      nonWorkingTime: {
        disabled: false,
        hideRangesOnZooming: false,
        highlightWeekends: true,
        maxTimeAxisUnit: "hour",
      },
      // Used to have the percent of progress of a task (validated / subtasks progress)
      percentBar: {
        allowResize: false,
        showPercentage: true,
      },
      // Used to get the "red line" to show what's the progress on the planning
      progressLine: true,
      // Should show projectStart projectEnd markers
      projectLines: true,
      taskCopyPaste: false,
      // Should be able to change date of a task with drag/drop
      // TODO: we should handle all thoses events in offline mode
      taskDrag: true,
      // It's not really "drag to create" but more "assigne" start/end date to tasks
      // who doesn't already have one
      // TODO: actually use this to open the "create task" dialog with prefill start/end date
      taskDragCreate: true,
      // We don't want to use the default taskEdit form, instead we open our own dialog
      taskEdit: false,
      taskMenu: false,
      taskResize: {
        allowResizeToZero: false,
      },
      taskTooltip: true,
      timeRanges: {
        showCurrentTimeLine: {
          name: "Today",
        },
        showHeaderElements: true,
      },
      tree: true,
    },
    // Keep the scroll within project range
    infiniteScroll: true,
    maxHeight: "100vh",
    maxWidth: "100vw",
    // set max zoom level to day
    maxZoomLevel: 11,
    // set minimal zoom level to acceptable values (multiples years with quarters)
    minZoomLevel: 5,
    tbar: [
      {
        checked: true,
        label: "Show Project Line",
        listeners: {
          change: ({ checked }: { checked: boolean }) => {
            if (ganttRef.current?.instance.features.progressLine) {
              ganttRef.current.instance.features.progressLine.disabled =
                !checked;
            }
          },
        },
        type: "checkbox",
      },
      {
        inputWidth: "7em",
        label: "Project Status Date",
        listeners: {
          change: ({ value }: { value: Date }) => {
            if (ganttRef.current?.instance.features.progressLine) {
              ganttRef.current.instance.features.progressLine.statusDate =
                value;
            }
          },
        },
        step: "1d",
        type: "datefield",
        value: new Date(),
      },
    ],
    viewPreset: {
      // By default, show (day, week, month) on the same timebar
      headers: [
        {
          align: "start",
          renderer: (startDate: Date) => startDate.getMonth(),
          unit: "month",
        },
        {
          renderer: (startDate: Date) => startDate.getDate(),
          unit: "week",
        },
        {
          dateFormat: "DD",
          unit: "day",
        },
      ],
      timeResolution: {
        increment: 1,
        unit: "day",
      },
    },
  };
  return (
    <div style={{ height: "90vh", width: "90vw" }}>
      <div
        style={{
          backgroundColor: "#ecf0f1",
          display: taskSelected === null ? "none" : "block",
          height: "100vh",
          inset: 0,
          position: "absolute",
          width: "100vw",
          zIndex: 999,
        }}
      >
        Popup for {JSON.stringify(taskSelected)}
        <button
          style={{
            backgroundColor: "red",
            borderRadius: "10px",
            height: "50px",
            width: "150px",
          }}
          onClick={() => setTaskSelected(null)}
        >
          Close popup
        </button>
      </div>
      <BryntumGantt
        {...config}
        ref={ganttRef}
        tasks={tasks}
        onTaskDblClick={(e: any) => {
          const { taskRecord } = e;
          setTaskSelected(taskRecord.originalData);
        }}
        onTaskDrop={({ taskRecords }: { taskRecords: any[] }) => {
          const taskRecord = taskRecords[0];
          const taskData = taskRecord.originalData;
          const filteredTasks = tasks.filter((t) => t.id !== taskData.id);
          // Save change into the store
          setTasks([
            ...filteredTasks,
            {
              ...taskData,
              endDate: taskRecord.endDate,
              startDate: taskRecord.startDate,
            },
          ]);
        }}
        onCellDblClick={(e: any) => {
          // extract the record from the event
          const { record } = e;
          ganttRef.current?.instance?.scrollTaskIntoView(record, {
            animate: true,
            block: "center",
          });
        }}
        taskStore={{ syncDataOnLoad: false }}
      />
    </div>
  );
}

export { Gantt };

I'm I missing something here ?


Post by Maxim Gorkovsky »

Hello.
You are probably missing actual non working time. You need to add calendar, smth like:

project = {
    calendar : 'general',
    calendarsData : [
        {
            id : 'general',
            intervals : [
                {
                    recurrentStartDate : 'on Sat at 0:00',
                    recurrentEndDate   : 'on Sun at 0:00',
                    isWorking          : false
                }
            ]
        }
    ],
    eventsData : tasks,
    taskStore : { syncDataOnLoad : false }
}

<BryntumGantt project={{project}} />

Post by avallete »

Hi,

Thank you for your response, it does indeed work to show the "nonWorkingTime".

However, with the React Wrapper this bring another issue that I already had trouble with viewtopic.php?f=52&t=19221&p=95137#p95137

Using the "project: { eventsData: tasks }" syntax doesn't play nice with changing data (it make the gantt, refresh and freeze a little). To have smooth "changing data" it has been advised to me to use the "tasks" props which use the inlineData.

Issue is, with inlineData, I cannot use a "project" definition. Therefore, I did tried other ways to define the ranges in the nonWorkingTime store directly without success so far:

nonWorkingTime: {
      disabled: false,
      highlightWeekends: true,
      store: {
        data: [
          {
            intervals: [
              {
                isWorking: false,
                recurrentEndDate: "on Sun at 0:00",
                recurrentStartDate: "on Sat at 0:00",
              },
            ],
          },
        ],
      },
    }

Post by Maxim Gorkovsky »

Calendar is part of the project, you cannot define it in the non working time feature. I provided that config just to demo how it could be done. If you cannot use that exact config, you can try another way around, i.e. provide calendars the same way you provide tasks.
All you need to do there is:

  1. load some calendars with non working time to the project
  2. tell project to use a specific calendar

Post by avallete »

Hi there,

Thank you for the clarification about the calendar being part of the project, and the step to follow to create "non-working time ranges".

I did tried to pass the "calendars" data the same way I pass the "tasks" data to the component as you suggested (trough props) without any luck. And looking at the wrapper source code, it doesn't seems like it can get a props containing this data.

Therefore, my issue still remain, as I mentioned in my previous post, in the React wrapper I use the "tasks" props to provide my tasks data to the component. This internally seems to use "inlineData", which cause conflict when declaring a "project" configuration.

And I need to use this props (that's what I've been told) to avoid blinking/freezing when my data change (see bellow some records of the issue):

When using the "tasks" wrapper props:

Screen Recording 2021-10-30 at 00.59.44.mov
(2.08 MiB) Downloaded 47 times

When using the "project" config (which put the tasks in eventData):

Screen Recording 2021-10-30 at 00.58.58.mov
(6.74 MiB) Downloaded 53 times

As for now, I haven't found another way to pass the "calendar" data to the "<BryntumGantt>" React component using something else than the "project" configuration. So I'm kind of stuck between a rock and a hard place here. When I do manage to get the "nonWorkingTime" ranges feature to work, it does break the smooth interaction with my tasks. And when I manage to get smooth interactions with my tasks, I cannot define "project" anymore, and therefore can't have my nonWorkingTime ranges.


Post by alex.l »

Hi avallete,

Yes, there is a known bug with using inlineData. We had a workaround (you mentioned above) but it is not universal and do not cover all cases.
The only way to declare calendars is to use project config or in initial server response (https://bryntum.com/docs/gantt/guide/Gantt/basics/calendars ). But you can also add calendars after gantt init using https://bryntum.com/docs/gantt/api/Gantt/data/CalendarManagerStore

It will be great if you'll attach your runnable app here, we will have a look at the code and suggest you something useful, or wait until the bug is fixed.

All the best,
Alex


Post by saki »

Just to illustrate how Alex' advise can be implemented, here is the code:

import '@bryntum/gantt/gantt.material.css';
import type { Gantt, GanttConfig } from '@bryntum/gantt';
import { BryntumGantt } from '@bryntum/gantt-react';
import * as React from 'react';

const serverData = [
    {
        children: [
            {
                children: false,
                draggable: false,
                endDate: '2021-10-30T00:00:00.000Z',
                id: '5a33440c-c1b5-4945-a999-edb80c774ab4',
                index: 0,
                manuallyScheduled: true,
                name: 'test',
                parentId: '357fb463-97aa-40b9-8ef3-f8ec1dc88f88',
                percentDone: 0,
                resizable: false,
                startDate: '2021-10-01T00:00:00.000Z',
                taskNumber: '1.1',
                type: 'subtask'
            }
        ],
        draggable: true,
        endDate: '2021-10-30T00:00:00.000Z',
        expanded: true,
        id: '357fb463-97aa-40b9-8ef3-f8ec1dc88f88',
        manuallyScheduled: true,
        name: 'build a wall (Link to Tour Eiffel project)',
        percentDone: 100,
        resizable: true,
        startDate: '2021-10-01T00:00:00.000Z',
        taskNumber: '1',
        type: 'task'
    },
    {
        children: [],
        draggable: true,
        endDate: '2021-10-26T00:00:00.000Z',
        expanded: false,
        id: 'af098e9c-0ae4-4821-9d29-bf12ce30e823',
        manuallyScheduled: true,
        name: 'talk to contractor',
        resizable: true,
        startDate: '2021-10-24T00:00:00.000Z',
        taskNumber: '2',
        type: 'task'
    },
    {
        children: [],
        draggable: true,
        endDate: undefined,
        expanded: false,
        id: '38b491a4-012d-4201-bcdd-4539a8cf5bad',
        manuallyScheduled: true,
        name: 'Task without dates',
        resizable: true,
        startDate: undefined,
        taskNumber: '3',
        type: 'task'
    }
];

const calendars = [
    {
        id: 'general',
        name: 'General',
        intervals: [
            {
                recurrentStartDate: 'on Sat at 0:00',
                recurrentEndDate: 'on Mon at 0:00',
                isWorking: false
            }
        ],
        expanded: true,
        children: [
            {
                id: 'business',
                name: 'Business',
                intervals: [
                    {
                        recurrentStartDate: 'every weekday at 12:00',
                        recurrentEndDate: 'every weekday at 13:00',
                        isWorking: false
                    },
                    {
                        recurrentStartDate: 'every weekday at 17:00',
                        recurrentEndDate: 'every weekday at 08:00',
                        isWorking: false
                    }
                ]
            },
            {
                id: 'night',
                name: 'Night shift',
                intervals: [
                    {
                        recurrentStartDate: 'every weekday at 6:00',
                        recurrentEndDate: 'every weekday at 22:00',
                        isWorking: false
                    }
                ]
            }
        ]
    }
];

type Config = Partial<GanttConfig>;

function App() {
    const ganttRef = React.useRef<{ instance: Gantt }>();
    const [tasks, setTasks] = React.useState(serverData);
    const [taskSelected, setTaskSelected] = React.useState(null);

    React.useEffect(() => {
        if (ganttRef && ganttRef.current?.instance) {
            const project = ganttRef.current.instance.project;
            const records = project.calendarManagerStore.add(calendars);
            // @ts-ignore
            project.calendar = records[0];
        }
    }, []);

    const config: Config = {
        barMargin: 10,
        columns: [
            { field: 'taskNumber', text: 'N°', type: 'number', width: 10 },
            { field: 'name', type: 'name', width: 50 }
        ],

        features: {
            // We don't want to use the gantt default edit form
            cellEdit: false,
            columnAutoWidth: true,
            columnLines: false,
            // We don't need the task/link dependencies
            dependencies: false,
            dependencyEdit: false,
            // We want to have the "weekends" greyed on the calendar
            // nonWorkingTime: {
            //     disabled: false,
            //     hideRangesOnZooming: false,
            //     highlightWeekends: true,
            //     maxTimeAxisUnit: 'hour'
            // },
            // Used to have the percent of progress of a task (validated / subtasks progress)
            percentBar: {
                allowResize: false,
                showPercentage: true
            },
            // Used to get the "red line" to show what's the progress on the planning
            progressLine: true,
            // Should show projectStart projectEnd markers
            projectLines: true,
            taskCopyPaste: false,
            // Should be able to change date of a task with drag/drop
            // TODO: we should handle all thoses events in offline mode
            taskDrag: true,
            // It's not really "drag to create" but more "assigne" start/end date to tasks
            // who doesn't already have one
            // TODO: actually use this to open the "create task" dialog with prefill start/end date
            taskDragCreate: true,
            // We don't want to use the default taskEdit form, instead we open our own dialog
            taskEdit: false,
            taskMenu: false,
            taskResize: {
                allowResizeToZero: false
            },
            taskTooltip: true,
            timeRanges: {
                showCurrentTimeLine: {
                    name: 'Today'
                },
                showHeaderElements: true
            },
            tree: true
        },
        // Keep the scroll within project range
        infiniteScroll: true,
        maxHeight: '100vh',
        maxWidth: '100vw',
        // set max zoom level to day
        maxZoomLevel: 11,
        // set minimal zoom level to acceptable values (multiples years with quarters)
        minZoomLevel: 5,
        tbar: [
            {
                checked: true,
                label: 'Show Project Line',
                listeners: {
                    change: ({ checked }: { checked: boolean }) => {
                        if (ganttRef.current?.instance.features.progressLine) {
                            ganttRef.current.instance.features.progressLine.disabled = !checked;
                        }
                    }
                },
                type: 'checkbox'
            },
            {
                inputWidth: '7em',
                label: 'Project Status Date',
                listeners: {
                    change: ({ value }: { value: Date }) => {
                        if (ganttRef.current?.instance.features.progressLine) {
                            ganttRef.current.instance.features.progressLine.statusDate = value;
                        }
                    }
                },
                step: '1d',
                type: 'datefield',
                value: new Date()
            }
        ],
        viewPreset: {
            // By default, show (day, week, month) on the same timebar
            headers: [
                {
                    align: 'start',
                    renderer: (startDate: Date) => startDate.getMonth(),
                    unit: 'month'
                },
                {
                    renderer: (startDate: Date) => startDate.getDate(),
                    unit: 'week'
                },
                {
                    dateFormat: 'DD',
                    unit: 'day'
                }
            ],
            timeResolution: {
                increment: 1,
                unit: 'day'
            }
        }
    };
    return (
        <div style={{ height: '90vh', width: '90vw' }}>
            <div
                style={{
                    backgroundColor: '#ecf0f1',
                    display: taskSelected === null ? 'none' : 'block',
                    height: '100vh',
                    inset: 0,
                    position: 'absolute',
                    width: '100vw',
                    zIndex: 999
                }}
            >
                Popup for {JSON.stringify(taskSelected)}
                <button
                    style={{
                        backgroundColor: 'red',
                        borderRadius: '10px',
                        height: '50px',
                        width: '150px'
                    }}
                    onClick={() => setTaskSelected(null)}
                >
                    Close popup
                </button>
            </div>
            <BryntumGantt
                {...config}
                ref={ganttRef}
                tasks={tasks}
                onTaskDblClick={(e: any) => {
                    const { taskRecord } = e;
                    setTaskSelected(taskRecord.originalData);
                }}
                onTaskDrop={({ taskRecords }: { taskRecords: any[] }) => {
                    const taskRecord = taskRecords[0];
                    const taskData = taskRecord.originalData;
                    const filteredTasks = tasks.filter(t => t.id !== taskData.id);
                    // Save change into the store
                    setTasks([
                        ...filteredTasks,
                        {
                            ...taskData,
                            endDate: taskRecord.endDate,
                            startDate: taskRecord.startDate
                        }
                    ]);
                }}
                onCellDblClick={(e: any) => {
                    // extract the record from the event
                    const { record } = e;
                    ganttRef.current?.instance?.scrollTaskIntoView(record, {
                        animate: true,
                        block: 'center'
                    });
                }}
                taskStore={{ syncDataOnLoad: false }}
            />
        </div>
    );
}

export default App;

Note: It is not optimized and TS errors are just suppressed with //@ts-ignore. Use it as a starting point and refine it to your needs please.


Post Reply