Our blazing fast Grid component built with pure JavaScript


Post by tspikings »

Hi There

I'm trying to change the height of a grouped row header without success. I've tried to follow your guide here:

https://www.bryntum.com/products/grid/docs/api/Grid/feature/Group

No matter what values I put in for Size.Height no change is made to the row height:

HeaderHeight.png
HeaderHeight.png (99.91 KiB) Viewed 234 times

Also when I collapse a header row, one cell has its contents disapear but the child rows do not collapse:

Row Collapse.png
Row Collapse.png (103.57 KiB) Viewed 234 times

I'm sure I'm doing something wrong. Please can someone assist?

This is the HTML for my component:

<div style = "height:100%">
  <div class="flex-container">
    <bryntum-grid
      #mainGrid
      [data] = "personData"
      [tbar]= "mainGridConfig.tbar!"
      [columns] = "mainGridConfig.columns!"
      [groupFeature]= "mainGridConfig.features!.group!"
      [groupSummaryFeature]="mainGridConfig.features!.groupSummary!"
    ></bryntum-grid>
  </div>
</div>

The Config:

import { group } from "@angular/animations";
import { GridConfig, Group, SplitterConfig, SubGrid, SubGridConfig } from "@bryntum/grid";
import { Person } from "src/PutneyBankRESTAPI/Person";

export const mainGridConfig: Partial<GridConfig> = {
  columns : [
    { field : 'firstName', text : 'First Name', width : 100, readOnly : true },
    { field : 'lastName', text : 'Last Name', width : 100, readOnly : true },
    { field : 'adjustedContractedHours', text : 'Contracted Hours', width : 100, readOnly : true, sum : 'sum' },
    { field : 'totalHours', text : 'Total Hours', width : 100, readOnly : true, sum : 'sum' }
  ],
  tbar : [
    {
      type         : 'combo',
      ref          : 'wardCombo',
      label        : 'Ward',
      displayField : 'name',
      value        : 'all',
      name         : 'ward',
      editable     : false
    },
    {
      type         : 'combo',
      ref          : 'templateCombo',
      label        : 'Template',
      displayField : 'name',
      value        : 'all',
      name         : 'template',
      editable     : false
    },
    {
      type         : 'button',
      ref          : 'saveButton',
      icon         : 'b-fa-disk',
      text         : 'Save'
    },
    {
      type         : 'button',
      ref          : 'generateButton',
      icon         : 'b-fa-plus',
      text         : 'Generate'
    }],
  features : {
    group :
    {
      field: 'jobType.name',
      groupSortFn : (a : Person, b : Person) =>
      {
        a.jobType.orderNumber < b.jobType.orderNumber ? -1 : 1
      },
      renderer(data: { value : String; record : any; size : any }) : String
      {
        data.size.height = 300;
        return data.value;
      }
    },
    groupSummary :{
      collapseToHeader : true,
      target : 'header'
    }
  }
};

And the component itself:

