diff --git a/.changeset/pl-data-table-default-sorting.md b/.changeset/pl-data-table-default-sorting.md new file mode 100644 index 0000000000..bd5c9d52d9 --- /dev/null +++ b/.changeset/pl-data-table-default-sorting.md @@ -0,0 +1,23 @@ +--- +"@platforma-sdk/ui-vue": patch +--- + +Keep the default `sorting` passed to `createPlDataTableV3` alive + +`resolveSorting` falls back to the block's default sorting only while the +persisted user sorting is `null` — `[]` means "the user cleared the sorting" +and deliberately suppresses the default. The grid state never produced that +`null`: AG Grid omits `sort` from its state while nothing is sorted, and +`convertAgSortingToPTableSorting` turned the absent state into `[]`. + +`onStateUpdated` fires as soon as the grid initialises, so the first persisted +state entry stamped `sorting: []` and the block's default sorting was dropped +from that moment on — the sorted column stayed in the table, just unsorted. + +The absent sort state now converts to `null`, and `normalizeSort` (next to the +existing `normalizeColumnVisibility`, which solves the same problem for hidden +columns) turns it into an explicit `{ sortModel: [] }` only once the grid has +reported an explicit sort model before — so clearing the sorting by hand still +suppresses the default. Existing projects recover on the next render without a +state migration: their stored `gridState.sort` is absent, which now reads as +"untouched". diff --git a/sdk/ui-vue/src/components/PlAgDataTable/PlAgDataTableV2.vue b/sdk/ui-vue/src/components/PlAgDataTable/PlAgDataTableV2.vue index 08c2da7cde..678d698f1b 100644 --- a/sdk/ui-vue/src/components/PlAgDataTable/PlAgDataTableV2.vue +++ b/sdk/ui-vue/src/components/PlAgDataTable/PlAgDataTableV2.vue @@ -129,7 +129,7 @@ const { gridApi, gridOptions } = useGrid({ let isReloading = false; gridOptions.value.onGridPreDestroyed = (event) => { if (!isReloading) { - gridOptions.value.initialState = gridState.value = normalizeColumnVisibility( + gridOptions.value.initialState = gridState.value = normalizeGridState( makePartialState(event.api.getState()), gridState.value, event.api, @@ -142,7 +142,7 @@ gridOptions.value.onRowDoubleClicked = (event) => { if (event.data && event.data.axesKey) emit("rowDoubleClicked", event.data.axesKey); }; gridOptions.value.onStateUpdated = (event) => { - const partialState = normalizeColumnVisibility( + const partialState = normalizeGridState( makePartialState(event.state), gridState.value, event.api, @@ -213,6 +213,42 @@ function makePartialState(state: GridState): PlDataTableGridStateCore { }; } +// AG Grid omits parts of its state that carry no information — columnVisibility +// when every column is visible, sort when nothing is sorted. The model needs +// "no state yet" (fall back to the block's defaults) to stay distinguishable +// from "user explicitly cleared it" (store []), so both are normalized against +// the previous state before the state is persisted. +function normalizeGridState( + partialState: PlDataTableGridStateCore, + prevState: PlDataTableGridStateCore, + api: GridApi, + columnsMeta: PlDataTableColumnsMeta | undefined, +): PlDataTableGridStateCore { + return normalizeSort( + normalizeColumnVisibility(partialState, prevState, api, columnsMeta), + prevState, + ); +} + +// AG Grid returns sort: undefined when no column is sorted. Left as-is that +// erases the block's default sorting the moment any unrelated grid state is +// persisted, so the undefined is only turned into an explicit empty sort model +// once the user has actually sorted something before. +function normalizeSort( + partialState: PlDataTableGridStateCore, + prevState: PlDataTableGridStateCore, +): PlDataTableGridStateCore { + if (partialState.sort !== undefined) return partialState; + + if (prevState.sort !== undefined) { + // Had an explicit sort model before → user cleared the sorting → store []. + return { ...partialState, sort: { sortModel: [] } }; + } + + // Never sorted → leave undefined so the model applies its default sorting. + return partialState; +} + // AG Grid returns columnVisibility: undefined when all columns are visible. // We need to distinguish "no state yet" (use isColumnOptional defaults) from // "user explicitly showed all columns" (store []). This function normalizes @@ -264,11 +300,14 @@ function getDefaultHiddenColIds( .map((col) => col.getColId() as PlTableColumnIdJson); } -// Normalize columnVisibility for comparison: undefined and { hiddenColIds: [] } are equivalent. +// Normalize for comparison: an absent and an empty columnVisibility / sort mean +// the same thing to AG Grid, and must not count as a state change to reload on. function stateForReloadCompare(state: PlDataTableGridStateCore): PlDataTableGridStateCore { const cv = state.columnVisibility; const normalizedCv = !cv || cv.hiddenColIds.length === 0 ? undefined : state.columnVisibility; - return { ...state, columnVisibility: normalizedCv }; + const sort = state.sort; + const normalizedSort = !sort || sort.sortModel.length === 0 ? undefined : sort; + return { ...state, columnVisibility: normalizedCv, sort: normalizedSort }; } // Reload AgGrid when new state arrives from server diff --git a/sdk/ui-vue/src/components/PlAgDataTable/sources/table-state-v2.test.ts b/sdk/ui-vue/src/components/PlAgDataTable/sources/table-state-v2.test.ts new file mode 100644 index 0000000000..7dcf9bf8a1 --- /dev/null +++ b/sdk/ui-vue/src/components/PlAgDataTable/sources/table-state-v2.test.ts @@ -0,0 +1,37 @@ +// @vitest-environment happy-dom +import { describe, expect, it, vi } from "vitest"; +import { computed } from "vue"; + +// The module pulls in uikit for `computedCached`; stub it so the test does not +// drag the whole component library (and its DOM-time side effects) in. +vi.mock("@milaboratories/uikit", () => ({ + computedCached: computed, +})); + +const { convertAgSortingToPTableSorting } = await import("./table-state-v2"); + +describe("convertAgSortingToPTableSorting", () => { + it("reports an absent sort state as untouched, so the model keeps its default sorting", () => { + expect(convertAgSortingToPTableSorting(undefined)).toBeNull(); + }); + + it("reports an empty sort model as explicitly cleared, which suppresses the default", () => { + expect(convertAgSortingToPTableSorting({ sortModel: [] })).toEqual([]); + }); + + it("converts a sort model, taking NA/absent as least values only when ascending", () => { + const column = { type: "column", id: "colId" }; + const colId = JSON.stringify(column) as never; + expect( + convertAgSortingToPTableSorting({ + sortModel: [ + { colId, sort: "asc" }, + { colId, sort: "desc" }, + ], + }), + ).toEqual([ + { column, ascending: true, naAndAbsentAreLeastValues: true }, + { column, ascending: false, naAndAbsentAreLeastValues: false }, + ]); + }); +}); diff --git a/sdk/ui-vue/src/components/PlAgDataTable/sources/table-state-v2.ts b/sdk/ui-vue/src/components/PlAgDataTable/sources/table-state-v2.ts index b48ab3dccf..a99696afe9 100644 --- a/sdk/ui-vue/src/components/PlAgDataTable/sources/table-state-v2.ts +++ b/sdk/ui-vue/src/components/PlAgDataTable/sources/table-state-v2.ts @@ -413,14 +413,23 @@ function convertPartitionFiltersToFilterSpec( }); } -function convertAgSortingToPTableSorting(state: PlDataTableGridStateCore["sort"]): PTableSorting[] { - return ( - state?.sortModel.map((item) => ({ - column: parseJson(item.colId), - ascending: item.sort === "asc", - naAndAbsentAreLeastValues: item.sort === "asc", - })) ?? [] - ); +/** + * AG Grid omits `sort` from its state while nothing is sorted, so the absent + * state must stay distinguishable from an explicitly emptied sort model: the + * model falls back to the block's default sorting on `null` only, and treats + * `[]` as "user cleared the sorting" (see `resolveSorting` in + * `createPlDataTableV3`). `normalizeSort` in `PlAgDataTableV2.vue` turns the + * absent state into `{ sortModel: [] }` once an explicit one has been seen. + */ +export function convertAgSortingToPTableSorting( + state: PlDataTableGridStateCore["sort"], +): PTableSorting[] | null { + if (isNil(state)) return null; + return state.sortModel.map((item) => ({ + column: parseJson(item.colId), + ascending: item.sort === "asc", + naAndAbsentAreLeastValues: item.sort === "asc", + })); } function getHiddenColIds(