Server-side grid with Northwind OData

by stitot

HTML

<script src="https://cdn.jsdelivr.net/npm/@highcharts/grid-pro/grid-pro.js"></script>

<div class="demo-shell">
    <div class="demo-intro">
        <h1>Grid Pro with one-request server-side products</h1>
        <p>
            This demo uses the public Northwind OData API. Sorting, filtering,
            and pagination all happen on the server, and each interaction is
            handled with one request. Nice and tidy, like a query string that
            actually knows where it is going.
        </p>
    </div>

    <div id="notice" class="notice" aria-live="polite">
        Loading products from Northwind...
    </div>

    <div id="container"></div>

    <p class="highcharts-description">
        Click a header to sort, or shift-click a second one for multi-column
        sorting. Use the filter icons in the headers to send matching OData
        filters to the server.
    </p>
</div>

CSS

@import url("https://cdn.jsdelivr.net/npm/@highcharts/grid-pro/css/grid-pro.css");

body {
    font-family:
        -apple-system,
        BlinkMacSystemFont,
        "Segoe UI",
        Roboto,
        Helvetica,
        Arial,
        sans-serif;
    background:
        linear-gradient(180deg, #f7fafc 0%, #edf7f4 100%);
    color: #1f2937;
    margin: 0;
}

.demo-shell {
    max-width: 1120px;
    margin: 0 auto;
    padding: 20px 16px 28px;
}

.demo-intro {
    margin-bottom: 14px;
}

.demo-intro h1 {
    margin: 0 0 8px;
    font-size: 1.8rem;
    line-height: 1.1;
}

.demo-intro p,
.highcharts-description {
    margin: 0;
    max-width: 78ch;
    line-height: 1.55;
}

.notice {
    margin: 0 0 12px;
    padding: 10px 12px;
    border: 1px solid #cfe7df;
    border-radius: 10px;
    background: #f1fbf8;
    color: #1f5e52;
}

.notice[data-level="error"] {
    border-color: #f0b4b4;
    background: #fff1f1;
    color: #8b1e1e;
}

#container {
    min-height: 640px;
    max-height: 720px;
}

.highcharts-description {
    padding-top: 12px;
}

JavaScript

const API_BASE = 'https://services.odata.org/V4/Northwind/Northwind.svc/Products';
const COLUMN_IDS = [
    'ProductID',
    'ProductName',
    'QuantityPerUnit',
    'UnitPrice',
    'UnitsInStock',
    'Discontinued'
];
const COLUMN_TYPES = {
    ProductID: 'number',
    ProductName: 'string',
    QuantityPerUnit: 'string',
    UnitPrice: 'number',
    UnitsInStock: 'number',
    Discontinued: 'boolean'
};
const currencyFormatter = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
});
const noticeEl = document.getElementById('notice');

function setNotice(message, level) {
    noticeEl.textContent = message;
    noticeEl.dataset.level = level || 'info';
}

function getActiveSortings(query) {
    const sortings = query.sorting.currentSortings ||
        (query.sorting.currentSorting ? [query.sorting.currentSorting] : []);

    return sortings.filter((sorting) => sorting?.columnId && sorting.order);
}

function getFilterRootCondition(state) {
    return state.query.filtering.modifier?.options?.condition;
}

function escapeODataString(value) {
    return String(value).replace(/'/g, "''");
}

function normalizeNumericValue(value) {
    const normalized = typeof value === 'number' ? value : Number(value);

    return Number.isFinite(normalized) ? String(normalized) : '';
}

function buildOrderBy(state) {
    const orderBy = getActiveSortings(state.query)
        .filter((sorting) => COLUMN_TYPES[sorting.columnId])
        .map((sorting) => `${sorting.columnId} ${sorting.order}`)
        .join(',');

    return orderBy ? encodeURIComponent(orderBy) : '';
}

function buildEmptyCondition(columnId, columnType) {
    if (columnType === 'string') {
        return `(${columnId} eq null or ${columnId} eq '')`;
    }

    return `${columnId} eq null`;
}

function buildStringCondition(columnId, operator, value) {
    const normalizedValue = String(value || '');

    if...