mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
Compare commits
1
Commits
v26.10.5
...
sdk-v1.0.20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f332aa5c3 |
No files matched your search
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 26.10.5
|
||||
|
||||
### Changed
|
||||
|
||||
- Card grids in the Plugins tab and in plugins can show Auto (as many as fit) or a set number of cards per row
|
||||
|
||||
## 26.10.4
|
||||
|
||||
### Fixed
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "termix",
|
||||
"version": "26.10.4",
|
||||
"version": "26.10.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "termix",
|
||||
"version": "26.10.4",
|
||||
"version": "26.10.5",
|
||||
"hasInstallScript": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "26.10.4",
|
||||
"version": "26.10.5",
|
||||
"description": "Self-hosted, plugin-based server management.",
|
||||
"author": "Karmaa",
|
||||
"main": "electron/main.cjs",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.20
|
||||
|
||||
Plugin API 1.12.0.
|
||||
|
||||
### Added
|
||||
|
||||
- `columns` and `onColumns` on `ViewToggle` show a cards per row picker: Auto fits as many as the pane has room for, or pick 1 to 6
|
||||
- `size` and `minCardWidth` on `DataView`, so a card grid follows that pick, and the `GridSize` type
|
||||
- `usePluginPanelView` also keeps `columns`, the cards per row pick, with `setColumns`
|
||||
|
||||
## 1.0.19
|
||||
|
||||
Plugin API 1.11.0.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Termix plugin guide
|
||||
|
||||
SDK 1.0.19 | Plugin API 1.11.0
|
||||
SDK 1.0.20 | Plugin API 1.12.0
|
||||
|
||||
This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
|
||||
`@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
|
||||
@@ -643,6 +643,9 @@ exports with props is `ui.api.txt` in the Termix repo
|
||||
`PanePrompt` for small prompts.
|
||||
- Every tab or panel sits in `PanelShell`. Lists use `PanelList` and `ListRow`, add with `AddButton`,
|
||||
forms end with `FormFooter`, empty lists show `EmptyState`.
|
||||
- Card grids use `DataView` with `ViewToggle`. Pass `columns` and `onColumns` to `ViewToggle` and the same
|
||||
value as `size` to `DataView`, so users can pick Auto (as many as fit) or a fixed number of cards per
|
||||
row. `usePluginPanelView` keeps the choice as `columns`. Older Termix versions ignore both props.
|
||||
- Forms: `TextField`, `NumberField`, `SelectField`, `SwitchRow`, `SecretField`, `SettingRow` and friends.
|
||||
- Host sessions use `ConnectionGate` for the whole connect flow (host keys, TOTP, passphrases, errors).
|
||||
- Mobile: check `useIsMobile()` and give small screens a layout that works. The desktop app always uses the
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@termix-ssh/plugin-sdk",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.20",
|
||||
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1711,28 +1711,49 @@ export function usePluginUiPreferences<
|
||||
return bridge.usePluginUiPreferences(bridge.usePluginId()) as never;
|
||||
}
|
||||
|
||||
type PanelGridSize = "auto" | "1" | "2" | "3" | "4" | "5" | "6";
|
||||
const PANEL_GRID_SIZES: readonly string[] = [
|
||||
"auto",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
];
|
||||
|
||||
/**
|
||||
* Grid or list and row density for one of this plugin's views, kept in its
|
||||
* Appearance area as `<prefix>viewMode` and `<prefix>density`. Declare those
|
||||
* keys in contributes.uiPresets so each preset level can pick a default.
|
||||
* Grid or list, row density and cards per row for one of this plugin's
|
||||
* views, kept in its Appearance area as `<prefix>viewMode`, `<prefix>density`
|
||||
* and `<prefix>columns`. Declare those keys in contributes.uiPresets so each
|
||||
* preset level can pick a default. Hand columns to ViewToggle and DataView.
|
||||
*/
|
||||
export function usePluginPanelView(prefix = ""): {
|
||||
view: "grid" | "list";
|
||||
density: "comfortable" | "compact";
|
||||
compact: boolean;
|
||||
columns: PanelGridSize;
|
||||
setView: (view: "grid" | "list") => void;
|
||||
setDensity: (density: "comfortable" | "compact") => void;
|
||||
setColumns: (columns: PanelGridSize) => void;
|
||||
} {
|
||||
const { values, set } = usePluginUiPreferences();
|
||||
const view = values[`${prefix}viewMode`] === "list" ? "list" : "grid";
|
||||
const density =
|
||||
values[`${prefix}density`] === "compact" ? "compact" : "comfortable";
|
||||
const saved = values[`${prefix}columns`];
|
||||
const columns =
|
||||
typeof saved === "string" && PANEL_GRID_SIZES.includes(saved)
|
||||
? (saved as PanelGridSize)
|
||||
: "auto";
|
||||
return {
|
||||
view,
|
||||
density,
|
||||
compact: density === "compact",
|
||||
columns,
|
||||
setView: (next) => set(`${prefix}viewMode`, next),
|
||||
setDensity: (next) => set(`${prefix}density`, next),
|
||||
setColumns: (next) => set(`${prefix}columns`, next),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export {
|
||||
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
||||
* version by the server.
|
||||
*/
|
||||
export const PLUGIN_API_VERSION = "1.11.0";
|
||||
export const PLUGIN_API_VERSION = "1.12.0";
|
||||
|
||||
/** The API major, the default engine.api for a new plugin. */
|
||||
export const SUPPORTED_PLUGIN_API_VERSION = String(
|
||||
|
||||
@@ -80,7 +80,7 @@ DEFAULT_FOLDER_COLOR (value): "#f59145"
|
||||
DEFAULT_FOLDER_ICON (value): "folder"
|
||||
DashboardCardId (type): DashboardCardId
|
||||
DataColumn (type): DataColumn<T>
|
||||
DataView (value): <T>({ items, view, density, getKey, renderCard, columns, listColumns, onRowClick, isRowActive, empty, className, }: { items: T[]; view: PanelViewMode; density?: PanelDensity; getKey: (item: T) => string; renderCard: (item: T) => ReactNode; columns: GridColumns; listColumns: DataColumn<T>[]; onRowClick?: (item: T) => void; isRowActive?: (item: T) => boolean; empty: ReactNode; className?: string; }) => Element
|
||||
DataView (value): <T>({ items, view, density, getKey, renderCard, columns, size, minCardWidth, listColumns, onRowClick, isRowActive, empty, className, }: { items: T[]; view: PanelViewMode; density?: PanelDensity; getKey: (item: T) => string; renderCard: (item: T) => ReactNode; columns: GridColumns; size?: GridSize; minCardWidth?: number; listColumns: DataColumn<T>[]; onRowClick?: (item: T) => void; isRowActive?: (item: T) => boolean; empty: ReactNode; className?: string; }) => Element
|
||||
DefaultOverrides (type): DefaultOverrides
|
||||
DefaultsOnly (value): ({ settingKey, children, }: { settingKey: string; children: ReactNode; }) => Element
|
||||
DesktopUpdateMode (type): DesktopUpdateMode
|
||||
@@ -131,6 +131,7 @@ GridCardCatalogEntry (type): GridCardCatalogEntry
|
||||
GridColSpan (type): GridColSpan
|
||||
GridColumns (type): GridColumns
|
||||
GridLayout (type): GridLayout
|
||||
GridSize (type): GridSize
|
||||
GridSlot (type): GridSlot
|
||||
GroupHeading (value): ({ title, count, action, className, }: { title: string; count?: number; action?: ReactNode; className?: string; }) => Element
|
||||
HexColorPicker (value): ({ value, onChange, }: { value: string; onChange: (hex: string) => void; }) => Element
|
||||
@@ -321,7 +322,7 @@ UserRole (type): UserRole
|
||||
UserSecondFactorSummary (type): UserSecondFactorSummary
|
||||
VersionBadge (value): ({ status, releaseUrl, className, }: VersionBadgeProps) => Element
|
||||
VersionInfo (type): VersionInfo
|
||||
ViewToggle (value): ({ view, onView, density, onDensity, }: { view: PanelViewMode; onView: (next: PanelViewMode) => void; density?: PanelDensity; onDensity?: (next: PanelDensity) => void; }) => Element
|
||||
ViewToggle (value): ({ view, onView, density, onDensity, columns, onColumns, }: { view: PanelViewMode; onView: (next: PanelViewMode) => void; density?: PanelDensity; onDensity?: (next: PanelDensity) => void; columns?: GridSize; onColumns?: (next: GridSize) => void; }) => Element
|
||||
WebSocketConnectionTarget (type): WebSocketConnectionTarget
|
||||
WidgetTitle (value): ({ title, icon, fallback }: WidgetTitleProps) => Element
|
||||
actionOrder (value): (action: HostActionDef) => number
|
||||
|
||||
@@ -151,6 +151,7 @@ const ALLOWED = [
|
||||
"GridCardCatalogEntry",
|
||||
"GridColSpan",
|
||||
"GridColumns",
|
||||
"GridSize",
|
||||
"GridLayout",
|
||||
"GridSlot",
|
||||
"GroupHeading",
|
||||
|
||||
@@ -2,6 +2,8 @@ import type React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
PANEL,
|
||||
gridSizeStyle,
|
||||
type GridSize,
|
||||
type PanelDensity,
|
||||
type PanelViewMode,
|
||||
} from "@/components/panel-layout";
|
||||
@@ -67,6 +69,8 @@ export function DataView<T>({
|
||||
getKey,
|
||||
renderCard,
|
||||
columns,
|
||||
size,
|
||||
minCardWidth,
|
||||
listColumns,
|
||||
onRowClick,
|
||||
isRowActive,
|
||||
@@ -78,7 +82,12 @@ export function DataView<T>({
|
||||
density?: PanelDensity;
|
||||
getKey: (item: T) => string;
|
||||
renderCard: (item: T) => React.ReactNode;
|
||||
/** Breakpoint columns, used when no size is given. */
|
||||
columns: GridColumns;
|
||||
/** Columns the user picked with ViewToggle. Wins over columns. */
|
||||
size?: GridSize;
|
||||
/** Narrowest card in auto mode, in px. */
|
||||
minCardWidth?: number;
|
||||
listColumns: DataColumn<T>[];
|
||||
onRowClick?: (item: T) => void;
|
||||
isRowActive?: (item: T) => boolean;
|
||||
@@ -93,9 +102,19 @@ export function DataView<T>({
|
||||
|
||||
if (view === "grid") {
|
||||
return (
|
||||
<div className={cn("grid", PANEL.gap, gridClass(columns), className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"grid",
|
||||
PANEL.gap,
|
||||
!size && gridClass(columns),
|
||||
className,
|
||||
)}
|
||||
style={size ? gridSizeStyle(size, minCardWidth) : undefined}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div key={getKey(item)}>{renderCard(item)}</div>
|
||||
<div key={getKey(item)} className="flex flex-col *:flex-1">
|
||||
{renderCard(item)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { Children, Fragment, isValidElement, useRef } from "react";
|
||||
import type React from "react";
|
||||
import { ArrowLeft, Grid3X3, List, Rows3, Search, X } from "lucide-react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Columns3,
|
||||
Grid3X3,
|
||||
List,
|
||||
Rows3,
|
||||
Search,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Separator } from "@/components/separator";
|
||||
import { DocsLink } from "@/components/docs-link";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/select";
|
||||
|
||||
/** One spacing rhythm for every panel and tab. */
|
||||
export const PANEL = {
|
||||
@@ -19,6 +34,37 @@ export const PANEL = {
|
||||
export type PanelViewMode = "grid" | "list";
|
||||
export type PanelDensity = "comfortable" | "compact";
|
||||
|
||||
/** Cards per row. "auto" fits as many as the pane has room for. */
|
||||
export type GridSize = "auto" | "1" | "2" | "3" | "4" | "5" | "6";
|
||||
|
||||
export const GRID_SIZES: readonly GridSize[] = [
|
||||
"auto",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
];
|
||||
|
||||
// Sized off the pane's own width, not the window. Cards keep a min width,
|
||||
// so narrow panes drop columns on their own.
|
||||
export function gridSizeStyle(
|
||||
size: GridSize,
|
||||
minCardWidth = 320,
|
||||
gap = "0.5rem",
|
||||
): React.CSSProperties {
|
||||
if (size === "auto") {
|
||||
return {
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${minCardWidth}px), 1fr))`,
|
||||
};
|
||||
}
|
||||
const n = Number(size);
|
||||
return {
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(max(min(100%, 220px), calc((100% - ${n - 1} * ${gap}) / ${n} - 0.5px)), 1fr))`,
|
||||
};
|
||||
}
|
||||
|
||||
/** The header, toolbar band and body every tab and panel draws. */
|
||||
export function PanelShell({
|
||||
chrome = true,
|
||||
@@ -308,20 +354,25 @@ export function Segmented<T extends string>({
|
||||
);
|
||||
}
|
||||
|
||||
/** Grid or list, plus the compact rows toggle. */
|
||||
/** Grid or list, plus the compact rows toggle and the cards per row picker. */
|
||||
export function ViewToggle({
|
||||
view,
|
||||
onView,
|
||||
density,
|
||||
onDensity,
|
||||
columns,
|
||||
onColumns,
|
||||
}: {
|
||||
view: PanelViewMode;
|
||||
onView: (next: PanelViewMode) => void;
|
||||
density?: PanelDensity;
|
||||
onDensity?: (next: PanelDensity) => void;
|
||||
/** Cards per row in grid view. Pass with onColumns to show the picker. */
|
||||
columns?: GridSize;
|
||||
onColumns?: (next: GridSize) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
const toggle = (
|
||||
<div className="flex items-center border border-border">
|
||||
<ToggleButton
|
||||
active={view === "grid"}
|
||||
@@ -356,6 +407,34 @@ export function ViewToggle({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!columns || !onColumns || view !== "grid") return toggle;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={columns} onValueChange={(v) => onColumns(v as GridSize)}>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-auto gap-1.5 text-xs"
|
||||
title={t("panel.columns.title")}
|
||||
aria-label={t("panel.columns.title")}
|
||||
>
|
||||
<Columns3 className="size-3.5 text-muted-foreground" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{GRID_SIZES.map((size) => (
|
||||
<SelectItem key={size} value={size} className="text-xs">
|
||||
{size === "auto"
|
||||
? t("panel.columns.auto")
|
||||
: t("panel.columns.count", { count: Number(size) })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{toggle}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleButton({
|
||||
|
||||
@@ -1325,11 +1325,6 @@
|
||||
"updated": "Recently updated",
|
||||
"category": "Category"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Columns",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} wide"
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
@@ -1654,7 +1649,12 @@
|
||||
"gridView": "Grid",
|
||||
"listView": "List",
|
||||
"compactRows": "Compact rows",
|
||||
"comfortableRows": "Comfortable rows"
|
||||
"comfortableRows": "Comfortable rows",
|
||||
"columns": {
|
||||
"title": "Columns",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} wide"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Saved. Type to replace it."
|
||||
|
||||
@@ -289,6 +289,7 @@ export {
|
||||
PanelShell,
|
||||
Segmented,
|
||||
ViewToggle,
|
||||
type GridSize,
|
||||
type PanelDensity,
|
||||
type PanelViewMode,
|
||||
} from "@/components/panel-layout";
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Check,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Columns3,
|
||||
Download,
|
||||
Globe,
|
||||
Library,
|
||||
@@ -37,7 +36,9 @@ import {
|
||||
GroupHeading,
|
||||
PANEL,
|
||||
PanelShell,
|
||||
GRID_SIZES,
|
||||
ViewToggle,
|
||||
type GridSize,
|
||||
type PanelViewMode,
|
||||
} from "@/components/panel-layout";
|
||||
import { DataView, type DataColumn } from "@/components/data-view";
|
||||
@@ -72,9 +73,6 @@ import { DocsLink } from "@/components/docs-link";
|
||||
|
||||
type Section = "installed" | "browse" | "updates" | "registries";
|
||||
|
||||
const GRID_SIZES = ["auto", "2", "3", "4", "5", "6"] as const;
|
||||
type GridSize = (typeof GRID_SIZES)[number];
|
||||
|
||||
interface Layout {
|
||||
view: PanelViewMode;
|
||||
columns: GridSize;
|
||||
@@ -364,54 +362,16 @@ function LayoutControls({
|
||||
onView: (next: PanelViewMode) => void;
|
||||
onColumns: (next: GridSize) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{layout.view === "grid" && (
|
||||
<Select
|
||||
value={layout.columns}
|
||||
onValueChange={(v) => onColumns(v as GridSize)}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-auto gap-1.5 text-xs"
|
||||
title={t("plugins.manager.columns.title")}
|
||||
aria-label={t("plugins.manager.columns.title")}
|
||||
>
|
||||
<Columns3 className="size-3.5 text-muted-foreground" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{GRID_SIZES.map((size) => (
|
||||
<SelectItem key={size} value={size} className="text-xs">
|
||||
{size === "auto"
|
||||
? t("plugins.manager.columns.auto")
|
||||
: t("plugins.manager.columns.count", { count: Number(size) })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<ViewToggle view={layout.view} onView={onView} />
|
||||
</>
|
||||
<ViewToggle
|
||||
view={layout.view}
|
||||
onView={onView}
|
||||
columns={layout.columns}
|
||||
onColumns={onColumns}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Sized off the tab's own width, not the window. Cards keep a min width,
|
||||
// so narrow panes drop columns on their own.
|
||||
function gridStyle(columns: GridSize): React.CSSProperties {
|
||||
if (columns === "auto") {
|
||||
return {
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 400px), 1fr))",
|
||||
};
|
||||
}
|
||||
const n = Number(columns);
|
||||
const gaps = (n - 1) * 12;
|
||||
return {
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(max(220px, calc((100% - ${gaps}px) / ${n} - 0.5px)), 1fr))`,
|
||||
};
|
||||
}
|
||||
|
||||
function PluginCollection({
|
||||
plugins,
|
||||
layout,
|
||||
@@ -440,13 +400,17 @@ function PluginCollection({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="grid gap-3" style={gridStyle(layout.columns)}>
|
||||
{plugins.map((p) => (
|
||||
<div key={p.id} className="flex flex-col *:flex-1">
|
||||
{renderCard(p)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<DataView
|
||||
items={plugins}
|
||||
view="grid"
|
||||
getKey={(p) => p.id}
|
||||
renderCard={renderCard}
|
||||
columns={{}}
|
||||
size={layout.columns}
|
||||
minCardWidth={400}
|
||||
listColumns={listColumns}
|
||||
empty={null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,24 @@ describe("DataView", () => {
|
||||
expect(screen.queryByText("Name")).toBeNull();
|
||||
});
|
||||
|
||||
it("uses the picked size over breakpoint columns", () => {
|
||||
const { container } = render(
|
||||
<DataView
|
||||
items={items}
|
||||
view="grid"
|
||||
getKey={(item) => item.id}
|
||||
renderCard={(item) => <div>card {item.name}</div>}
|
||||
columns={{ base: 1, md: 2 }}
|
||||
size="3"
|
||||
listColumns={[]}
|
||||
empty={null}
|
||||
/>,
|
||||
);
|
||||
const grid = container.firstElementChild as HTMLElement;
|
||||
expect(grid.className).not.toContain("md:grid-cols-2");
|
||||
expect(grid.style.gridTemplateColumns).toContain("/ 3");
|
||||
});
|
||||
|
||||
it("renders a header and clickable rows in list mode", () => {
|
||||
const onRowClick = vi.fn();
|
||||
renderView("list", onRowClick);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PanelShell,
|
||||
Segmented,
|
||||
ViewToggle,
|
||||
gridSizeStyle,
|
||||
} from "@/components/panel-layout";
|
||||
|
||||
afterEach(cleanup);
|
||||
@@ -72,6 +73,34 @@ describe("ViewToggle", () => {
|
||||
fireEvent.click(screen.getByLabelText("panel.compactRows"));
|
||||
expect(onDensity).toHaveBeenCalledWith("compact");
|
||||
});
|
||||
|
||||
it("shows the columns picker only in grid view", () => {
|
||||
const props = { onView: vi.fn(), columns: "auto" as const };
|
||||
const { rerender } = render(
|
||||
<ViewToggle view="grid" {...props} onColumns={vi.fn()} />,
|
||||
);
|
||||
expect(screen.getByLabelText("panel.columns.title")).toBeTruthy();
|
||||
|
||||
rerender(<ViewToggle view="list" {...props} onColumns={vi.fn()} />);
|
||||
expect(screen.queryByLabelText("panel.columns.title")).toBeNull();
|
||||
|
||||
rerender(<ViewToggle view="grid" {...props} />);
|
||||
expect(screen.queryByLabelText("panel.columns.title")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("gridSizeStyle", () => {
|
||||
it("fits auto cards to the pane", () => {
|
||||
expect(gridSizeStyle("auto", 280).gridTemplateColumns).toBe(
|
||||
"repeat(auto-fill, minmax(min(100%, 280px), 1fr))",
|
||||
);
|
||||
});
|
||||
|
||||
it("splits the width for a fixed count", () => {
|
||||
expect(gridSizeStyle("4").gridTemplateColumns).toContain(
|
||||
"(100% - 3 * 0.5rem) / 4",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Facts", () => {
|
||||
|
||||
@@ -183,17 +183,13 @@ describe("PluginsTab", () => {
|
||||
it("switches to a table and remembers it", async () => {
|
||||
await renderTab();
|
||||
expect(screen.queryByRole("table")).toBeNull();
|
||||
expect(
|
||||
screen.getByLabelText(en.plugins.manager.columns.title),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByLabelText(en.panel.columns.title)).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByLabelText(en.panel.listView));
|
||||
|
||||
expect(screen.getByRole("table")).toBeTruthy();
|
||||
expect(screen.getByText(en.plugins.manager.table.version)).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByLabelText(en.plugins.manager.columns.title),
|
||||
).toBeNull();
|
||||
expect(screen.queryByLabelText(en.panel.columns.title)).toBeNull();
|
||||
expect(localStorage.getItem("pluginsTab.view")).toBe("list");
|
||||
|
||||
cleanup();
|
||||
|
||||
Reference in new issue
Block a user