fix(webapp): require explicit native button types (#4692)
## Summary Add explicit types to native dashboard buttons and enforce `react/button-has-type`. This prevents action buttons from accidentally submitting a surrounding form. Shared button primitives retain their caller-selected submit and reset semantics with documented lint exceptions. Base: [#4691](https://github.com/triggerdotdev/trigger.dev/pull/4691)
This commit is contained in:
@@ -115,6 +115,7 @@
|
||||
{
|
||||
"files": ["apps/webapp/app/**/*.ts", "apps/webapp/app/**/*.tsx"],
|
||||
"rules": {
|
||||
"react/button-has-type": "error",
|
||||
"react/jsx-no-useless-fragment": "error",
|
||||
"react/no-unstable-nested-components": "error",
|
||||
"react/rules-of-hooks": "error",
|
||||
|
||||
@@ -970,6 +970,7 @@ function HeaderCellContent({
|
||||
)}
|
||||
{onFilterClick && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFilterClick();
|
||||
|
||||
@@ -240,6 +240,7 @@ export function SideMenuItem({
|
||||
}
|
||||
|
||||
/** Button styled to match {@link SideMenuItem}, for entries that open a dialog rather than navigate. */
|
||||
/* oxlint-disable react/button-has-type -- Callers can select button, reset, or submit semantics. */
|
||||
export const SideMenuItemButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{
|
||||
@@ -276,3 +277,4 @@ export const SideMenuItemButton = forwardRef<
|
||||
</button>
|
||||
);
|
||||
});
|
||||
/* oxlint-enable react/button-has-type */
|
||||
|
||||
@@ -67,6 +67,7 @@ export function AppliedFilter({
|
||||
</div>
|
||||
{removable && (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"group flex size-6 items-center justify-center focus-custom",
|
||||
variantClassName.clear
|
||||
|
||||
@@ -374,6 +374,7 @@ type ButtonPropsType = Pick<
|
||||
> &
|
||||
React.ComponentProps<typeof ButtonContent>;
|
||||
|
||||
/* oxlint-disable react/button-has-type -- Callers can select button, reset, or submit semantics. */
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
({ type, disabled, autoFocus, onClick, "aria-label": ariaLabel, ...props }, ref) => {
|
||||
const innerRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -435,6 +436,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
return buttonElement;
|
||||
}
|
||||
);
|
||||
/* oxlint-enable react/button-has-type */
|
||||
|
||||
type LinkPropsType = Pick<
|
||||
LinkProps,
|
||||
|
||||
@@ -51,6 +51,7 @@ function PopoverSectionHeader({
|
||||
);
|
||||
}
|
||||
|
||||
/* oxlint-disable react/button-has-type -- The trigger supports form button semantics. */
|
||||
const PopoverMenuItem = React.forwardRef<
|
||||
HTMLButtonElement | HTMLAnchorElement,
|
||||
{
|
||||
@@ -141,6 +142,7 @@ const PopoverMenuItem = React.forwardRef<
|
||||
}
|
||||
);
|
||||
PopoverMenuItem.displayName = "PopoverMenuItem";
|
||||
/* oxlint-enable react/button-has-type */
|
||||
|
||||
function PopoverCustomTrigger({
|
||||
isOpen,
|
||||
|
||||
@@ -582,6 +582,7 @@ export function SelectButtonItem({
|
||||
}: SelectButtonItemProps) {
|
||||
const render = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn("block w-full text-left", selectItemClasses, props.className)}
|
||||
/>
|
||||
|
||||
@@ -433,6 +433,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
<div className={cn(flexClasses, "gap-2")}>
|
||||
{leadingContent}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"inline-flex cursor-pointer items-center gap-2 focus:outline-hidden",
|
||||
@@ -446,6 +447,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn("cursor-pointer focus:outline-hidden", flexClasses, actionClassName)}
|
||||
tabIndex={isTabbableCell ? 0 : -1}
|
||||
|
||||
@@ -128,6 +128,7 @@ export function ToastUI({
|
||||
{actionNode}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"-mr-1 ms-auto rounded p-2 text-text-dimmed transition hover:text-text-bright",
|
||||
title && "-mt-1"
|
||||
|
||||
@@ -344,6 +344,7 @@ export function ToolUseRow({ tool }: { tool: ToolUse }) {
|
||||
>
|
||||
{availableTabs.map((tab) => (
|
||||
<button
|
||||
type="button"
|
||||
key={tab}
|
||||
onClick={() => handleTabClick(tab)}
|
||||
className={`px-2.5 py-1 text-[11px] capitalize transition-colors ${
|
||||
|
||||
@@ -47,6 +47,7 @@ function ToolDefRow({ def, wasCalled }: { def: ToolDefinition; wasCalled: boolea
|
||||
{def.parametersJson && (
|
||||
<div className="pl-3.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSchema(!showSchema)}
|
||||
className="text-[10px] text-text-link hover:underline"
|
||||
>
|
||||
|
||||
+1
@@ -436,6 +436,7 @@ function Upgradable({
|
||||
<span>
|
||||
Save your changes or{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="inline text-indigo-500 hover:text-indigo-300"
|
||||
onClick={() => {
|
||||
setAllocation(initialAllocation(environments));
|
||||
|
||||
@@ -164,6 +164,7 @@ export default function Story() {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{exampleQueries.map((example) => (
|
||||
<button
|
||||
type="button"
|
||||
key={example.name}
|
||||
onClick={() => setQuery(example.query)}
|
||||
className="rounded bg-background-raised px-3 py-1.5 text-sm text-text-dimmed transition hover:bg-surface-control hover:text-text-bright"
|
||||
|
||||
Reference in New Issue
Block a user