A termék-lista gridje rendezhető és szűrhető lett

Alapból csak az aktív termékek jönnek: a katalógusban ~600 termék van, ebből egyszerre ~70 él,
a többi archív. A nop kereső-paneljének Publikált legördülője eddig „Mind"-en állt — most csak
publikált az alapérték, és a legördülőn bármikor átkapcsolható.

- Ordering = true (eddig ki volt kommentelve) és ServerSide = false. A kliens-oldali mód nem
  kényelmi döntés: a saját oszlopok (tára, átlagsúly, mérendő, kamionon, elérhető) a lapozott
  találatra UTÓLAG kerülnek rá a ProductDto-ból, tehát nincsenek a SQL-ben — szerver-oldalon
  csak oldalanként lehetne rendezni őket, ami helyesnek látszik és téved
- a ProductList emiatt nem lapoz: a BaseSearchModel alapértéke 10 sor, és a DataTables
  kliens-módban nem küld lapozást — beavatkozás nélkül 10 sort hinne az egésznek
- ha a lista mégis elérné a felső korlátot, a grid kiírja; enélkül a rendezés és a szűrés némán
  a katalógus egy részén dolgozna
- gyorsszűrő sor: Név, SKU (szöveg), Elérhető (van/nincs), Mérendő?, Publikált
- alapból 100 sor/oldal, a menüben is szerepel

Két csapda a nop grid körül, mindkettő beírva a kódba:
- scrollX mellett a DataTables KLÓNOZZA a fejlécet, és a jQuery kezelők a klónra nem másolódnak
  át. Ezért a sor a törzs-tábla fejlécébe épül (azt klónozza újra minden rajzoláskor), az
  események pedig delegáltak, data-fb-col alapján
- a kereséshez a DataTables kiszedi a HTML-t a cellákból, tehát a nop ikonra renderelt logikai
  oszlopokra a kirenderelt osztálynévvel keresni SOSEM talál — üres eredményt ad, ami hibátlan
  szűrőnek látszik. Minden nem-szöveges szűrő ezért a nyers sor-objektumot nézi
