Dear Bryntum:
I had a question regarding how to get filtering to work on new custom columns we created in our Gantt project.
I'll give to you the code below to show how I'm currently configuring it. I've not been able to get the custom columns to filter correctly -- however, please note the other columns (Name, Start Date, End Date, Duration, Effort) are working fine. I currently have the filter set to true (in features in gantt config) -- as shown in the code below (Gantt.js).
features: {
filter: true,
},Code below given for reference on how I'm using filtering within each of my custom columns.
Please note the custom columns are shown as "Status" (StatusColumn.js), "Assigned To" (AssignedToColumn.js), "Responsiblity" (TaskResponsibility.js), "Visibility" (ExternalVisibilityColumn.js). I have also included a screenshot below to show each of the different columns displayed (custom columns are highlighted in red).
Could you please share with me info on how to make the custom columns filtering to work correctly? What am I doing wrong on this? Thank you so much for all your help. I will look forward to your response.
Gantt.js
import { BryntumGantt } from '@bryntum/gantt-react'
import '@bryntum/gantt/gantt.stockholm.css'
import { useGetGantt } from '~/views/hooks/Gantt/useGetGantt'
import './StatusColumn'
import './AssignedToColumn'
import './TaskResponsibilityColumn'
import './PriorityColumn'
import './ExternalVisibilityColumn'
import Task from './Task'
import './GanttToolbar'
import { useRouter } from 'next/router'
import React, { useEffect, useRef, useState } from 'react'
import Loader from '~/views/components/ui/Loader'
export default function Gantt() {
const [ganttData, setGanttData] = useState(null)
const [delayed, setDelayed] = useState(true)
const ganttRef = useRef()
const router = useRouter()
const projectId = router.query.projectId
const { data } = useGetGantt({
projectId,
})
useEffect(() => {
if (data) {
setDelayed(false)
setGanttData((ganttConfig.project.tasksData = data.ganttChartTimelines))
}
}, [data])
const ganttConfig = {
project: {
// Let the Project know we want to use our own Task model with custom fields / methods
taskModelClass: Task,
autoLoad: true,
manuallyScheduled: true,
hoursPerDay: 24,
daysPerWeek: 5,
daysPerMonth: 20,
validateResponse: true,
tasksData: ganttData,
},
taskMenu: {
items: {
indent: false,
outdent: false,
convertToMilestone: false,
},
},
tbar: {
type: 'gantttoolbar',
},
columns: [
{ type: 'name', field: 'name', width: 230 },
{ type: 'date', field: 'startDate', text: 'Start Date', width: 50 },
{ type: 'date', field: 'endDate', text: 'Due Date', width: 50 },
{ type: 'duration', width: 70 },
{ type: 'effort' },
{ type: 'statuscolumn' },
{ type: 'assignedcolumn' },
{ type: 'taskresponsibility' },
{ type: 'prioritycolumn' },
{ type: 'visibilitycolumn' },
],
viewPreset: {
base: 'weekAndDay',
displayDateFormat: 'YYYY-MM-DD',
},
barMargin: 10,
headerMenuFeature: true,
minHeight: 1200,
autoHeight: true,
excelExporterFeature: {
// Choose the date format for date fields
dateFormat: 'YYYY-MM-DD HH:mm',
},
pdfExportFeature: {
exportServer: 'http://localhost:3000',
// Development config
translateURLsToAbsolute: 'http://localhost:3000',
clientURL: 'http://localhost:3000',
// For production replace with this one. See README.md for explanation
// translateURLsToAbsolute : 'http://localhost:8080/resources/', // Trailing slash is important
keepPathName: false,
openInNewTab: true,
openAfterExport: true,
},
features: {
filter: true,
},
}
return !delayed ? (
<>
<BryntumGantt {...ganttConfig} ref={ganttRef} />
</>
) : (
<Loader />
)
}StatusColumn.js
import { Column, ColumnStore } from '@bryntum/gantt'
class StatusColumn extends Column {
static get $name() {
return 'StatusColumn'
}
static get type() {
return 'statuscolumn'
}
static get isGanttColumn() {
return true
}
static get defaults() {
return {
// Set your default instance config properties here
field: 'status',
text: 'Status',
editor: false,
cellCls: 'b-status-column-cell',
htmlEncode: false,
filterable: {
filterField: {
type: 'combo',
items: [
'Not Started',
'Working on it',
'Stuck',
'Done',
'Not Applicable',
],
},
},
}
}
renderer({ record }) {
const { originalData, statusStyle } = record
return originalData.status
? [
// {
// tag: 'i',
// className: `b-fa b-fa-circle ${statusStyle} `,
// },
capitalizeFirstLetter(
originalData.status.replace('_', ' ').toLowerCase(),
),
]
: ''
}
}
function capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
ColumnStore.registerColumnType(StatusColumn)Task.js
import { TaskModel } from '@bryntum/gantt'
export default class Task extends TaskModel {
static get fields() {
return [{ name: 'deadline', type: 'date' }]
}
static get $name() {
return 'StatusColumn'
}
get isLate() {
return this.deadline && Date.now() > this.deadline
}
get status() {
let status = 'Not started'
if (this.isCompleted) {
status = 'Completed'
} else if (this.endDate < Date.now()) {
status = 'Late'
} else if (this.isStarted) {
status = 'Started'
}
return status
}
get statusStyle() {
return `b-status-${this.status.toLowerCase().replace(' ', '')}`
}
}AssignedToColumn.js
import { Column, ColumnStore } from '@bryntum/gantt'
class AssignedToColumn extends Column {
static get $name() {
return 'AssignedToColumn'
}
static get type() {
return 'assignedcolumn'
}
static get isGanttColumn() {
return true
}
static get defaults() {
return {
// Set your default instance config properties here
field: 'assignee',
text: 'Assigned To',
editor: false,
htmlEncode: false,
width: 100,
showAvatars: true,
itemTpl: (assignment) => assignment.resourceName,
editor: {
chipView: {
itemTpl: (assignment) => assignment.resourceName,
},
},
}
}
renderer({ record }) {
const { originalData, statusStyle } = record
return originalData.assignee
? [`${originalData.assignee.firstName} ${originalData.assignee.lastName}`]
: ''
}
}
ColumnStore.registerColumnType(AssignedToColumn)PriorityColumn.js
import { Column, ColumnStore } from '@bryntum/gantt'
class PriorityColumn extends Column {
static get $name() {
return 'PriorityColumn'
}
static get type() {
return 'prioritycolumn'
}
static get isGanttColumn() {
return true
}
static get defaults() {
return {
// Set your default instance config properties here
field: 'priority',
text: 'Priority',
flex: 1,
filterable: {
filterField: {
type: 'combo',
multiSelect: true,
valueField: 'priority',
displayField: 'priority',
items: ['Low', 'Medium', 'High'],
},
filterFn: ({ record, value }) =>
!value.length || value.includes(record),
},
}
}
renderer({ record }) {
const { originalData, statusStyle } = record
return originalData.priority
? [capitalizeFirstLetter(originalData.priority.toLowerCase())]
: ''
}
}
function capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
ColumnStore.registerColumnType(PriorityColumn)TaskResponsiblityColumn.js
import { Column, ColumnStore } from '@bryntum/gantt'
class TaskResponsibilityColumn extends Column {
static get $name() {
return 'TaskResponsibility'
}
static get type() {
return 'taskresponsibility'
}
static get isGanttColumn() {
return true
}
static get defaults() {
return {
// Set your default instance config properties here
field: 'taskresponsibility',
text: 'Responsibility',
editor: false,
htmlEncode: false,
flex: 1,
filterable: {
filterField: {
type: 'combo',
multiSelect: true,
editable: false,
items: ['Internal', 'Customer', 'Third Party'],
},
filterFn: ({ record, value }) =>
!value.length || value.includes(record.city),
},
}
}
renderer({ record }) {
const { originalData, statusStyle } = record
return originalData.responsibility
? [capitalizeFirstLetter(originalData.responsibility.toLowerCase())]
: ''
}
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
ColumnStore.registerColumnType(TaskResponsibilityColumn)ExternalVisiblityColumn.js
import { Column, ColumnStore } from '@bryntum/gantt'
class ExternalVisibilityColumn extends Column {
static get $name() {
return 'ExternalVisibilityColumn'
}
static get type() {
return 'visibilitycolumn'
}
static get isGanttColumn() {
return true
}
static get defaults() {
return {
// Set your default instance config properties here
field: 'visibility',
text: 'Visibility',
editor: false,
htmlEncode: false,
flex: 1,
filterable: {
filterField: {
type: 'combo',
multiSelect: true,
editable: false,
items: ['Visible', 'Name only', 'Hidden'],
},
}
}
renderer({ record }) {
const { originalData, statusStyle } = record
return originalData.visibility
? [
capitalizeFirstLetter(
originalData.visibility.replace('_', ' ').toLowerCase(),
),
]
: ''
}
}
function capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
ColumnStore.registerColumnType(ExternalVisibilityColumn)GanttToolbar.js
import { Toolbar, Toast, DateHelper, CSSHelper } from '@bryntum/gantt'
export default class GanttToolbar extends Toolbar {
static get type() {
return 'gantttoolbar'
}
static get $name() {
return 'GanttToolbar'
}
// Called when toolbar is added to the Gantt panel
set parent(parent) {
super.parent = parent
const me = this
me.gantt = parent
parent.project.on({
thisObj: me,
})
me.styleNode = document.createElement('style')
document.head.appendChild(me.styleNode)
}
get parent() {
return super.parent
}
static get configurable() {
return {
items: [
{
type: 'buttonGroup',
items: [
{
ref: 'expandAllButton',
icon: 'b-fa b-fa-angle-double-down',
tooltip: 'Expand all',
onAction: 'up.onExpandAllClick',
},
{
ref: 'collapseAllButton',
icon: 'b-fa b-fa-angle-double-up',
tooltip: 'Collapse all',
onAction: 'up.onCollapseAllClick',
},
],
},
{
type: 'buttonGroup',
items: [
{
ref: 'zoomInButton',
icon: 'b-fa b-fa-search-plus',
tooltip: 'Zoom in',
onAction: 'up.onZoomInClick',
},
{
ref: 'zoomOutButton',
icon: 'b-fa b-fa-search-minus',
tooltip: 'Zoom out',
onAction: 'up.onZoomOutClick',
},
{
ref: 'zoomToFitButton',
icon: 'b-fa b-fa-compress-arrows-alt',
tooltip: 'Zoom to fit',
onAction: 'up.onZoomToFitClick',
},
{
ref: 'previousButton',
icon: 'b-fa b-fa-angle-left',
tooltip: 'Previous time span',
onAction: 'up.onShiftPreviousClick',
},
{
ref: 'nextButton',
icon: 'b-fa b-fa-angle-right',
tooltip: 'Next time span',
onAction: 'up.onShiftNextClick',
},
],
},
{
type: 'buttonGroup',
items: [
{
type: 'button',
ref: 'featuresButton',
icon: 'b-fa b-fa-tasks',
text: 'Features',
tooltip: 'Toggle features',
toggleable: true,
menu: {
onItem: 'up.onFeaturesClick',
onBeforeShow: 'up.onFeaturesShow',
items: [
{
text: 'Draw dependencies',
feature: 'dependencies',
checked: false,
},
{
text: 'Task labels',
feature: 'labels',
checked: true,
},
{
text: 'Project lines',
feature: 'projectLines',
checked: false,
},
{
text: 'Highlight non-working time',
feature: 'nonWorkingTime',
checked: false,
},
{
text: 'Enable cell editing',
feature: 'cellEdit',
checked: false,
},
{
text: 'Show baselines',
feature: 'baselines',
checked: false,
},
{
text: 'Show rollups',
feature: 'rollups',
checked: false,
},
{
text: 'Show progress line',
feature: 'progressLine',
checked: false,
},
{
text: 'Hide schedule',
cls: 'b-separator',
subGrid: 'normal',
checked: false,
},
],
},
},
{
type: 'button',
ref: 'settingsButton',
icon: 'b-fa b-fa-cogs',
text: 'Settings',
tooltip: 'Adjust settings',
toggleable: true,
menu: {
type: 'popup',
anchor: true,
cls: 'settings-menu',
layoutStyle: {
flexDirection: 'column',
},
onBeforeShow: 'up.onSettingsShow',
items: [
{
type: 'slider',
ref: 'rowHeight',
text: 'Row height',
width: '12em',
showValue: true,
min: 30,
max: 70,
onInput: 'up.onSettingsRowHeightChange',
},
{
type: 'slider',
ref: 'barMargin',
text: 'Bar margin',
width: '12em',
showValue: true,
min: 0,
max: 10,
onInput: 'up.onSettingsMarginChange',
},
{
type: 'slider',
ref: 'duration',
text: 'Animation duration ',
width: '12em',
min: 0,
max: 2000,
step: 100,
showValue: true,
onInput: 'up.onSettingsDurationChange',
},
],
},
},
],
},
],
}
}
onPdfExportClick() {
const { gantt } = this
gantt.features.pdfExport.showExportDialog()
}
setAnimationDuration(value) {
const me = this,
cssText = `.b-animating .b-gantt-task-wrap { transition-duration: ${
value / 1000
}s !important; }`
me.gantt.transitionDuration = value
if (me.transitionRule) {
me.transitionRule.cssText = cssText
} else {
me.transitionRule = CSSHelper.insertRule(cssText)
}
}
async onAddTaskClick() {
const { gantt } = this,
added = gantt.taskStore.rootNode.appendChild({
name: 'New task',
duration: 1,
})
// wait for immediate commit to calculate new task fields
await gantt.project.commitAsync()
// scroll to the added task
await gantt.scrollRowIntoView(added)
gantt.features.cellEdit.startEditing({
record: added,
field: 'name',
})
}
onEditTaskClick() {
const { gantt } = this
if (gantt.selectedRecord) {
gantt.editTask(gantt.selectedRecord)
} else {
Toast.show('First select the task you want to edit')
}
}
onExpandAllClick() {
this.gantt.expandAll()
}
onCollapseAllClick() {
this.gantt.collapseAll()
}
onZoomInClick() {
this.gantt.zoomIn()
}
onZoomOutClick() {
this.gantt.zoomOut()
}
onZoomToFitClick() {
this.gantt.zoomToFit({
leftMargin: 50,
rightMargin: 50,
})
}
onShiftPreviousClick() {
this.gantt.shiftPrevious()
}
onShiftNextClick() {
this.gantt.shiftNext()
}
onStartDateChange({ value, oldValue }) {
if (!oldValue) {
// ignore initial set
return
}
this.gantt.startDate = DateHelper.add(value, -1, 'week')
this.gantt.project.setStartDate(value)
}
onFilterChange({ value }) {
if (value === '') {
this.gantt.taskStore.clearFilters()
} else {
value = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
this.gantt.taskStore.filter({
filters: (task) => task.name && task.name.match(new RegExp(value, 'i')),
replace: true,
})
}
}
onFeaturesClick({ source: item }) {
const { gantt } = this
if (item.feature) {
const feature = gantt.features[item.feature]
feature.disabled = !feature.disabled
} else if (item.subGrid) {
const subGrid = gantt.subGrids[item.subGrid]
subGrid.collapsed = !subGrid.collapsed
}
}
onFeaturesShow({ source: menu }) {
const { gantt } = this
menu.items.map((item) => {
const { feature } = item
if (feature) {
// a feature might be not presented in the gantt
// (the code is shared between "advanced" and "php" demos which use a bit different set of features)
if (gantt.features[feature]) {
item.checked = !gantt.features[feature].disabled
}
// hide not existing features
else {
item.hide()
}
} else {
item.checked = gantt.subGrids[item.subGrid].collapsed
}
return item
})
}
onSettingsShow({ source: menu }) {
const { gantt } = this,
{ rowHeight, barMargin, duration } = menu.widgetMap
rowHeight.value = gantt.rowHeight
barMargin.value = gantt.barMargin
barMargin.max = gantt.rowHeight / 2 - 5
duration.value = gantt.transitionDuration
}
onSettingsRowHeightChange({ value }) {
this.gantt.rowHeight = value
this.widgetMap.settingsButton.menu.widgetMap.barMargin.max = value / 2 - 5
}
onSettingsMarginChange({ value }) {
this.gantt.barMargin = value
}
onSettingsDurationChange({ value }) {
this.gantt.transitionDuration = value
this.styleNode.innerHTML = `.b-animating .b-gantt-task-wrap { transition-duration: ${
value / 1000
}s !important; }`
}
onCriticalPathsClick({ source }) {
this.gantt.features.criticalPaths.disabled = !source.pressed
}
}
// Register this widget type with its Factory
GanttToolbar.initClass()