Skip to content

Commit 0d179ef

Browse files
betegonclaude
andcommitted
refactor(dashboard): deduplicate widget validation and resolution into shared module
- Move resolveWidgetIndex from edit.ts to resolve.ts (shared by edit + delete) - Extract validateWidgetEnums to resolve.ts (shared by add + edit) - Remove inline enum validation from add.ts and edit.ts - Remove inline widget-index logic from delete.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6ae803c commit 0d179ef

File tree

4 files changed

+71
-89
lines changed

4 files changed

+71
-89
lines changed

src/commands/dashboard/resolve.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import type { parseOrgProjectArg } from "../../lib/arg-parsing.js";
1010
import { ContextError, ValidationError } from "../../lib/errors.js";
1111
import { resolveOrg } from "../../lib/resolve-target.js";
1212
import { isAllDigits } from "../../lib/utils.js";
13+
import {
14+
type DashboardWidget,
15+
DISPLAY_TYPES,
16+
WIDGET_TYPES,
17+
} from "../../types/dashboard.js";
1318

1419
/**
1520
* Resolve org slug from a parsed org/project target argument.
@@ -119,3 +124,62 @@ export async function resolveDashboardId(
119124

120125
return match.id;
121126
}
127+
128+
/**
129+
* Resolve widget index from --index or --title flags.
130+
*
131+
* @param widgets - Array of widgets in the dashboard
132+
* @param index - Explicit 0-based widget index
133+
* @param title - Widget title to match
134+
* @returns Resolved widget index
135+
*/
136+
export function resolveWidgetIndex(
137+
widgets: DashboardWidget[],
138+
index: number | undefined,
139+
title: string | undefined
140+
): number {
141+
if (index !== undefined) {
142+
if (index < 0 || index >= widgets.length) {
143+
throw new ValidationError(
144+
`Widget index ${index} out of range (dashboard has ${widgets.length} widgets).`,
145+
"index"
146+
);
147+
}
148+
return index;
149+
}
150+
const matchIndex = widgets.findIndex((w) => w.title === title);
151+
if (matchIndex === -1) {
152+
throw new ValidationError(
153+
`No widget with title '${title}' found in dashboard.`,
154+
"title"
155+
);
156+
}
157+
return matchIndex;
158+
}
159+
160+
/**
161+
* Validate --display and --dataset flag values against known enums.
162+
*
163+
* @param display - Display type flag value
164+
* @param dataset - Dataset flag value
165+
*/
166+
export function validateWidgetEnums(display?: string, dataset?: string): void {
167+
if (
168+
display &&
169+
!DISPLAY_TYPES.includes(display as (typeof DISPLAY_TYPES)[number])
170+
) {
171+
throw new ValidationError(
172+
`Invalid --display value "${display}".\nValid display types: ${DISPLAY_TYPES.join(", ")}`,
173+
"display"
174+
);
175+
}
176+
if (
177+
dataset &&
178+
!WIDGET_TYPES.includes(dataset as (typeof WIDGET_TYPES)[number])
179+
) {
180+
throw new ValidationError(
181+
`Invalid --dataset value "${dataset}".\nValid datasets: ${WIDGET_TYPES.join(", ")}`,
182+
"dataset"
183+
);
184+
}
185+
}

src/commands/dashboard/widget/add.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,17 @@ import {
1515
assignDefaultLayout,
1616
type DashboardDetail,
1717
type DashboardWidget,
18-
DISPLAY_TYPES,
1918
parseAggregate,
2019
parseSortExpression,
2120
parseWidgetInput,
2221
prepareDashboardForUpdate,
2322
prepareWidgetQueries,
24-
WIDGET_TYPES,
2523
} from "../../../types/dashboard.js";
2624
import {
2725
parseDashboardPositionalArgs,
2826
resolveDashboardId,
2927
resolveOrgFromTarget,
28+
validateWidgetEnums,
3029
} from "../resolve.js";
3130