This commit is contained in:
2026-09-01 14:00:20 +02:00
parent 409b0eaadc
commit f0bde4b51a
2 changed files with 211 additions and 5 deletions
@@ -880,11 +880,29 @@ public partial class CustomProductController : BaseAdminController
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> List()
{
var model = await _productModelFactory.PrepareProductSearchModelAsync(new ProductSearchModel());
// Alapból csak az AKTÍV termékek. A katalógusban ~600 termék van, ebből egyszerre ~70 él; a többi
// archív, és a listát használhatatlanná tenné. A nop konvenciója: 0 = mind, 1 = csak publikált,
// 2 = csak nem publikált — a kereső-panel legördülőjén az admin bármikor átállíthatja.
var model = await _productModelFactory.PrepareProductSearchModelAsync(
new ProductSearchModel { SearchPublishedId = PublishedOnlySearchId });
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Product/List.cshtml", model);
}
/// <summary>A nop „csak publikált" szűrő-értéke a termék-kereső legördülőjében.</summary>
private const int PublishedOnlySearchId = 1;
/// <summary>
/// A kliens-oldali termék-grid felső sorkorlátja.
/// </summary>
/// <remarks>
/// A grid a böngészőben rendez és szűr (<c>List.cshtml</c> → <c>ServerSide = false</c>), ezért EGYBEN kell
/// megkapnia a találatokat. A <see cref="BaseSearchModel"/> alapértéke 10 sor, és a DataTables kliens-módban
/// nem küld lapozást — beavatkozás nélkül tehát 10 sort hinne az egésznek, a rendezés pedig némán csak azon
/// belül működne. A korlát bőséges a ~600-as katalógushoz; ha valaha túlnő rajta, a nézet ki is írja.
/// </remarks>
private const int ProductGridMaxRows = 5000;
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> BulkEdit()
{
@@ -918,6 +936,10 @@ public partial class CustomProductController : BaseAdminController
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductList(ProductSearchModel searchModel)
{
// A kliens-oldali grid nem lapoz a szerveren — lásd ProductGridMaxRows.
searchModel.Start = 0;
searchModel.Length = ProductGridMaxRows;
//prepare model
var model = await _productModelFactory.PrepareProductListModelExtendedAsync(searchModel);
@@ -218,10 +218,18 @@
Name = "products-grid",
UrlRead = new DataUrl("ProductList", "CustomProduct", null),
SearchButtonId = "search-products",
Length = Model.PageSize,
// Ordering = true,
// ServerSide = false,
LengthMenu = Model.AvailablePageSizes,
// Alapból 100 sor: ~70 aktív termék van, tehát egy lapon elfér az egész napi munka.
Length = 100,
// Rendezés ÉS oszlop-szűrés a böngészőben. A saját oszlopok (tára, átlagsúly,
// mérendő, kamionon, elérhető) a lapozott találatra UTÓLAG kerülnek rá
// (CustomProductModelFactory), tehát nincsenek benne a SQL-ben — szerver-oldalon
// rendezni őket csak oldalanként lehetne, ami helyesnek látszik és téved.
// Kliens-oldalon viszont mind rendezhető és szűrhető, egyformán.
Ordering = true,
ServerSide = false,
// A menüben szerepelnie KELL a 100-nak, különben a DataTables az első menüpontra esne
// vissza, és a fenti alapérték néma módon nem érvényesülne.
LengthMenu = "20,50,100,200,500",
Filters = new List<FilterParameter>
{
new FilterParameter(nameof(Model.SearchProductName)),
@@ -349,6 +357,182 @@
});
</script>
<script>
/* ── Gyorsszűrő sor a termék-gridhez ──────────────────────────────────────
Két dolgot kell kikerülnie, és mindkettő megharapott minket:
1) A grid `scrollX: true`-val megy, ezért a DataTables a fejlécet KLÓNOZZA egy külön,
görgethető táblába, és minden rajzoláskor újraklónozza. Az elemekre kötött jQuery
eseménykezelők a klónra NEM másolódnak át. Ezért a sor a törzs-tábla fejlécébe
épül (azt klónozza a DataTables, tehát magától visszaépül), az események pedig
DELEGÁLTAK, `data-fb-col` attribútum alapján.
2) A DataTables a kereséshez KISZEDI a HTML-t a cellákból. A logikai oszlopokat a nop
ikonra rendereli (`<i class="fas fa-check true-icon">`), tehát a kirenderelt
osztálynévre keresni sosem talál — üres eredményt ad, ami hibátlan szűrőnek látszik.
Ezért minden nem-szöveges szűrő a NYERS sor-objektumot nézi (`ext.search` 4.
paramétere), nem a megjelenítést.
A definíciók az OSZLOP ADATNEVÉRE kulcsolnak, nem a sorszámára: az `Id` háromszor is
oszlop (kijelölő, azonosító, szerkesztés-gomb), és a rejtett kép-oszlop miatt a
pozíciók amúgy is elcsúsznának. */
(function () {
var GRID = "products-grid";
/* Oszlop adatneve → a beírt szűrő-érték. A klónok ebből épülnek vissza. */
var state = {};
/* Adatnév → oszlop-index. A nop NEM ad `name` propertyt az oszlopoknak, ezért a
`"Name:name"` szelektor nem oldódna fel — némán nem szűrne semmit. */
var colIndex = {};
var yesNo = [["", "Mind"], ["true", "Igen"], ["false", "Nem"]];
var defs = {
"Name" : { kind: "text" },
"Sku" : { kind: "text" },
/* Az „elérhető" MENNYISÉG: a részszó-keresés félrevezetne (a „10" a 100-ra és a
210-re is illeszkedne), ezért van/nincs bontás, számként összehasonlítva. */
"AvailableQuantity": { kind: "qty", opts: [["", "Mind"], ["pos", "Van (> 0)"], ["zero", "Nincs (0)"]] },
"IsMeasurable" : { kind: "bool", opts: yesNo },
"Published" : { kind: "bool", opts: yesNo }
};
/* A nyers soron dolgozó szűrők. Az ext.search GLOBÁLIS: minden gridre lefut, ezért a
sajátunkra kell szűkíteni. A 4. paraméter a JSON-ból jött sor-objektum — pont az, amit
a HTML-kiszedés nem érint. */
$.fn.dataTable.ext.search.push(function (settings, searchData, dataIndex, row) {
if (settings.nTable.id !== GRID || !row) return true;
for (var key in defs) {
var def = defs[key];
var want = state[key];
if (def.kind === "text" || !want) continue;
if (def.kind === "bool") {
if (String(row[key]) !== want) return false;
continue;
}
var value = parseFloat(row[key]);
if (isNaN(value)) value = 0;
if (want === "pos" && !(value > 0)) return false;
if (want === "zero" && value > 0) return false;
}
return true;
});
function buildRow(api, $header) {
var $row = $("<tr class='fb-filter-row'></tr>");
api.columns().every(function (idx) {
/* A rejtett kép-oszlopnak fejléc-cellája sincs — egy fölösleges th elcsúsztatná a sort. */
if (!this.visible()) return;
var key = this.dataSrc();
colIndex[key] = idx;
var $th = $("<th></th>").appendTo($row);
var def = defs[key];
if (!def) return;
var value = state[key] || "";
var $field;
if (def.kind === "text") {
$field = $("<input type='text' class='form-control form-control-sm'>")
.attr("placeholder", "szűrés…").attr("value", value);
} else {
$field = $("<select class='form-control form-control-sm'></select>");
def.opts.forEach(function (o) {
var $o = $("<option>").val(o[0]).text(o[1]);
/* selected ATTRIBÚTUM, nem property: a klón csak az attribútumot viszi magával. */
if (o[0] === value) $o.attr("selected", "selected");
$o.appendTo($field);
});
}
$field.attr("data-fb-col", key).attr("data-fb-kind", def.kind).val(value).appendTo($th);
});
/* A TÖRZS-tábla fejlécébe kötünk: scrollX mellett ezt klónozza a DataTables a látható
görgető-fejlécbe, minden rajzoláskor újra — így a sor magától visszaépül. Közvetlenül
a klónba írni hasztalan: a következő rajzolás felülírja. */
$header.append($row);
}
$(document).on("init.dt", function (e, settings) {
if (settings.nTable.id !== GRID) return;
buildRow(new $.fn.dataTable.Api(settings), $(settings.nTable).find("thead").first());
});
function syncCopies(selector, key, value) {
$(selector + "[data-fb-col='" + key + "']").each(function () {
if (this.value !== value) this.value = value;
if (this.tagName === "SELECT") {
$(this).find("option").each(function () {
$(this).attr("selected", this.value === value ? "selected" : null);
});
} else {
$(this).attr("value", value);
}
});
}
/* Delegálva a dokumentumra: a klónok bármikor keletkezhetnek, kezelőt nem kapnak. */
var timer;
$(document).on("input", "input[data-fb-col]", function () {
var key = $(this).attr("data-fb-col");
var value = this.value;
state[key] = value;
syncCopies("input", key, value);
clearTimeout(timer);
timer = setTimeout(function () {
var api = $("#" + GRID).DataTable();
api.column(colIndex[key]).search(value);
api.draw();
}, 300);
});
$(document).on("change", "select[data-fb-col]", function () {
var key = $(this).attr("data-fb-col");
state[key] = this.value;
syncCopies("select", key, this.value);
/* A logikai és a mennyiségi szűrő az ext.search-ben fut, ezért itt elég újrarajzolni. */
$("#" + GRID).DataTable().draw();
});
/* A kliens-oldali mód a kapott sorokat tekinti a teljes halmaznak. Ha a szerver a felső
korlátnál (ProductGridMaxRows) elvágta a listát, azt LÁTNI kell — különben a rendezés
és a szűrés némán a katalógus egy részén dolgozna.
A nop `dataSrc: "Data"`-val ad vissza, tehát a mező NAGY kezdőbetűs. */
$(document).on("xhr.dt", function (e, settings, json) {
if (settings.nTable.id !== GRID || !json || !json.recordsTotal) return;
var got = (json.Data || []).length;
if (json.recordsTotal <= got) return;
$("#" + GRID + "_wrapper").prepend(
"<div class='alert alert-warning py-2 mb-2'>A lista csonkolva: " + got + " / " +
json.recordsTotal + " termék jött le. A rendezés és a szűrés csak a letöltött részen " +
"dolgozik — szűkíts a fenti kereső-panellel.</div>");
});
})();
</script>
<nop-alert asp-alert-id="deleteSelectedFailed" />
<nop-alert asp-alert-id="nothingSelectedAlert" />
</div>