Skip to content

Commit

Permalink
Added eslint rules that warn for non-readonly props, as they sometime…
Browse files Browse the repository at this point in the history
…s cause bugs and it makes sense to just make all the props readonly.
  • Loading branch information
TPReal committed Nov 15, 2023
1 parent b2ef0b0 commit 14e5c30
Show file tree
Hide file tree
Showing 42 changed files with 124 additions and 107 deletions.
17 changes: 17 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@
{
"selector": "JSXOpeningElement > JSXIdentifier[name='button']",
"message": "Use <Button> instead."
},
{
// It's impossible to match slashes, so here \W is used instead.
"selector": "ImportDeclaration[source.value=/^\\.\\.\\W\\.\\.\\W/]",
"message": "Don't import from ../../, use the absolute path instead."
},
{
"selector": "TSInterfaceDeclaration[id.name=/Props$/] > TSInterfaceBody > TSPropertySignature[readonly!='true']",
"message": "Mark props as readonly, this avoids some tricky bugs."
},
{
"selector": "TSInterfaceDeclaration[id.name=/Props$/] > TSInterfaceBody > TSPropertySignature > TSTypeAnnotation > TSArrayType",
"message": "Mark array props as readonly arrays (`readonly Type[]`), this avoids some tricky bugs."
},
{
"selector": "TSInterfaceDeclaration[id.name=/Props$/] > TSInterfaceBody > TSPropertySignature > TSTypeAnnotation > TSTypeReference[typeName.name=/^(Set|Map)$/]",
"message": "Mark Set/Map props as ReadonlySet/ReadonlyMap, this avoids some tricky bugs."
}
],
"no-unused-expressions": "warn",
Expand Down
4 changes: 2 additions & 2 deletions resources/js/components/felte-form/FelteSubmit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ interface Props extends htmlAttributes.button {
* The cancel handler. If present, there will be a cancel button to the right of the submit button.
* The value is the handler called on cancel click. Default: no cancel.
*/
cancel?: () => void;
readonly cancel?: () => void;
/** Whether to include the unknown validation messages above the button. Default: true. */
includeUnknownValidationMessages?: boolean;
readonly includeUnknownValidationMessages?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/felte-form/ValidationMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {Index, VoidComponent, createMemo, on} from "solid-js";
import {HideableSection} from "../ui/HideableSection";

interface Props {
fieldName: string;
readonly fieldName: string;
}

export const ValidationMessages: VoidComponent<Props> = (props) => {
Expand Down
4 changes: 2 additions & 2 deletions resources/js/components/ui/Capitalize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {VoidComponent, splitProps} from "solid-js";
import {cx, htmlAttributes} from "../utils";

interface Props extends htmlAttributes.span {
text: string | undefined;
capitalize?: boolean;
readonly text: string | undefined;
readonly capitalize?: boolean;
}

/** Displays a span with the specified text with its first letter capitalised using CSS. */
Expand Down
4 changes: 2 additions & 2 deletions resources/js/components/ui/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {useLangFunc} from "../utils";
import {Button} from "./Button";

interface Props {
text: string | undefined;
readonly text: string | undefined;
/** Whether the text should be displayed on hover. */
textInTitle?: boolean;
readonly textInTitle?: boolean;
}

/** A "Copy to clipboard" icon, copying the specified text on click. */
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/Email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {CopyToClipboard} from "./CopyToClipboard";
import {EMPTY_VALUE_SYMBOL} from "./symbols";

interface Props extends htmlAttributes.div {
email: string | undefined;
readonly email: string | undefined;
}

/** A component for displaying a copiable email address. No mailto. */
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/HideableSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {ParentComponent} from "solid-js";
import {debouncedAccessor} from "../utils";

interface Props {
show: boolean;
readonly show: boolean;
}

const TRANSITION_TIME_MS = 200;
Expand Down
10 changes: 5 additions & 5 deletions resources/js/components/ui/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,30 @@ import s from "./Modal.module.scss";
import {ChildrenOrFunc, getChildrenElement} from "./children_func";

interface BaseProps<T> {
title?: string;
readonly title?: string;
/**
* Style of the modal, mostly for specifying the size. When absent, a reasonable minimum width is used.
*/
style?: JSX.CSSProperties;
readonly style?: JSX.CSSProperties;
/**
* A value determining whether the modal is open. If truthy, the value is also available for the modal
* children in its function form.
*
* The only way to close the modal is to set this to a falsy value (the modal never closes itself).
*/
open: T | undefined | false;
readonly open: T | undefined | false;
/**
* Children can be either a standard JSX element, or a function that is called with an accessor
* to the non-nullable value passed to open. This is similar to the function form of the Show component.
* see Modal doc for example.
*/
children: ChildrenOrFunc<[Accessor<NonNullable<T>>]>;
readonly children: ChildrenOrFunc<[Accessor<NonNullable<T>>]>;
/**
* Handler called when the user tries to escape from the modal, either by pressing the Escape key,
* or by clicking outside. If these actions should close the modal, this handler needs to set the
* open prop to false.
*/
onEscape?: (reason: EscapeReason) => void;
readonly onEscape?: (reason: EscapeReason) => void;
}

export const MODAL_STYLE_PRESETS = {
Expand Down
4 changes: 2 additions & 2 deletions resources/js/components/ui/Table/CellRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {Dynamic} from "solid-js/web";

export interface CellRendererProps<TProps extends object> {
/** The cell content (e.g. component/string) render */
component: ColumnDefTemplate<TProps> | undefined;
readonly component: ColumnDefTemplate<TProps> | undefined;
/** The props to pass to the cell component */
props: TProps;
readonly props: TProps;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/Table/ColumnName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {useTable} from "./TableContext";

interface Props {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
def: ColumnDef<any>;
readonly def: ColumnDef<any>;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions resources/js/components/ui/Table/FilterIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import {Dynamic} from "solid-js/web";
import {Button} from "../Button";

interface Props {
isFiltering?: boolean;
class?: string;
onClear?: () => void;
readonly isFiltering?: boolean;
readonly class?: string;
readonly onClear?: () => void;
}

export const FilterIcon: VoidComponent<Props> = (props) => {
Expand Down
4 changes: 2 additions & 2 deletions resources/js/components/ui/Table/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import s from "./Header.module.scss";

interface Props {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ctx: HeaderContext<any, unknown>;
filter?: JSX.Element;
readonly ctx: HeaderContext<any, unknown>;
readonly filter?: JSX.Element;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/Table/IdColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {CopyToClipboard} from "../CopyToClipboard";
import {EMPTY_VALUE_SYMBOL} from "../symbols";

interface Props {
id: string | undefined;
readonly id: string | undefined;
}

/** A component for displaying a copiable id, truncated to take less space. */
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/Table/SortMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {useTable} from "./TableContext";

interface Props {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
column: Column<any>;
readonly column: Column<any>;
}

const ICONS = new Map<false | SortDirection, IconTypes>()
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/Table/TQueryTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {ColumnDef, IdentifiedColumnDef, RowData, SortingState, createSolidTable} from "@tanstack/solid-table";
import {toastMessages} from "components/utils/toast";
import {FilterH} from "data-access/memo-api/tquery/filter_utils";
import {ColumnConfig, createTableRequestCreator, tableHelper} from "data-access/memo-api/tquery/table";
import {createTQuery} from "data-access/memo-api/tquery/tquery";
Expand All @@ -20,7 +21,6 @@ import {
getBaseTableOptions,
useTableCells,
} from ".";
import {toastMessages} from "../../utils/toast";
import {ColumnFilterController, FilteringParams} from "./tquery_filters/ColumnFilterController";

declare module "@tanstack/table-core" {
Expand Down
16 changes: 8 additions & 8 deletions resources/js/components/ui/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,27 +91,27 @@ export const AUTO_SIZE_COLUMN_DEFS = {
} satisfies Partial<ColumnDef<object>>;

interface Props<T = object> {
table: TanStackTable<T>;
readonly table: TanStackTable<T>;
/** Table mode. Default: embedded. */
mode?: DisplayMode;
readonly mode?: DisplayMode;
/**
* The iteration component used for iterating over rows. Default: For.
*
* The For iteration might be more useful for a frontend table, where the rows is a constant
* collection of elements. For backend tables, when the rows change identity after each query
* to the backend, Index might be a better choice.
*/
rowsIteration?: "For" | "Index";
readonly rowsIteration?: "For" | "Index";
/** The content to put above the table, e.g. the global search bar. It has access to the table context */
aboveTable?: () => JSX.Element;
readonly aboveTable?: () => JSX.Element;
/** The content to put below the table, e.g. the pagination controller. It has access to the table context */
belowTable?: () => JSX.Element;
readonly belowTable?: () => JSX.Element;
/** Whether the whole table content is loading. This hides the whole table and displays a spinner. */
isLoading?: boolean;
readonly isLoading?: boolean;
/** Whether the content of the table is reloading. This dims the table and makes it inert. */
isDimmed?: boolean;
readonly isDimmed?: boolean;
/** A signal which changes when the table should scroll itself to the top. */
scrollToTopSignal?: Accessor<unknown>;
readonly scrollToTopSignal?: Accessor<unknown>;
}

const DEFAULT_PROPS = {
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/Table/TableSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {ParentProps, VoidComponent, createComputed, createSignal, splitProps} fr
import {useTable} from ".";

interface Props extends htmlAttributes.div {
placeholder?: string;
readonly placeholder?: string;
}

export const TableSearch: VoidComponent<ParentProps<Props>> = (allProps) => {
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/Table/TableSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface Props {
* Number of rows. Must be specified for backend tables where it cannot be taken from the
* table object.
*/
rowsCount?: number;
readonly rowsCount?: number;
}

export const TableSummary: VoidComponent<Props> = (props) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ type DateTimeRangeFilter =
| (DateColumnFilter & {op: "="});

interface DateTimeColumnProps extends FilterControlProps<DateTimeRangeFilter> {
columnType?: "datetime";
readonly columnType?: "datetime";
/** Whether the inputs should set date and time. Default is only date. */
useDateTimeInputs?: boolean;
readonly useDateTimeInputs?: boolean;
}

interface DateColumnProps extends FilterControlProps<DateTimeRangeFilter> {
columnType: "date";
readonly columnType: "date";
}

type Props = DateColumnProps | DateTimeColumnProps;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {makeSelectItem} from "./select_items";
import {FilterControlProps} from "./types";

interface StringColumnProps extends FilterControlProps {
columnType: "string" | "text";
readonly columnType: "string" | "text";
}

export const TextualFilterControl: VoidComponent<StringColumnProps> = (props) => {
Expand Down
16 changes: 8 additions & 8 deletions resources/js/components/ui/Table/tquery_filters/select_items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,28 @@ import {cx} from "components/utils";
import {Match, Show, Switch, VoidComponent} from "solid-js";

interface SelectItemSymbolProps {
symbol: string;
class?: string;
readonly symbol: string;
readonly class?: string;
}

export const SelectItemSymbol: VoidComponent<SelectItemSymbolProps> = (props) => (
<span class={cx("font-semibold", props.class)}>{props.symbol}</span>
);

interface SelectItemDescriptionProps {
description: string;
class?: string;
readonly description: string;
readonly class?: string;
}

export const SelectItemDescription: VoidComponent<SelectItemDescriptionProps> = (props) => (
<span class={cx("text-sm text-gray-600", props.class)}>{props.description}</span>
);

interface SelectItemLabelProps {
symbol?: string;
symbolClass?: string;
description?: string;
infoIcon?: InfoIconProps;
readonly symbol?: string;
readonly symbolClass?: string;
readonly description?: string;
readonly infoIcon?: InfoIconProps;
}

export const SelectItemLabelOnList: VoidComponent<SelectItemLabelProps> = (props) => (
Expand Down
8 changes: 4 additions & 4 deletions resources/js/components/ui/Table/tquery_filters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import {FilterH} from "data-access/memo-api/tquery/filter_utils";
import {VoidComponent} from "solid-js";

export interface FilterControlProps<F extends FilterH = FilterH> {
name: string;
nullable?: boolean;
filter: F | undefined;
setFilter: (filter: F | undefined) => void;
readonly name: string;
readonly nullable?: boolean;
readonly filter: F | undefined;
readonly setFilter: (filter: F | undefined) => void;
}

export type FilterControl<F extends FilterH = FilterH> = VoidComponent<FilterControlProps<F>>;
10 changes: 5 additions & 5 deletions resources/js/components/ui/TranslatedText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import {Capitalize} from "./Capitalize";

interface Props {
/** The highest priority source. */
override?: () => JSX.Element;
readonly override?: () => JSX.Element;
/** The second-highest priority source. */
langFunc?: LangEntryFunc | [func?: LangPrefixFunc, subKey?: string];
readonly langFunc?: LangEntryFunc | [func?: LangPrefixFunc, subKey?: string];
/** Whether to capitalize. Applies to the result of the lang func only. */
capitalize?: boolean;
readonly capitalize?: boolean;
/** The last source. */
fallbackCode?: string;
readonly fallbackCode?: string;
/* The function that wraps the computed text. Called with no argument if there is no text. */
wrapIn?: (text?: JSX.Element) => JSX.Element;
readonly wrapIn?: (text?: JSX.Element) => JSX.Element;
}

/**
Expand Down
14 changes: 7 additions & 7 deletions resources/js/components/ui/calendar/FullCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ export const MODES = ["month", "week", "day"] as const;
export type Mode = (typeof MODES)[number];

interface Props extends htmlAttributes.div {
locale: Intl.Locale;
resourceGroups: readonly ResourceGroup[];
holidays?: readonly DateTime[];
modes?: Mode[];
initialMode?: Mode;
initialResourcesSelection?: readonly string[];
initialDay?: DateTime;
readonly locale: Intl.Locale;
readonly resourceGroups: readonly ResourceGroup[];
readonly holidays?: readonly DateTime[];
readonly modes?: readonly Mode[];
readonly initialMode?: Mode;
readonly initialResourcesSelection?: readonly string[];
readonly initialDay?: DateTime;
}

const defaultProps = () =>
Expand Down
4 changes: 2 additions & 2 deletions resources/js/components/ui/form/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {VoidComponent} from "solid-js";
import {FieldLabel, labelIdForField} from "./FieldLabel";

interface Props extends htmlAttributes.input {
name: string;
label?: string;
readonly name: string;
readonly label?: string;
}

/**
Expand Down
Loading

0 comments on commit 14e5c30

Please sign in to comment.