Dear Bryntum Team,
We are currently using Bryntum Grid v6.2.0 with Vue 3 and encountering an issue with sorting the datetime column when the grid is bound using mock AJAX data.
Issue:
We need to sort dates in the format: MM/dd/YYYY hh:mm AM/PM.
Although we convert the created_at field to a date object (new Date(item.created_at)), sorting still treats it as a string.
Interestingly, sorting works fine in the Tree Grid, but breaks in the Paging Grid.
Example Scenario for Better Understanding:
<bryntum-grid ref="childGrid" :config="gridConfig" :height="'80vh'" />
this.gridConfig = {
tbar: {
items: [
{
type: "Combo",
label: "Item Per Page",
items: this.itemSize,
value: this.itemSize[1],
editable: false,
style: {
float: "right",
position: "absolute",
top: 10,
right: 10,
width: "200px",
},
onChange: ({ value }) => {
this.$refs.childGrid.instance.value.store.pageSize = value;
},
},
],
},
bbar: {
type: "pagingtoolbar",
},
columns: [
{
order: 1,
field: "Id",
text: "#",
width: 20,
default: true,
action: false,
},
{
order: 2,
field: "PageName",
text: "Page Name",
autoWidth: true,
default: true,
action: false,
},
{
order: 3,
field: "Suggestions",
text: "Suggestions",
autoWidth: true,
default: true,
action: false,
},
{
order: 4,
field: "SuggestionTypeDes",
text: "Suggestion Type",
autoWidth: true,
default: true,
action: false,
},
{
order: 5,
field: "username",
text: "Suggested By",
autoWidth: true,
default: true,
action: true,
},
{
order: 6,
text: "Date",
field: "created_at",
type: "date",
autoWidth: true,
default: false,
action: false,
format: "MM/DD/YY hh:mm A",
},
{
order: 7,
field: "sgstatus",
text: "Current Status",
autoWidth: true,
default: true,
action: false,
filterable: {
filterField: {
type: "combo",
valueField: "value",
displayField: "label",
cls: "issues-combo",
picker: { cls: "ccstatus-list", allowGroupSelect: false },
store: {
data: [
{ label: "Approved", value: "1" },
{ label: "On-Hold", value: "2" },
{ label: "Pending", value: "3" },
{ label: "Denies", value: "0" },
],
},
multiSelect: false,
},
},
renderer: ({ value, cellElement }) => {
const cstatus = this.getCurrentStatus(value);
let cssClass = "";
if (cstatus === "Denies") {
cssClass = "sg-denies";
} else if (cstatus === "Approved") {
cssClass = "sg-approve";
} else if (cstatus === "On-Hold") {
cssClass = "sg-onhold";
} else {
cssClass = "sg-waiting";
}
cellElement.classList.add(cssClass);
return cstatus;
},
},
{
order: 11,
field: "LastNote",
text: "Last Note",
autoWidth: true,
default: true,
action: false,
},
{
text: "Notes",
collapsible: true,
collapsed: true,
default: true,
action: true,
children: [
{
order: 10,
field: "NotesGroup",
text: "Notes Group",
autoWidth: true,
default: true,
action: false,
},
{
order: 8,
field: "LastAddedBy",
text: "Last Added By",
autoWidth: true,
default: true,
action: false,
},
{
order: 9,
field: "AssignedTo",
text: "Assigned To",
autoWidth: true,
default: true,
action: false,
},
],
},
{
order: 12,
field: "sgstatus",
region: "right",
text: "sgstatus",
width: 120,
default: true,
action: false,
type: "widget",
widgets: [
{
type: "combo",
items: this.statusOptions, // The data for your combo box
readOnly: true, // Make it read-only
editable: false,
onSelect: ({ source: combo }) => {
const { record } = combo.cellInfo;
const originalValue = record.sg_status;
if (originalValue !== combo.value) {
this.$swal
.fire({
title: "Are you sure?",
text: "You want to update the status!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes!",
})
.then((result) => {
if (result.isConfirmed) {
this.updateSgStatus(
combo.value,
record.Id,
record.parentIndex
);
} else {
combo.value = originalValue;
}
});
}
},
},
],
},
{
order: 13,
field: "filename",
text: "File(s)",
autoWidth: true,
region: "right",
filterable: false,
sortable: false,
callOnFunctions: true,
default: true,
action: false,
renderer: ({ record, cellElement }) => {
const link = record.data.link;
const filenames = record.data.link
? record.data.link.split(",")
: [];
// Check if both link and at least one filename are not null or undefined
if (link !== null && link !== undefined && filenames.length > 0) {
cellElement.innerHTML = "";
for (const filename of filenames) {
let isPdf = filename.trim().toLowerCase().endsWith(".pdf");
const buttonConfig = isPdf
? {
cls: "b-widget b-button b-raised b-rounded b-green",
icon: "b-icon b-fa-file-pdf",
tooltip: "Open PDF",
onClick: () => this.showmodeldocument(filename, record),
}
: {
cls: "b-widget b-button b-raised b-rounded b-green",
icon: "b-icon b-fa-image",
tooltip: "Open File",
onClick: () => this.showmodeldocument(filename, record),
};
// Create a button element using plain HTML
const button = document.createElement("button");
button.className = buttonConfig.cls;
button.innerHTML = `<i class="${buttonConfig.icon}"></i>`;
button.title = buttonConfig.tooltip;
button.addEventListener("click", buttonConfig.onClick);
// Clear the cell content before appending the button
// Append the button element to the cell
cellElement.appendChild(button);
}
} else {
// If link or no filenames are provided, clear the cell content
cellElement.innerHTML = "";
}
},
style: "margin-right: 5px;",
},
{
field: "show_edit",
text: "Edit",
width: 40,
region: "right",
default: true,
action: true,
type: "widget",
widgets: [
{
type: "button",
cls: "b-raised b-blue b-rounded",
icon: "b-icon b-fa-pen",
onClick: ({ source: btn }) => {
const { record } = btn.cellInfo;
this.review(record);
},
},
],
renderer: ({ record }) => {
if (!this.userHasPermission(["can view suggestions"])) {
return "";
}
},
},
{
field: "show_delete",
text: "",
width: 40,
default: true,
action: true,
type: "widget",
widgets: [
{
type: "button",
cls: "b-raised b-red b-rounded",
icon: "b-icon b-fa-trash",
onClick: ({ source: btn }) => {
const { record } = btn.cellInfo;
this.deleteRecord(record.Id);
},
},
],
renderer: ({ record }) => {
if (!this.userHasPermission(["can delete suggestions"])) {
return "";
}
},
},
],
features: {
filterBar: true,
cellMenu: false,
cellEdit: false,
stripe: true,
quickFind: true,
regionResize: true,
rowExpander: {
// This will put the expander button as the last column // Expander column configs
column: {
width: 120,
align: "center",
readOnly: true,
},
widget: {
type: "container",
cls: "action-panel",
items: [
{
type: "textfield",
ref: "txtnotes",
flex: 3,
placeholder: "Notes",
value: "",
width: 20,
},
{
type: "combo",
flex: 3,
ref: "cmbgroup",
label: "Notes Group",
items: this.selectedOption,
placeholder: "Select Notes Group",
editable: false,
onSelect: ({ source: combo, record }) => {
const selectedValue = combo?.value;
// Avoid executing the logic if the value is null
if (selectedValue === null || selectedValue === undefined) {
return;
}
this.bindUsers(combo, selectedValue, record.parentIndex);
},
},
{
type: "combo",
ref: "cmbuser",
flex: 3,
label: "Assigned To",
valueField: "id",
textField: "text",
placeholder: "Select Employee",
editable: false,
},
{
type: "button",
text: "Submit",
flex: 1,
ref: "btn1",
cls: "b-raised b-blue",
onClick: ({ source }) => {
let vm = this;
const { rowExpander } = source.up("grid").features,
record = rowExpander.getExpandedRecord(source.owner);
const notesData = source.closest("container").widgetMap;
vm.onNoteSubmit(record, 0, notesData);
},
},
{
type: "button",
ref: "btn2",
flex: 1,
text: "Submit & Notify",
cls: "b-raised b-green",
onClick: ({ source }) => {
let vm = this;
const { rowExpander } = source.up("grid").features,
record = rowExpander.getExpandedRecord(source.owner);
const notesData = source.closest("container").widgetMap;
vm.onNoteSubmit(record, 1, notesData);
},
},
{
type: "grid",
ref: "notesgrid",
cls: "col-md-12 custom-notes-grid",
height: 200,
features: {
headerMenu: false,
cellEdit: false,
cellMenu: false,
},
columns: [
{ text: "Assigned To", field: "NameAssign", flex: 1 },
{ text: "Note", field: "Des", flex: 1 },
{ text: "Added By", field: "Name", flex: 1 },
{
text: "Date",
field: "DateUpdate",
flex: 1,
renderer: ({ record, value }) => {
if (!value) return "";
// Assume input is in UTC: "2025-05-26 05:44:23.407"
// Add 'Z' to tell JS it's a UTC timestamp
const utcString = value.replace(" ", "T") + "Z"; // --> "2025-05-26T05:44:23.407Z"
const d = new Date(utcString); // Now JS treats it as UTC and converts to local
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
let hours = d.getHours();
const minutes = String(d.getMinutes()).padStart(2, "0");
const ampm = hours >= 12 ? "pm" : "am";
hours = hours % 12 || 12;
const formattedDate = `${month}-${day}-${year} ${hours}:${minutes} ${ampm}`;
// Optional: set formatted date to record field
record.set("Date", formattedDate);
return formattedDate;
},
},
],
store: {
data: [],
},
},
],
},
listeners: {
expand: async (source) => {
try {
const cmbGroup = source.widget.items.find(
(item) => item.ref === "cmbgroup"
);
cmbGroup.items = this.selectedOption;
const { record } = source;
const notesgrid = source.widget.items.find(
(item) => item.ref === "notesgrid"
);
const response = await this.getNotes(record);
//bind the subgrid inside rowexpander
notesgrid.store.data = response.data.content;
console.log("Row expanded successfully.");
} catch (error) {
console.error("Error expanding row:", error);
}
},
},
},
},
store: {
readUrl: "/pagedMockUrl",
pageParamName: "page",
sortParamName: "sort",
filterParamName: "filter",
pageSize: this.pageSize,
autoLoad: false,
proxy: true,
},
};
CreateStore() {
AjaxHelper.mockUrl("/pagedMockUrl", (url, params) => {
// Mock data generation logic
const page = parseInt(params.page, 10);
const pageSize = parseInt(params.pageSize, 10);
const startIdx = (page - 1) * pageSize;
let returnedData = this.gridData.map((item) => {
return {
...item,
Id: Number(item.Id),
};
});
// Filter the data if filter parameter is passed
if (params.filter) {
returnedData = returnedData.filter(
CollectionFilter.generateFiltersFunction(
JSON.parse(params.filter).map((f) => {
f.property = f.field;
return new CollectionFilter(f);
})
)
);
}
// Sort the data if sort parameter is passed
if (params.sort) {
const gridComponent = this.$refs.childGrid;
returnedData.sort(
gridComponent.instance.value.store.createSorterFn(
JSON.parse(params.sort)
)
);
}
return {
responseText: JSON.stringify({
success: true,
total: returnedData.length,
data: returnedData.slice(startIdx, startIdx + pageSize),
}),
};
});
},
Can you please help us resolve this sorting issue for the Paging Grid? Any insights or workaround would be greatly appreciated!
Thanks in advance!