import { Component, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { BryntumGridComponent } from '@bryntum/schedulerpro-angular'
import { Column, Combo, Grid, TextField, ColumnStore, Store, Field, Button, SummaryConfig } from '@bryntum/schedulerpro';
import { empty, forkJoin, Observable, Subscription } from 'rxjs';
import { ILocation } from 'src/PutneyBankRESTAPI/Location';
import { LocationDataService } from 'src/PutneyBankRESTAPI/Location.Service';
import { IPerson, Person } from 'src/PutneyBankRESTAPI/Person';
import { PersonService } from 'src/PutneyBankRESTAPI/Person.Service';
import { mainGridConfig } from './app.config';
import { GridWidgetMap } from './gridwidgetmap';
import { StaffingRequirementTemplateService } from 'src/PutneyBankRESTAPI/StaffingRequirementTemplate.Service';
import { IStaffingRequirementTemplate } from 'src/PutneyBankRESTAPI/StaffingRequirementTemplate';
import { IStaffingShiftTemplate } from 'src/PutneyBankRESTAPI/StaffingLocationShiftTemplate';
import { StaffingLocationShiftTemplateService } from 'src/PutneyBankRESTAPI/StaffingLocationShiftTemplate.Service';
import { IStaffingRequirementTemplateAllocation } from 'src/PutneyBankRESTAPI/StaffingRequirementTemplateAllocation';
import { StaffingRequirementTemplateAllocationService } from 'src/PutneyBankRESTAPI/StaffingRequirementTemplateAllocation.Service';
import { MetaData } from 'src/PutneyBankRESTAPI/MetaData';
import { ActivatedRoute, Router } from '@angular/router';
import { IStaffingRequirementBaseline } from 'src/PutneyBankRESTAPI/StaffingRequirementBaseline';
import { StaffingRequirementBaselineService } from 'src/PutneyBankRESTAPI/StaffingRequirementBaseline.Service';
import { ColumnSummaryConfig, GridRowModel, Summary } from '@bryntum/grid';
import { ExtendedColumn } from 'src/ExtendedBryntumClasses/Column';

@Component({
  selector: 'st-roster-template',
  templateUrl: './roster-template.component.html',
  styleUrls: ['./roster-template.component.scss', './grid.classic.scss']
})
export class RosterTemplateComponent implements OnInit {
  @ViewChild('mainGrid') mainGridComponent!: BryntumGridComponent;

  mainGridConfig = mainGridConfig;
  //private router: Router;
  //grid!: Grid;
  locationId! : number;
  templateId! : number;
  mainGrid! : Grid;
  personDataSub! : Subscription;
  personDataObservable! : Observable<Person[]>;
  errorMessage : string = "";
  summaryData! : IStaffingRequirementBaseline[];
  personData! : Person[];
  personDataStore! : Store;
  templateDataSub! : Subscription;
  templateData! : IStaffingRequirementTemplate;
  locationDataSub! : Subscription;
  locationData = {} as ILocation[];
  gridWidgetMap! : GridWidgetMap;
  staffingRequirementTemplateSub! : Subscription;
  staffingRequirementTemplateData! : IStaffingRequirementTemplate[];
  staffingLocationShiftTemplateSub! : Subscription;
  staffingLocationShiftTemplateData! : IStaffingShiftTemplate[];
  combinedCallObservable : Observable<[Person[], IStaffingRequirementTemplateAllocation[]]>;

  staffingRequirementBaselineSub : Subscription;
  staffingRequirementBaselineData! : IStaffingRequirementBaseline[];
  staffingRequirementBaselineDataStore! : Store;
  staffingRequirementBaselineObservable! : Observable<IStaffingRequirementBaseline[]>;

  shiftTemplateStore! : Store;
  templateAllocationData! : IStaffingRequirementTemplateAllocation[];
  templateAllocationDataSub! : Subscription;
  templateAllocationObservable! : Observable<IStaffingRequirementTemplateAllocation[]>;

  constructor(private personDataService : PersonService,
    private locationDataService : LocationDataService,
    private staffingRequirementTemplateDataService : StaffingRequirementTemplateService,
    private staffingLocationShiftTemplateService : StaffingLocationShiftTemplateService,
    private staffingRequirementTemplateAllocationDataService : StaffingRequirementTemplateAllocationService,
    private staffingRequirementBaselineService : StaffingRequirementBaselineService,
    public editor : MatDialog,
    private router : Router,
    private route : ActivatedRoute) {

  }

  ngOnInit(): void
  {
    this.updateLocations();
  }

  ngAfterViewInit(): void
  {
    this.mainGrid = this.mainGridComponent.instance;
    this.gridWidgetMap = new GridWidgetMap();
    this.gridWidgetMap.wardCombo = this.mainGrid.widgetMap["wardCombo"] as Combo;
    this.gridWidgetMap.templateCombo = this.mainGrid.widgetMap["templateCombo"] as Combo;
    this.gridWidgetMap.saveButton = this.mainGrid.widgetMap["saveButton"] as Button;
    this.gridWidgetMap.generateButton = this.mainGrid.widgetMap["generateButton"] as Button;
    this.shiftTemplateStore = new Store();

this.personDataStore = this.mainGrid.store as Store;
this.gridWidgetMap.wardCombo.onChange = (event : any) => {
  console.log(event.value.data.name);
  this.locationId = event.value.data.locationId;
  if(this.locationId != null)
  {
    this.updateStaffingRequirementTemplates(this.locationId);
    this.updateStaffingShiftTemplates(this.locationId);
    this.refreshStaffingRequirementBaselineData (this.locationId);
  }
};

this.gridWidgetMap.saveButton.onClick = (event : any) => {
  this.saveToDatabase();
};

this.gridWidgetMap.generateButton.onClick = (event : any) => {
  this.generatePotentialRoster();
}

this.gridWidgetMap.templateCombo.onChange = (event : any) => {
  console.log(event.value.data.name);
  this.templateId = event.value.data.staffingRequirementTemplateId;
  if(this.templateId != null)
  {
    this.templateDataSub = this.staffingRequirementTemplateDataService.getStaffingRequirementTemplate(this.templateId).subscribe({
      next: templateDataDownload => {
        this.templateData = templateDataDownload;
        console.log("Template Data receieved");
        var colStore : ColumnStore = this.mainGrid.columns as ColumnStore;
        var prefixColumnCount : number = 4;
        this.UpdateMainGridForCorrectNumberOfWeeks(this.templateData.numberOfWeeks, colStore, prefixColumnCount);
      },
      error: err =>
      {
          console.log(err);
          this.errorMessage = err;
      }
    });
    this.refreshGrid(this.locationId, this.templateId);
  }
}
  }

  generatePotentialRoster()
  {
    this.router.navigate(['rosterpotential'], { queryParams: { templateId: this.templateId } });
  }

  saveToDatabase()
  {
    this.mainGrid.maskBody("Saving Data");
    console.log("Save Pressed");
    var listOfAllocations : IStaffingRequirementTemplateAllocation[] = [] as IStaffingRequirementTemplateAllocation[];
    this.personDataStore.forEach(p =>
    {
      p.staffingRequirementTemplateAllocations.forEach(ad =>
      {
        const srta : IStaffingRequirementTemplateAllocation =
        {
          staffingRequirementTemplateAllocationId : ad.staffingRequirementTemplateAllocationId,
          jobTypeId : ad.jobTypeId,
          staffingShiftTemplateId : ad.staffingShiftTemplateId,
          personId : ad.personId,
          staffingRequirementTemplateId : ad.staffingRequirementTemplateId,
          dayNumber : ad.dayNumber,
          jobType : null,
          staffingShiftTemplate : null,
          person : null
        };
        listOfAllocations.push(srta);
      });
    });
    this.staffingRequirementTemplateAllocationDataService.updateListOfAllocations(listOfAllocations, this.templateId);
    //console.log(listOfAllocations);
    window.alert("Template Saved");
    window.location.reload();
    this.mainGrid.unmaskBody();
  }

  onDropDownEditEnds(context : any)
  {
    var newId = context.value;
    var p : any = context.record;
    var column = context.editorContext.column;
    p.staffingRequirementTemplateAllocations[(column.parentIndex - 4)].staffingShiftTemplateId = newId;
    let pObject : Person = Object.assign(new Person(), p.data);
    p.totalHours = pObject.updateTotalHours();
    return true;
  }

  GetSummary(sum : any, record : any)
  {
    var i = 1;
  }

  UpdateMainGridForCorrectNumberOfWeeks(numberOfWeeks : number, colStore : ColumnStore, prefixColumnCount : number)
  {
    console.log("Updating Main grid for " + numberOfWeeks + " weeks.")
    const weekday = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"];
    while(colStore.count >= (prefixColumnCount + 1))
    {
      colStore.remove(colStore.last);
    }
    var numberOfDays : number = 7 * numberOfWeeks;

for(var num = 0; num < numberOfDays; num++)
{
  var fieldString = "data.staffingRequirementTemplateAllocations[" + (num) + "].staffingShiftTemplateId";
  const col:ExtendedColumn = <ExtendedColumn><unknown>({
    text: weekday[num % 7],
    field: fieldString,
    dayIndexNumber: num + 1,
    flex: 1,
    finalizeCellEdit : this.onDropDownEditEnds,
    editor : {
          type : 'combo',
          items : this.shiftTemplateStore,
          // specify valueField'/'displayField' to match the data format in the store
          valueField : 'staffingShiftTemplateId',
          displayField : 'name'
    },
    summaries : [
    ],
    renderer(data: { value : String; record : any; column : Column, grid : Grid, size : any }) : String
    {
      var id : Number | null = null;
      if(data != null && data.record != null && data.record.staffingRequirementTemplateAllocations != null && data.column != null && data.column.parentIndex != null)
      {
        var indexToUse : number = (data.column.parentIndex - 4);
        if(data.record.staffingRequirementTemplateAllocations[indexToUse] != null)
        {
          id = data.record.staffingRequirementTemplateAllocations[indexToUse].staffingShiftTemplateId;
        }
      }
      var grid : Grid = data.grid;
      var result : string = "";
      if(id != null)
      {
        //result = id.toString();
        var x = (data.column as any);
        var y = x.editor.store;
        var z = y.findByField("staffingShiftTemplateId", id);
        var v = z[0].data.name;
        result = v;
      }
      return result;
    }
  });
  col.summaries = [];
  this.staffingLocationShiftTemplateData.forEach(t =>
  {
    var s =
    { sum : (sum, record) =>
      {
        var store : Store = record.stores[0];
        var people = store.records.filter((p : any) => p.jobTypeId == record.jobTypeId);
        var result : String = "";
        var assignedShifts : number = 0;
        if(people.length > 0)
        {
          var relevantAllocations : IStaffingRequirementTemplateAllocation[] = [];
          var dayNumber : number = col.dayIndexNumber;
          people.forEach((p) => {
            if((p as any).staffingRequirementTemplateAllocations != null)
            {
              var allocations = (p as any).staffingRequirementTemplateAllocations.filter((a : any) => a.dayNumber == dayNumber && a.staffingShiftTemplateId == t.staffingShiftTemplateId);

              if(allocations.length > 0)
              {
                relevantAllocations = relevantAllocations.concat(allocations);
              }
            }
          });
          assignedShifts = relevantAllocations.length;
          var baseline : number = 0;
          var baselineRecords = this.staffingRequirementBaselineData.filter((a : IStaffingRequirementBaseline) => a.dayOfWeekId == (weekday.indexOf(col.text) + 1) && a.jobTypeId == (people[0] as any).jobTypeId && a.staffingShiftTemplateId == t.staffingShiftTemplateId);
          if(baselineRecords.length > 0)
          {
            baseline = baselineRecords[0].staffRequired;
          }
        }
        result += t.name + " " + assignedShifts.toString() + "/" + baseline.toString();
        return result;
      }
    };
    col.summaries.push(s);
  });

  colStore.add(col);
}
  }

  refreshStaffingRequirementBaselineData(locationId : number)
  {
    this.staffingRequirementBaselineObservable = this.staffingRequirementBaselineService.getMaximumBaselineByLocation(locationId);
    this.staffingRequirementBaselineSub = this.staffingRequirementBaselineObservable.subscribe({
      next: baselineData =>{
        this.staffingRequirementBaselineData = baselineData;
        console.log("Baseline Data Received");
      },
      error: err =>
      {
          console.log(err);
          this.errorMessage = err;
      }
    });
  }

  processPersonAndTemplateResult(personResult : Person[], templateResult : IStaffingRequirementTemplateAllocation[])
  {
    this.personData = personResult;
    console.log("Person Data Received");
    this.templateAllocationData = templateResult;
    console.log("Template Allocation Data Recieved");
    this.personDataStore.removeAll();
    this.personData.forEach(p => {
      p.staffingRequirementTemplateAllocations = this.templateAllocationData.filter(t => t.personId == p.personId)
      let pObject : Person = Object.assign(new Person(), p);
      p.totalHours = pObject.updateTotalHours();
      p.adjustedContractedHours = p.contractedHours * this.templateData.numberOfWeeks;
      this.personDataStore.add(p);
    });
  }

  refreshGrid(locationId : number, templateId : number) : void
  {
    this.mainGrid.mask("Loading Data");
    if(locationId != null && templateId != null)
    {
      this.templateAllocationObservable = this.staffingRequirementTemplateAllocationDataService.getStaffingRequirementTemplatesAllocations(templateId);
      this.personDataObservable = this.personDataService.getPermanentStaffForLocation(locationId);
      this.combinedCallObservable = forkJoin([this.personDataObservable, this.templateAllocationObservable]);
      this.combinedCallObservable.subscribe({
        next: resultVar => {
          this.processPersonAndTemplateResult(resultVar[0], resultVar[1])
          this.mainGrid.refreshRows();
          this.mainGrid.unmaskBody();
        }
      });
      console.log("Test");
    }
    else
    {
      this.mainGrid.unmaskBody();
    }
  }

  updateLocations() : void
  {
    this.locationDataSub = this.locationDataService.getLocations().subscribe({
        next: locationDataDownload => {
            this.locationData = locationDataDownload;
            console.log("Location Data received");
            var s : Store = this.gridWidgetMap.wardCombo.store as Store;
            this.locationData.forEach(l => {
                s.add(l);
            });
        },
        error: err =>
        {
            console.log(err);
            this.errorMessage = err;
        }
    })
  }

  updateStaffingShiftTemplates(locationId : number) : void
  {
    this.staffingLocationShiftTemplateSub = this.staffingLocationShiftTemplateService.getStaffingLocationShiftTemplate(locationId).subscribe({
      next: staffingLocationShiftTemplateDataDownload => {
        this.staffingLocationShiftTemplateData = staffingLocationShiftTemplateDataDownload;
        console.log("Staffing Location Templates Data received");
        var emptyShift : IStaffingShiftTemplate = { endTime : "00:00", "breakMinutes" : 0, startTime : "00:00", staffingShiftTemplateId : 0, name : "None", orderNumber : 0};
        this.shiftTemplateStore.removeAll();
        MetaData.StaffingShiftTemplates = this.staffingLocationShiftTemplateData;
        this.shiftTemplateStore.add(emptyShift);
        this.staffingLocationShiftTemplateData.forEach(t => {
          this.shiftTemplateStore.add(t);
        })
      },
      error: err =>
      {
        console.log(err);
        this.errorMessage = err;
      }
    })
  }

  updateStaffingRequirementTemplates(locationId : number) : void
  {
    this.staffingRequirementTemplateSub = this.staffingRequirementTemplateDataService.getStaffingRequirementTemplates(locationId).subscribe({
      next: staffingRequirementTemplateDataDownload => {
        this.staffingRequirementTemplateData = staffingRequirementTemplateDataDownload;
        console.log("Staffing Requirement Template Data received");
        var s : Store = this.gridWidgetMap.templateCombo.store as Store;
        s.removeAll();
        this.staffingRequirementTemplateData.forEach(t => {
          s.add(t);
        });
      },
      error: err =>
      {
        console.log(err);
        this.errorMessage = err;
      }
    })
  }
}

Also if it is relevant this is the ExtendedColumn class:

import { Column } from "@bryntum/grid";

export class ExtendedColumn extends Column{
  summaries : Object[];
  dayIndexNumber : number;
}

Post by mats »

Looks like a bug, we'll get that fixed. Thanks for reporting! https://github.com/bryntum/support/issues/6976


Post by tspikings »

Thank you Mats, will this resolve the second problem I had with the collapse button as well?

This only occurs when I have my custom summaries being added. Please see the second screenshot. If I comment out the below code then the pressing the collapse button on the header will hide all the rows associated with the header. When I leave this code in pressing the collapse button just hides the contents of the cell for the first header column:

this.staffingLocationShiftTemplateData.forEach(t =>
      {
        var s =
        { sum : (sum, record) =>
          {
            var store : Store = record.stores[0];
            var people = store.records.filter((p : any) => p.jobTypeId == record.jobTypeId);
            var result : String = "";
            var assignedShifts : number = 0;
            if(people.length > 0)
            {
              var relevantAllocations : IStaffingRequirementTemplateAllocation[] = [];
              var dayNumber : number = col.dayIndexNumber;
              people.forEach((p) => {
                if((p as any).staffingRequirementTemplateAllocations != null)
                {
                  var allocations = (p as any).staffingRequirementTemplateAllocations.filter((a : any) => a.dayNumber == dayNumber && a.staffingShiftTemplateId == t.staffingShiftTemplateId);

              if(allocations.length > 0)
              {
                relevantAllocations = relevantAllocations.concat(allocations);
              }
            }
          });
          assignedShifts = relevantAllocations.length;
          var baseline : number = 0;
          var baselineRecords = this.staffingRequirementBaselineData.filter((a : IStaffingRequirementBaseline) => a.dayOfWeekId == (weekday.indexOf(col.text) + 1) && a.jobTypeId == (people[0] as any).jobTypeId && a.staffingShiftTemplateId == t.staffingShiftTemplateId);
          if(baselineRecords.length > 0)
          {
            baseline = baselineRecords[0].staffRequired;
          }
        }
        result += t.name + " " + assignedShifts.toString() + "/" + baseline.toString();
        return result;
      }
    };
    col.summaries.push(s);
  });

Post by mats »

Hard to say, you can try using a nightly build (tomorrow) and see if the 2nd issue is also fixed? Please let us know.


Post Reply