3231
type AddFlags = {
@@ -171,24 +170,7 @@ export const addCommand = buildCommand({
171170
);
172171
const dashboardId = await resolveDashboardId(orgSlug, dashboardRef);
173172

174-
// Validate --display value early for better error messages
175-
if (
176-
!DISPLAY_TYPES.includes(flags.display as (typeof DISPLAY_TYPES)[number])
177-
) {
178-
throw new ValidationError(
179-
`Invalid --display value "${flags.display}".\nValid display types: ${DISPLAY_TYPES.join(", ")}`,
180-
"display"
181-
);
182-
}
183-
if (
184-
flags.dataset &&
185-
!WIDGET_TYPES.includes(flags.dataset as (typeof WIDGET_TYPES)[number])
186-
) {
187-
throw new ValidationError(
188-
`Invalid --dataset value "${flags.dataset}".\nValid datasets: ${WIDGET_TYPES.join(", ")}`,
189-
"dataset"
190-
);
191-
}
173+
validateWidgetEnums(flags.display, flags.dataset);
192174

193175
const aggregates = (flags.query ?? ["count"]).map(parseAggregate);
194176
const columns = flags["group-by"] ?? [];

src/commands/dashboard/widget/delete.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
parseDashboardPositionalArgs,
2020
resolveDashboardId,
2121
resolveOrgFromTarget,
22+
resolveWidgetIndex,
2223
} from "../resolve.js";
2324

2425
type DeleteFlags = {
@@ -96,25 +97,7 @@ export const deleteCommand = buildCommand({
9697
const current = await getDashboard(orgSlug, dashboardId);
9798
const widgets = current.widgets ?? [];
9899

99-
let widgetIndex: number;
100-
if (flags.index !== undefined) {
101-
if (flags.index < 0 || flags.index >= widgets.length) {
102-
throw new ValidationError(
103-
`Widget index ${flags.index} out of range (dashboard has ${widgets.length} widgets).`,
104-
"index"
105-
);
106-
}
107-
widgetIndex = flags.index;
108-
} else {
109-
const matchIndex = widgets.findIndex((w) => w.title === flags.title);
110-
if (matchIndex === -1) {
111-
throw new ValidationError(
112-
`No widget with title '${flags.title}' found in dashboard.`,
113-
"title"
114-
);
115-
}
116-
widgetIndex = matchIndex;
117-
}
100+
const widgetIndex = resolveWidgetIndex(widgets, flags.index, flags.title);
118101

119102
const widgetTitle = widgets[widgetIndex]?.title;
120103
const updateBody = prepareDashboardForUpdate(current);

src/commands/dashboard/widget/edit.ts

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@ import {
1515
type DashboardDetail,
1616
type DashboardWidget,
1717
type DashboardWidgetQuery,
18-
DISPLAY_TYPES,
1918
parseAggregate,
2019
parseSortExpression,
2120
parseWidgetInput,
2221
prepareDashboardForUpdate,
2322
prepareWidgetQueries,
24-
WIDGET_TYPES,
2523
} from "../../../types/dashboard.js";
2624
import {
2725
parseDashboardPositionalArgs,
2826
resolveDashboardId,
2927
resolveOrgFromTarget,
28+
resolveWidgetIndex,
29+
validateWidgetEnums,
3030
} from "../resolve.js";
3131

3232
type EditFlags = {
@@ -50,53 +50,6 @@ type EditResult = {
5050
url: string;
5151
};
5252

53-
/** Resolve widget index from --index or --title flags */
54-
function resolveWidgetIndex(
55-
widgets: DashboardWidget[],
56-
index: number | undefined,
57-
title: string | undefined
58-
): number {
59-
if (index !== undefined) {
60-
if (index < 0 || index >= widgets.length) {
61-
throw new ValidationError(
62-
`Widget index ${index} out of range (dashboard has ${widgets.length} widgets).`,
63-
"index"
64-
);
65-
}
66-
return index;
67-
}
68-
const matchIndex = widgets.findIndex((w) => w.title === title);
69-
if (matchIndex === -1) {
70-
throw new ValidationError(
71-
`No widget with title '${title}' found in dashboard.`,
72-
"title"
73-
);
74-
}
75-
return matchIndex;
76-
}
77-
78-
/** Validate enum flag values */
79-
function validateEditEnums(flags: EditFlags): void {
80-
if (
81-
flags.display &&
82-
!DISPLAY_TYPES.includes(flags.display as (typeof DISPLAY_TYPES)[number])
83-
) {
84-
throw new ValidationError(
85-
`Invalid --display value "${flags.display}".\nValid display types: ${DISPLAY_TYPES.join(", ")}`,
86-
"display"
87-
);
88-
}
89-
if (
90-
flags.dataset &&
91-
!WIDGET_TYPES.includes(flags.dataset as (typeof WIDGET_TYPES)[number])
92-
) {
93-
throw new ValidationError(
94-
`Invalid --dataset value "${flags.dataset}".\nValid datasets: ${WIDGET_TYPES.join(", ")}`,
95-
"dataset"
96-
);
97-
}
98-
}
99-
10053
/** Merge query-level flags over existing widget query */
10154
function mergeQueries(
10255
flags: EditFlags,
@@ -258,7 +211,7 @@ export const editCommand = buildCommand({
258211
);
259212
}
260213

261-
validateEditEnums(flags);
214+
validateWidgetEnums(flags.display, flags.dataset);
262215

263216
const { dashboardRef, targetArg } = parseDashboardPositionalArgs(args);
264217
const parsed = parseOrgProjectArg(targetArg);

0 commit comments

Comments
 (0)