Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## Unreleased

### Added

- Add the ability to pin columns ([#1781](https://github.com/sassoftware/vscode-sas-extension/pull/1781))

### Changed

- Update pubsdata files to use the latest version (2026.01) of the SAS documentation for procedures, statements, and functions ([#1770](https://github.com/sassoftware/vscode-sas-extension/pull/1770))
Expand Down
4 changes: 4 additions & 0 deletions client/src/panels/DataViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class DataViewer extends WebView {
return {
"Ascending (add to sorting)": l10n.t("Ascending (add to sorting)"),
"Descending (add to sorting)": l10n.t("Descending (add to sorting)"),
"Not pinned": l10n.t("Not pinned"),
"Pinned to the left": l10n.t("Pinned to the left"),
"Pinned to the right": l10n.t("Pinned to the right"),
"Remove all sorting": l10n.t("Remove all sorting"),
"Remove sorting": l10n.t("Remove sorting"),
"Row number": l10n.t("Row number"),
Expand All @@ -43,6 +46,7 @@ class DataViewer extends WebView {
Descending: l10n.t("Descending"),
Numeric: l10n.t("Numeric"),
Options: l10n.t("Options"),
Pin: l10n.t("Pin"),
Properties: l10n.t("Properties"),
Sort: l10n.t("Sort"),
};
Expand Down
109 changes: 79 additions & 30 deletions client/src/webview/ColumnMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { AgColumn, GridApi } from "ag-grid-community";
import { AgColumn, ColumnState, GridApi } from "ag-grid-community";

import GridMenu from "./GridMenu";
import localize from "./localize";
Expand All @@ -13,59 +13,76 @@ export interface ColumnMenuProps {
hasSort: boolean;
left: number;
loadColumnProperties: () => void;
pinColumn: (side: "left" | "right" | false) => void;
removeAllSorting: () => void;
removeFromSort: () => void;
sortColumn: (direction: "asc" | "desc") => void;
top: number;
}

// Lets pick off only the column properties we care about.
const filteredColumnState = (columnState: ColumnState[]) => {
return columnState.map(({ colId, sort, sortIndex, pinned }) => {
return { colId, sort, sortIndex, pinned };
});
};

export const getColumnMenu = (
api: GridApi,
column: AgColumn,
{ height, top, left }: DOMRect,
dismissMenu: () => void,
loadColumnProperties: (columnName: string) => void,
) => ({
): ColumnMenuProps => ({
column,
dismissMenu,
hasSort: api.getColumnState().some((c) => c.sort),
left,
top: top + height,
pinColumn: (side: "left" | "right" | false) => {
const columnState = filteredColumnState(api.getColumnState());
const foundColumn = columnState.find((col) => col.colId === column.colId);
foundColumn.pinned = side;
applyColumnState(api, columnState);
},
sortColumn: (direction: "asc" | "desc" | null) => {
const newColumnState = api.getColumnState().filter((c) => c.sort);
const colIndex = newColumnState.findIndex((c) => c.colId === column.colId);
if (colIndex === -1) {
newColumnState.push({
colId: column.colId,
sort: direction,
sortIndex: newColumnState.length,
});
} else {
newColumnState[colIndex] = {
colId: newColumnState[colIndex].colId,
sort: direction,
};
const columnState = filteredColumnState(api.getColumnState());
const foundColumn = columnState.find((col) => col.colId === column.colId);
const currentSortValue = foundColumn.sort;
foundColumn.sort = direction;
if (!currentSortValue) {
foundColumn.sortIndex = api.getColumnState().filter((c) => c.sort).length;
}
applyColumnState(api, newColumnState);
applyColumnState(api, columnState);
},
removeAllSorting: () =>
applyColumnState(
api,
api
.getColumnState()
.filter((c) => c.sort)
.map((c) => ({ colId: c.colId, sort: null })),
),
removeFromSort: () =>
applyColumnState(
api,
api
.getColumnState()
.sort((a, b) => a.sortIndex - b.sortIndex)
.filter((c) => c.sort && c.colId !== column.colId)
// After we remove the column, lets reindex what's left
.map((c, sortIndex) => ({ ...c, sortIndex })),
filteredColumnState(api.getColumnState()).map((c) => ({
...c,
sort: null,
})),
),
removeFromSort: () => {
// First, lets remove from sort
let newColumnState = filteredColumnState(api.getColumnState())
.sort((a, b) => a.sortIndex - b.sortIndex)
.map((c) => ({
...c,
sort: column.colId === c.colId ? null : c.sort,
}));

// Next, lets assign updated sort indices
let sortIndex = 0;
newColumnState = newColumnState.map((c) => {
if (!c.sort) {
return c;
}
return { ...c, sortIndex: sortIndex++ };
});

applyColumnState(api, newColumnState);
},
loadColumnProperties: () => {
loadColumnProperties(column.colId);
},
Expand All @@ -77,14 +94,46 @@ const ColumnMenu = ({
hasSort,
left,
loadColumnProperties,
pinColumn,
removeAllSorting,
removeFromSort,
sortColumn,
top,
}: ColumnMenuProps) => {
const theme = useTheme();
const sort = column.getSort();
const pinned = column.getPinned();
const menuItems = [
{
name: localize("Pin"),
children: [
{
name: localize("Pinned to the left"),
checked: pinned === "left",
onPress: () => {
pinColumn("left");
dismissMenu();
},
},
{
name: localize("Pinned to the right"),
checked: pinned === "right",
onPress: () => {
pinColumn("right");
dismissMenu();
},
},
{
name: localize("Not pinned"),
checked: !pinned,
onPress: () => {
pinColumn(false);
dismissMenu();
},
},
],
},
"separator",
{
name: localize("Sort"),
children: [
Expand Down
7 changes: 6 additions & 1 deletion client/src/webview/GridMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ interface MenuItem {

const GridMenu = ({
dismissMenu,
// note: `id` is used to distinguish one menu from another, so that we trigger
// a resize when the menu contents have changed.
id,
left: incomingLeft,
menuItems,
parentDimensions,
Expand All @@ -20,6 +23,7 @@ const GridMenu = ({
top,
}: {
dismissMenu?: () => void;
id?: number;
left?: number;
menuItems: (MenuItem | string)[];
parentDimensions?: { left: number; width: number };
Expand Down Expand Up @@ -155,12 +159,13 @@ const GridMenu = ({
if (left + width > clientWidth) {
setLeft(left - (left + width - clientWidth + 15));
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps

return (
<>
{subMenu && subMenu.items.length > 0 && (
<GridMenu
id={subMenu.index}
dismissMenu={() => {
focusItem(subMenu.index);
setSubMenu(undefined);
Expand Down
5 changes: 3 additions & 2 deletions client/src/webview/useDataViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,10 @@ const useDataViewer = () => {

columns.unshift({
field: "#",
suppressMovable: true,
sortable: false,
headerTooltip: localize("Row number"),
pinned: "left",
sortable: false,
suppressMovable: true,
});

setColumns(columns);
Expand Down
1 change: 1 addition & 0 deletions website/docs/Features/accessLibraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ You can use the Libraries pane to delete a table, drag and drop tables into your

When viewing table data, you have the following options available for each column (accessible by hovering over a column and clicking the menu button):

- Column pinning: Each column can be pinned to the left or the right of the data viewer to have an always on-screen view of important columns.
- Sort: Each column can be sorted ascending or descending. To add multiple columns to a sort, click the menu for each column and select the sort type. Sort priority will be displayed next to each column's direction icon.
- Properties: Clicking properties will reveal the properties for the specific column chosen.

Expand Down
Loading