Skip to content

fix(site): tighten interface design for various frontend utility functions #18894

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

Parkreiner
Copy link
Member

@Parkreiner Parkreiner commented Jul 16, 2025

Precursor to #18895
Splitting this off so that the changes are easier to review.

Changes made

  • Improve type-safety for the withQuery Storybook decorator
  • Centralized almost all queries that deal with template versions to use a shared, exported query key prefix
  • Updated useFilter and usePagination to have much more strict return types, and decoupled them from React Router at the interface level
  • Also added some extra input validation and performance optimizations to useFilter and usePagination
  • Removed a stale data when working with checked workspaces for the /workspaces page
  • Removed hacky useEffect call for syncing URL state to checked workspaces, in favor of explicit state syncs
  • Did some extra cleanup and removed unused values

Notes

  • Many of the changes here were in service of the main PR. I'll try to highlight notable areas, but if there's anything that's not clear, feel free to post a comment in this PR. Ideally you shouldn't really have to look at the other PR to understand this one, so if something's confusing, that's a sign that something's wrong

Summary by CodeRabbit

  • Refactor

    • Improved handling of URL search parameters and state synchronization in filter and pagination features across multiple pages.
    • Centralized and clarified state management for workspace selection and batch actions on the Workspaces page.
    • Enhanced type safety and naming consistency in batch actions and filter components.
    • Updated filter and pagination hooks to accept explicit parameters and callbacks for better maintainability.
    • Streamlined prop naming and menu handling in workspace filter components for clarity.
  • Bug Fixes

    • Prevented unnecessary state updates when filter values remain unchanged.
  • Tests

    • Updated tests for improved type safety and more precise assertions.

@Parkreiner Parkreiner self-assigned this Jul 16, 2025
@Parkreiner Parkreiner requested a review from aslilac as a code owner July 16, 2025 01:40
@Parkreiner Parkreiner changed the title fix(site): tighten up interface design for various frontend utility functions fix(site): tighten interface design for various frontend utility functions Jul 16, 2025
Comment on lines -51 to -64
const getTemplatesByOrganizationQueryKey = (
organization: string,
options?: GetTemplatesOptions,
) => [organization, "templates", options?.deprecated];

const templatesByOrganization = (
organization: string,
options: GetTemplatesOptions = {},
) => {
return {
queryKey: getTemplatesByOrganizationQueryKey(organization, options),
queryFn: () => API.getTemplatesByOrganization(organization, options),
};
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weren't used at all

@@ -121,9 +106,11 @@ export const templateExamples = () => {
};
};

export const templateVersionRoot: string = "templateVersion";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ends up being more important for the upcoming PR, but I really, really think that we need to find the time to formalize our entire data layer, and be way more deliberate with our React Query caching

I fully expect our caching to create a super, super hard-to-recreate bug sometime in the future, because it's a bunch of semi-structured global mutable state right now

Copy link
Member Author

@Parkreiner Parkreiner Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also added an explicit string type annotation so that TypeScript doesn't over-infer the value and treat it as a literal. We shouldn't know what exact combination of characters it is – just what purpose the string serves in the greater codebase as an opaque value

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am very interested to hear you elaborate on all of this because I am not sure I follow. 😅

Comment on lines +33 to +34
searchParams: URLSearchParams;
onSearchParamsChange: (newParams: URLSearchParams) => void;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decoupled type signature from React Router. Splitting this up gives us a lot more freedom, which comes up later in this PR

Comment on lines +38 to +45
export type UseFilterResult = Readonly<{
query: string;
values: FilterValues;
used: boolean;
update: (newValues: string | FilterValues) => void;
debounceUpdate: (newValues: string | FilterValues) => void;
cancelDebounce: () => void;
}>;
Copy link
Member Author

@Parkreiner Parkreiner Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the previous type was super, super risky, because it was derived from a function that didn't have much type-safety

With this, the contract is way more explicit and simple, and there's basically no chance of the TypeScript LSP getting confused and dropping properties for tooltips

Comment on lines +10 to +15
type UsePaginationResult = Readonly<{
page: number;
limit: number;
offset: number;
goToPage: (page: number) => void;
}>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were missing a return type altogether before

Comment on lines +30 to +34
const abortNavigation =
page === newPage || !Number.isFinite(page) || !Number.isInteger(page);
if (abortNavigation) {
return;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another render optimization

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this render optimization? Not re-rendering the page if it's the same page?

Copy link
Member Author

@Parkreiner Parkreiner Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there's two parts:

  • The basic input validation from the Number static methods
  • The equality check

I could maybe add a comment, but React only has Object.is for change detection, so in general, a lot of render optimizations boil down to "only dispatch a new value (especially a non-primitive value) when you know the contents have changed". Most of the time, you don't have to worry about over-dispatching primitive values (as long as they're done outside a render, React will detect that nothing changed and do a no-op for the state update). But because JS is wacky, there are edge cases with Object.is and numbers, which is why there's the extra === check

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdym edge cases with Object.is? the only "edge case" I'm aware of is Object.is(0, -0) === false

const [suggestedName, setSuggestedName] = useState(() =>
generateWorkspaceName(),
);
const [suggestedName, setSuggestedName] = useState(generateWorkspaceName);
Copy link
Member Author

@Parkreiner Parkreiner Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extra wrapper function was redundant

(touched, parameter) => {
if (autofillByName[parameter.name] !== undefined) {
touched[parameter.name] = true;
}
return touched;
},
{} as Record<string, boolean>,
{},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed type assertion to remove risk of accidentally silencing the compiler

Comment on lines +70 to +72
const [checkedWorkspaceIds, setCheckedWorkspaceIds] = useState(
new Set<string>(),
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of a fix for a super nasty bug that I ran into when updating the batch-update UI

Because we were storing an entire copy of the original workspaces each time we checked off something, that also meant that as the query cache updated its data and triggered re-renders, anything that used the checked workspaces would receive the stale versions of the workspaces

"delete" | "update" | null
>(null);
const [urlSearchParams] = searchParamsResult;
const [activeBatchAction, setActiveBatchAction] = useState<BatchAction>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly re-jiggered confirmingBatchAction

@@ -142,7 +164,18 @@ const WorkspacesPage: FC = () => {
canCreateTemplate={permissions.createTemplates}
canChangeVersions={permissions.updateTemplates}
checkedWorkspaces={checkedWorkspaces}
onCheckChange={setCheckedWorkspaces}
onCheckChange={(newWorkspaces) => {
setCheckedWorkspaceIds((current) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another render optimization

onBatchDeleteTransition={() => setActiveBatchAction("delete")}
onBatchStartTransition={() => batchActions.start(checkedWorkspaces)}
onBatchStopTransition={() => batchActions.stop(checkedWorkspaces)}
onBatchUpdateTransition={() => {
Copy link
Member Author

@Parkreiner Parkreiner Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change becomes a little more relevant for the other PR, but even if this were the only PR, this would still help the update UI avoid stale state

Comment on lines +15 to +25
type UseBatchActionsResult = Readonly<{
isProcessing: boolean;
start: (workspaces: readonly Workspace[]) => Promise<WorkspaceBuild[]>;
stop: (workspaces: readonly Workspace[]) => Promise<WorkspaceBuild[]>;
delete: (workspaces: readonly Workspace[]) => Promise<WorkspaceBuild[]>;
updateTemplateVersions: (
payload: UpdateAllPayload,
) => Promise<WorkspaceBuild[]>;
favorite: (payload: readonly Workspace[]) => Promise<void>;
unfavorite: (payload: readonly Workspace[]) => Promise<void>;
}>;
Copy link
Member Author

@Parkreiner Parkreiner Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added explicit return type, which was super important here, because the function's return type was getting inferred as something super complicated and nasty from React Query

@Parkreiner Parkreiner requested a review from brettkolodny July 16, 2025 02:09
Comment on lines +80 to +88
type WorkspaceFilterProps = Readonly<{
filter: UseFilterResult;
error: unknown;
templateMenu: TemplateFilterMenu;
statusMenu: StatusFilterMenu;

userMenu?: UserFilterMenu;
organizationsMenu?: OrganizationsFilterMenu;
}>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decouples the main state from the actual component props

The way I think about it is that props are a hack to get JS function to support named arguments and make it mirror HTML more, so you really shouldn't ever have any direct dependencies on that

Copy link
Contributor

@brettkolodny brettkolodny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall lgtm, I think these changes are a lot cleaner but would definitely wait for Kayla's review

Comment on lines +30 to +34
const abortNavigation =
page === newPage || !Number.isFinite(page) || !Number.isInteger(page);
if (abortNavigation) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this render optimization? Not re-rendering the page if it's the same page?

@@ -121,9 +106,11 @@ export const templateExamples = () => {
};
};

export const templateVersionRoot: string = "templateVersion";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: there seems to be inconsistencies within the code base between using camelCase or UPPER_SNAKE_CASE for file-wide constants so while out of scope for this PR I think deciding on one and sticking to it would be good

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's fair. The JS/TS way seems to be SCREAMING_CASE for constants, while the Go way seems to be to treat them as any other package value

I'm leaning towards switching this to SCREAMING_CASE, but I'll wait for Kayla to chime in

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to just name them like normal variables personally. an example of why in this context is that for an actual constant key vs a function that returns the key we get...

const aQueryKey = ["a"];
const bQueryKey = (id: string) => ["b"];

({ queryKey: aQueryKey });
({ queryKey: bQueryKey(bId) });

a small benefit of this aside from the visual consistency is that if a query key ever needs to go from truly constant to a function the amount of typing required is slightly smaller.

I think SCREAMING_CASE served more of a purpose in the days before LSPs. now it's just so much easier to tell what something is and where it's defined.

* workspace is in the middle of a transition and will eventually reach a more
* stable state/status.
*/
const ACTIVE_BUILD_STATUSES: readonly WorkspaceStatus[] = [
Copy link
Member Author

@Parkreiner Parkreiner Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This becomes exported in the second PR, which is why I put more of a public-facing comment on it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe worth moving to modules/workspaces when you do

Comment on lines -128 to -133
// We want to uncheck the selected workspaces always when the url changes
// because of filtering or pagination
// biome-ignore lint/correctness/useExhaustiveDependencies: consider refactoring
useEffect(() => {
setCheckedWorkspaces([]);
}, [urlSearchParams]);
Copy link
Member Author

@Parkreiner Parkreiner Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to comment on this, but the resetChecked logic above is meant to make sure that if we add more URL logic in the future, it doesn't break our filtering and pagination

We were coarsely syncing based on the entire URL params object. That worked fine for now, but if we were to add any logic on the same page that also dealt with URLs and had nothing to do with the filtering/pagination, it would still cause all the checkboxes to get unchecked on every change

Moving the state syncing to the event handlers for the filter/pagination hooks seemed like the better choice. That way, we're also not coupled to the hooks formatting the URLs a specific way either

@github-actions github-actions bot added the stale This issue is like stale bread. label Jul 24, 2025
Copy link

coderabbitai bot commented Jul 24, 2025

Walkthrough

This set of changes refactors how filter and pagination state is managed and propagated across multiple pages and components, especially regarding URL search parameters. It centralizes and synchronizes state management for filters, pagination, and batch actions, improves type safety and naming consistency, and removes some deprecated or redundant code. Components and hooks now generally accept explicit parameters and callbacks instead of React Router tuples, and prop naming is clarified throughout the workspace-related modules.

Changes

File(s) Change Summary
site/.storybook/preview.jsx Centralized the "queries" string key into a constant for maintainability in the withQuery decorator.
site/src/api/queries/templates.ts Removed deprecated template query functions; introduced and used a templateVersionRoot constant for query keys.
site/src/components/Filter/Filter.tsx Refactored useFilter to accept explicit searchParams and update callback; improved type definitions and update logic.
site/src/hooks/usePagination.ts Refactored usePagination to use explicit options object with searchParams and update callback; improved type safety and page handling.
site/src/pages/AuditPage/AuditPage.test.tsx Updated test to use explicit type in spy assertion for improved type safety.
site/src/pages/AuditPage/AuditPage.tsx
site/src/pages/ConnectionLogPage/ConnectionLogPage.tsx
site/src/pages/TemplatesPage/TemplatesPage.tsx
site/src/pages/UsersPage/UsersPage.tsx
Updated usage of useFilter and useSearchParams to new explicit parameter/callback style for filter and search param state.
site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx Changed state initialization and accumulator typing for clarity; no logic changes.
site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx Improved test typing and assertion style for workspace page tests.
site/src/pages/WorkspacesPage/WorkspacesPage.tsx Centralized and synchronized search param, filter, and workspace selection state; unified batch action state; improved state and handler naming.
site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx Updated story prop types and naming from filterProps to filterState for clarity.
site/src/pages/WorkspacesPage/WorkspacesPageView.tsx Renamed filter and batch action props for clarity; updated prop passing and menu handling for workspace view.
site/src/pages/WorkspacesPage/batchActions.ts Improved type safety and naming in batch actions hook; updated promise handling to use async/await.
site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx Refactored filter component to accept individual menu props; updated types and internal logic for clarity.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant PageComponent
    participant useFilter
    participant usePagination

    User->>PageComponent: Interacts with filter or pagination UI
    PageComponent->>useFilter: Passes searchParams, onSearchParamsChange
    PageComponent->>usePagination: Passes searchParams, onSearchParamsChange
    useFilter-->>PageComponent: Calls onSearchParamsChange with new params
    usePagination-->>PageComponent: Calls onSearchParamsChange with new params
    PageComponent->>PageComponent: Updates state, resets selections as needed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Poem

In the warren of code, search params now flow,
With filters and pages all ready to go.
Batch actions are tidy, their types crystal clear,
And workspaces hop with less state to fear.
A rabbit’s delight: refactoring done right,
Our burrow’s now cozy, efficient, and bright! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch mes/batch-update-01

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
site/src/components/Filter/Filter.tsx (1)

65-78: Technical debt: Direct URLSearchParams mutation

The direct mutation is a pragmatic workaround for existing issues in the codebase. The detailed comment is helpful for understanding why this approach was taken. Consider tracking this as technical debt for future refactoring.

Would you like me to create an issue to track refactoring this mutation pattern and the related code that depends on it?

site/src/pages/WorkspacesPage/WorkspacesPage.tsx (1)

193-212: Pragmatic approach to query invalidation with clear documentation.

The prefetch invalidation strategy improves UX by ensuring fresh data before the update modal opens. The comment clearly explains why granular invalidation is necessary given current architectural constraints.

Consider tracking this technical debt - a more unified query invalidation API would simplify this logic in the future.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0cdcf89 and fb9d8f2.

📒 Files selected for processing (16)
  • site/.storybook/preview.jsx (2 hunks)
  • site/src/api/queries/templates.ts (7 hunks)
  • site/src/components/Filter/Filter.tsx (1 hunks)
  • site/src/hooks/usePagination.ts (1 hunks)
  • site/src/pages/AuditPage/AuditPage.test.tsx (2 hunks)
  • site/src/pages/AuditPage/AuditPage.tsx (1 hunks)
  • site/src/pages/ConnectionLogPage/ConnectionLogPage.tsx (1 hunks)
  • site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx (2 hunks)
  • site/src/pages/TemplatesPage/TemplatesPage.tsx (1 hunks)
  • site/src/pages/UsersPage/UsersPage.tsx (2 hunks)
  • site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx (2 hunks)
  • site/src/pages/WorkspacesPage/WorkspacesPage.tsx (10 hunks)
  • site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx (4 hunks)
  • site/src/pages/WorkspacesPage/WorkspacesPageView.tsx (10 hunks)
  • site/src/pages/WorkspacesPage/batchActions.ts (5 hunks)
  • site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx (3 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
site/**/*.tsx

📄 CodeRabbit Inference Engine (.cursorrules)

All user-facing frontend code is developed in TypeScript using React and lives in the site/ directory.

Files:

  • site/src/pages/ConnectionLogPage/ConnectionLogPage.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx
  • site/src/pages/UsersPage/UsersPage.tsx
  • site/src/pages/AuditPage/AuditPage.test.tsx
  • site/src/pages/AuditPage/AuditPage.tsx
  • site/src/pages/TemplatesPage/TemplatesPage.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
  • site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx
  • site/src/components/Filter/Filter.tsx
  • site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPage.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
site/src/**/*.tsx

📄 CodeRabbit Inference Engine (site/CLAUDE.md)

site/src/**/*.tsx: MUI components are deprecated - migrate away from these when encountered
Use shadcn/ui components first - check site/src/components for existing implementations
Emotion CSS is deprecated. Use Tailwind CSS instead.
Responsive design - use Tailwind's responsive prefixes (sm:, md:, lg:, xl:)
Do not use dark: prefix for dark mode
Group related Tailwind classes
Prefer Tailwind utilities over custom CSS when possible
Use Tailwind classes for all new styling
Replace Emotion css prop with Tailwind classes
Leverage custom color tokens: content-primary, surface-secondary, etc.
Use className with clsx for conditional styling
Don’t call component functions directly; render them via JSX. This keeps Hook rules intact and lets React optimize reconciliation.
After calling a setter you’ll still read the previous state during the same event; updates are queued and batched.
Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.
Pass a function to useState(initialFn) for lazy initialization—it runs only on the first render.
If the next state is Object.is-equal to the current one, React skips the re-render.
An Effect takes a setup function and optional cleanup; React runs setup after commit, cleanup before the next setup or on unmount.
The dependency array must list every reactive value referenced inside the Effect, and its length must stay constant.
Effects run only on the client, never during server rendering.
Use Effects solely to synchronize with external systems; if you’re not “escaping React,” you probably don’t need one.
Every sibling element in a list needs a stable, unique key prop. Never use array indexes or Math.random(); prefer data-driven IDs.
Keys aren’t passed to children and must not change between renders; if you return multiple nodes per item, use
useRef stores a mutable .current without causing re-renders.
Avoid reading or mutating refs during render; access them in event handlers or Effects ...

Files:

  • site/src/pages/ConnectionLogPage/ConnectionLogPage.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx
  • site/src/pages/UsersPage/UsersPage.tsx
  • site/src/pages/AuditPage/AuditPage.test.tsx
  • site/src/pages/AuditPage/AuditPage.tsx
  • site/src/pages/TemplatesPage/TemplatesPage.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
  • site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx
  • site/src/components/Filter/Filter.tsx
  • site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPage.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
site/src/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (site/CLAUDE.md)

site/src/**/*.{ts,tsx}: Use ES modules (import/export) syntax, not CommonJS (require)
Destructure imports when possible (eg. import { foo } from 'bar')
Prefer for...of over forEach for iteration
Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.
Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.
Only call Hooks at the top level of a function component or another custom Hook—never in loops, conditions, nested functions, or try / catch.
Only call Hooks from React functions. Regular JS functions, classes, event handlers, useMemo, etc. are off-limits.
Never pass Hooks around as values or mutate them dynamically. Keep Hook usage static and local to each component.
Don’t call Hooks (including useRef) inside loops, conditions, or map(). Extract a child component instead.

Files:

  • site/src/pages/ConnectionLogPage/ConnectionLogPage.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx
  • site/src/pages/UsersPage/UsersPage.tsx
  • site/src/pages/AuditPage/AuditPage.test.tsx
  • site/src/pages/AuditPage/AuditPage.tsx
  • site/src/pages/TemplatesPage/TemplatesPage.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
  • site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx
  • site/src/pages/WorkspacesPage/batchActions.ts
  • site/src/hooks/usePagination.ts
  • site/src/components/Filter/Filter.tsx
  • site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx
  • site/src/api/queries/templates.ts
  • site/src/pages/WorkspacesPage/WorkspacesPage.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
site/src/**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit Inference Engine (.cursorrules)

React components and pages are organized in the site/src/ directory, with Jest used for testing.

Files:

  • site/src/pages/AuditPage/AuditPage.test.tsx
  • site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
site/**/*.ts

📄 CodeRabbit Inference Engine (.cursorrules)

All user-facing frontend code is developed in TypeScript using React and lives in the site/ directory.

Files:

  • site/src/pages/WorkspacesPage/batchActions.ts
  • site/src/hooks/usePagination.ts
  • site/src/api/queries/templates.ts
site/src/components/**/*

📄 CodeRabbit Inference Engine (site/CLAUDE.md)

Create custom components only when shadcn alternatives don't exist

Files:

  • site/src/components/Filter/Filter.tsx
🧠 Learnings (16)
📓 Common learnings
Learnt from: CR
PR: coder/coder#0
File: .cursorrules:0-0
Timestamp: 2025-07-21T14:32:43.034Z
Learning: Applies to site/**/*.tsx : All user-facing frontend code is developed in TypeScript using React and lives in the `site/` directory.
Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.
Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.
Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : MUI components are deprecated - migrate away from these when encountered
Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.
site/src/pages/ConnectionLogPage/ConnectionLogPage.tsx (4)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never pass Hooks around as values or mutate them dynamically. Keep Hook usage static and local to each component.

site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx (10)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : MUI components are deprecated - migrate away from these when encountered

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Destructure imports when possible (eg. import { foo } from 'bar')

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : If the next state is Object.is-equal to the current one, React skips the re-render.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Use ES modules (import/export) syntax, not CommonJS (require)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use shadcn/ui components first - check site/src/components for existing implementations

Learnt from: CR
PR: coder/coder#0
File: .cursorrules:0-0
Timestamp: 2025-07-21T14:32:43.035Z
Learning: Applies to site/src/**/*.test.{ts,tsx,js,jsx} : React components and pages are organized in the site/src/ directory, with Jest used for testing.

Learnt from: CR
PR: coder/coder#0
File: .cursorrules:0-0
Timestamp: 2025-07-21T14:32:43.034Z
Learning: Applies to site/**/*.tsx : All user-facing frontend code is developed in TypeScript using React and lives in the site/ directory.

site/src/pages/UsersPage/UsersPage.tsx (9)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never pass Hooks around as values or mutate them dynamically. Keep Hook usage static and local to each component.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Destructure imports when possible (eg. import { foo } from 'bar')

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Don’t call Hooks (including useRef) inside loops, conditions, or map(). Extract a child component instead.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Don’t call component functions directly; render them via JSX. This keeps Hook rules intact and lets React optimize reconciliation.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : MUI components are deprecated - migrate away from these when encountered

Learnt from: CR
PR: coder/coder#0
File: .cursorrules:0-0
Timestamp: 2025-07-21T14:32:43.034Z
Learning: Applies to site/**/*.tsx : All user-facing frontend code is developed in TypeScript using React and lives in the site/ directory.

site/.storybook/preview.jsx (1)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

site/src/pages/AuditPage/AuditPage.test.tsx (1)

Learnt from: CR
PR: coder/coder#0
File: .cursorrules:0-0
Timestamp: 2025-07-21T14:32:43.035Z
Learning: Applies to site/src/**/*.test.{ts,tsx,js,jsx} : React components and pages are organized in the site/src/ directory, with Jest used for testing.

site/src/pages/AuditPage/AuditPage.tsx (5)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never pass Hooks around as values or mutate them dynamically. Keep Hook usage static and local to each component.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Don’t call Hooks (including useRef) inside loops, conditions, or map(). Extract a child component instead.

site/src/pages/TemplatesPage/TemplatesPage.tsx (6)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Destructure imports when possible (eg. import { foo } from 'bar')

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never pass Hooks around as values or mutate them dynamically. Keep Hook usage static and local to each component.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Use ES modules (import/export) syntax, not CommonJS (require)

site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx (3)

Learnt from: CR
PR: coder/coder#0
File: .cursorrules:0-0
Timestamp: 2025-07-21T14:32:43.035Z
Learning: Applies to site/src/**/*.test.{ts,tsx,js,jsx} : React components and pages are organized in the site/src/ directory, with Jest used for testing.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Use ES modules (import/export) syntax, not CommonJS (require)

Learnt from: CR
PR: coder/coder#0
File: .cursorrules:0-0
Timestamp: 2025-07-21T14:32:43.034Z
Learning: Applies to site/**/*.tsx : All user-facing frontend code is developed in TypeScript using React and lives in the site/ directory.

site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx (5)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Pass a function to useState(initialFn) for lazy initialization—it runs only on the first render.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Avoid reading or mutating refs during render; access them in event handlers or Effects after commit.

site/src/pages/WorkspacesPage/batchActions.ts (3)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

site/src/hooks/usePagination.ts (4)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : If the next state is Object.is-equal to the current one, React skips the re-render.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Every sibling element in a list needs a stable, unique key prop. Never use array indexes or Math.random(); prefer data-driven IDs.

site/src/components/Filter/Filter.tsx (10)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never pass Hooks around as values or mutate them dynamically. Keep Hook usage static and local to each component.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Don’t call Hooks (including useRef) inside loops, conditions, or map(). Extract a child component instead.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Don’t call component functions directly; render them via JSX. This keeps Hook rules intact and lets React optimize reconciliation.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Only call Hooks from React functions. Regular JS functions, classes, event handlers, useMemo, etc. are off-limits.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : MUI components are deprecated - migrate away from these when encountered

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Destructure imports when possible (eg. import { foo } from 'bar')

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: ALWAYS use TypeScript LSP tools first when investigating code - don't manually search files

site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx (9)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : MUI components are deprecated - migrate away from these when encountered

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Replace Emotion css prop with Tailwind classes

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Destructure imports when possible (eg. import { foo } from 'bar')

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never pass Hooks around as values or mutate them dynamically. Keep Hook usage static and local to each component.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Don’t call component functions directly; render them via JSX. This keeps Hook rules intact and lets React optimize reconciliation.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use shadcn/ui components first - check site/src/components for existing implementations

site/src/pages/WorkspacesPage/WorkspacesPage.tsx (15)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Avoid reading or mutating refs during render; access them in event handlers or Effects after commit.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : MUI components are deprecated - migrate away from these when encountered

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Use ES modules (import/export) syntax, not CommonJS (require)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never pass Hooks around as values or mutate them dynamically. Keep Hook usage static and local to each component.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : After calling a setter you’ll still read the previous state during the same event; updates are queued and batched.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Only call Hooks from React functions. Regular JS functions, classes, event handlers, useMemo, etc. are off-limits.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Don’t call Hooks (including useRef) inside loops, conditions, or map(). Extract a child component instead.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Don’t call component functions directly; render them via JSX. This keeps Hook rules intact and lets React optimize reconciliation.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Only call Hooks at the top level of a function component or another custom Hook—never in loops, conditions, nested functions, or try / catch.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use shadcn/ui components first - check site/src/components for existing implementations

Learnt from: CR
PR: coder/coder#0
File: .cursorrules:0-0
Timestamp: 2025-07-21T14:32:43.034Z
Learning: Applies to site/**/*.tsx : All user-facing frontend code is developed in TypeScript using React and lives in the site/ directory.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use Effects solely to synchronize with external systems; if you’re not “escaping React,” you probably don’t need one.

site/src/pages/WorkspacesPage/WorkspacesPageView.tsx (6)

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Use functional updates (setX(prev ⇒ …)) whenever next state depends on previous state.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : MUI components are deprecated - migrate away from these when encountered

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Never mutate props, state, or values returned by Hooks. Always create new objects or use the setter from useState.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.{ts,tsx} : Components and custom Hooks must be pure and idempotent—same inputs → same output; move side-effects to event handlers or Effects.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : After calling a setter you’ll still read the previous state during the same event; updates are queued and batched.

Learnt from: CR
PR: coder/coder#0
File: site/CLAUDE.md:0-0
Timestamp: 2025-07-21T14:33:50.919Z
Learning: Applies to site/src/**/*.tsx : Replace Emotion css prop with Tailwind classes

🧬 Code Graph Analysis (3)
site/src/pages/TemplatesPage/TemplatesPage.tsx (1)
site/src/components/Filter/Filter.tsx (1)
  • useFilter (49-93)
site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx (1)
site/src/testHelpers/renderHelpers.tsx (1)
  • renderWithAuth (88-115)
site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx (4)
site/src/components/Filter/Filter.tsx (2)
  • UseFilterResult (38-45)
  • MenuSkeleton (145-147)
site/src/pages/WorkspacesPage/filter/menus.tsx (4)
  • TemplateFilterMenu (69-69)
  • StatusFilterMenu (130-130)
  • TemplateMenu (76-96)
  • StatusMenu (137-148)
site/src/components/Filter/UserFilter.tsx (2)
  • UserFilterMenu (81-81)
  • UserMenu (89-109)
site/src/modules/tableFiltering/options.tsx (2)
  • OrganizationsFilterMenu (89-91)
  • OrganizationsMenu (98-121)
🔇 Additional comments (44)
site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx (2)

111-111: LGTM! Improved lazy initialization pattern.

The change correctly removes the redundant wrapper function. Passing generateWorkspaceName directly to useState leverages React's built-in lazy initialization - the function is called only on the first render, which is the intended behavior.


125-132: LGTM! Enhanced type safety with explicit generic typing.

The change correctly removes the type assertion and uses explicit generic typing reduce<Record<string, boolean>> instead. This approach is safer as it avoids potential compiler silencing while maintaining clear type information.

site/src/pages/AuditPage/AuditPage.test.tsx (2)

4-4: LGTM! Added type import for better test type safety.

The import of AuditLogsRequest enables explicit typing in the test assertion, improving type safety.


110-115: LGTM! Enhanced test assertion with explicit typing.

The change to toHaveBeenCalledWith<[AuditLogsRequest]> with explicit type parameter improves type safety in the test assertion. Also good to see the migration from the deprecated toBeCalledWith to the preferred toHaveBeenCalledWith.

site/src/pages/AuditPage/AuditPage.tsx (1)

36-38: LGTM! Improved hook interface with explicit parameters.

The change from searchParamsResult to separate searchParams and onSearchParamsChange parameters successfully decouples the useFilter hook from React Router's tuple pattern. This makes the interface more explicit and easier to understand, aligning with the PR's objectives for interface design improvements.

site/src/pages/TemplatesPage/TemplatesPage.tsx (2)

17-17: LGTM! Explicit destructuring for clearer parameter handling.

The explicit destructuring of useSearchParams() return value makes the code more readable and prepares for the improved useFilter interface.


20-22: LGTM! Consistent interface improvement.

The change to separate searchParams and onSearchParamsChange parameters follows the same pattern as other pages in this refactor, successfully decoupling from React Router's tuple pattern while maintaining functionality.

site/.storybook/preview.jsx (2)

104-109: LGTM! Good practice centralizing the magic string.

The introduction of queryParametersKey constant improves maintainability and reduces the risk of typos. The clear comment explains the reasoning well.


122-123: LGTM! Consistent usage of the centralized constant.

The replacement of hardcoded "queries" strings with the queryParametersKey constant maintains consistency and aligns with the maintainability improvements throughout the codebase.

site/src/pages/ConnectionLogPage/ConnectionLogPage.tsx (1)

32-34: LGTM! Interface improvement aligns with refactoring goals.

The change from passing a searchParamsResult tuple to explicit searchParams and onSearchParamsChange parameters improves the useFilter hook interface by decoupling it from React Router internals and making dependencies explicit.

site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx (3)

4-4: Good addition for type safety.

Adding the WorkspacesResponse type import ensures proper typing for mock server responses in tests.


33-36: Improved mock response formatting and type safety.

The explicit WorkspacesResponse type annotation and formatted JSON structure make the test more readable and type-safe.


41-41: Better semantic testing with findByRole.

Using findByRole("heading", { name: /Create a workspace/ }) is more semantically correct than findByText for testing heading elements.

site/src/pages/UsersPage/UsersPage.tsx (3)

42-42: Consistent with refactoring pattern.

Destructuring useSearchParams() into separate variables aligns with the systematic refactor to decouple hooks from React Router tuples.


60-60: Direct parameter passing improves clarity.

Passing searchParams directly to paginatedUsers is clearer than passing it as part of a tuple.


62-63: Explicit interface parameters improve design.

The updated useFilter call with explicit searchParams and onSearchParamsChange parameters follows the established refactoring pattern and improves the hook's interface design.

site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx (3)

33-33: Explicit type import improves clarity.

Using the explicit WorkspaceFilterState type instead of deriving it from ComponentProps makes the type dependencies clearer and more maintainable.


137-137: Consistent type annotation.

The explicit WorkspaceFilterState type annotation aligns with the imported type and improves type safety.


170-170: Better semantic naming with filterState.

Renaming from filterProps to filterState better reflects the semantic meaning of the data and aligns with the updated component interface.

Also applies to: 267-267

site/src/api/queries/templates.ts (2)

109-109: Good centralization of query key constant.

The templateVersionRoot constant centralizes all "templateVersion" string literals used in query keys, improving maintainability and reducing magic strings. The explicit string type annotation correctly prevents TypeScript from over-inferring the type as a literal.


113-113: Consistent usage of centralized constant.

Replacing all "templateVersion" string literals with the templateVersionRoot constant ensures consistency and makes future changes easier to manage across all template version query keys.

Also applies to: 124-124, 143-143, 206-206, 247-247, 254-254, 261-261, 283-283, 303-303

site/src/hooks/usePagination.ts (2)

3-23: Good refactoring to decouple from React Router

The explicit type definitions and decoupled interface improve type safety and make the hook more reusable. The readonly modifiers on both input and output types prevent accidental mutations.


22-28: Robust page parsing and offset calculation

Good defensive programming with proper validation of the page number and safe offset calculation that prevents negative values.

site/src/pages/WorkspacesPage/batchActions.ts (2)

6-26: Excellent type safety improvements

The explicit return type definition and extracted payload type improve code clarity and maintainability. The consistent use of readonly modifiers prevents accidental mutations.


81-112: Smart solution for TypeScript promise typing

Good use of async/await to ensure the mutation functions return Promise<void> instead of Promise<void[]>. The explanatory comment helps future maintainers understand the design decision.

site/src/pages/WorkspacesPage/WorkspacesPageView.tsx (3)

29-57: Clear and consistent prop naming

The renamed batch action handlers (onBatchDeleteTransition, etc.) and filter state type make the component's interface more explicit and easier to understand.


90-93: Better boolean variable naming

The rename to pageNumberIsInvalid follows boolean naming conventions and makes the code more readable.


119-126: Explicit menu prop passing improves type safety

Passing individual menu props instead of a nested object makes the component's dependencies clearer and enables better TypeScript type checking.

site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx (2)

1-88: Good type restructuring for better component interface

The separation of WorkspaceFilterState (for state management) and WorkspaceFilterProps (for component props) with individual menu properties improves type safety and makes the component's dependencies explicit.


90-135: Clean implementation with simplified conditional logic

Good use of the organizationsActive variable to simplify the conditional rendering logic. The component correctly handles the individual menu props.

site/src/components/Filter/Filter.tsx (2)

27-54: Excellent decoupling from React Router

The explicit type definition and decoupled interface make the hook more reusable and testable. The clear parameter documentation helps with maintainability.


57-63: Good performance optimization

The early return prevents unnecessary updates when the filter value hasn't changed, reducing re-renders and improving performance.

site/src/pages/WorkspacesPage/WorkspacesPage.tsx (12)

3-3: Good use of type imports and centralized query keys.

The addition of templateVersionRoot supports better query key management, and using type imports for TypeScript types is a best practice that improves build performance.

Also applies to: 5-5, 14-14


25-41: Well-structured constants with clear documentation.

The ACTIVE_BUILD_STATUSES constant properly centralizes the transitional workspace states, and the refresh interval constants are clearly documented. This improves code maintainability.


57-58: Clean type definition for batch actions.

The BatchAction union type provides type safety for the unified batch action state management.


61-77: Excellent refactoring of workspace selection state.

Using a Set<string> for workspace IDs instead of storing full objects prevents stale data issues and improves performance. The comment clearly explains the rationale for centralizing useSearchParams.


79-85: Good decoupling of pagination from React Router.

The new interface with explicit searchParams and onSearchParamsChange callback improves testability and makes the data flow more explicit. The resetChecked() call ensures UI consistency.


109-116: Proper coordination of filter, pagination, and selection state.

The filter change handler correctly resets both pagination (to page 1) and selections, preventing UI inconsistencies when filters change.


120-120: Correct property access for the refactored filter state.


144-144: Clean unification of batch action state.

The single activeBatchAction state variable simplifies the logic compared to separate boolean states. Deriving checkedWorkspaces from the ID set ensures fresh data.

Also applies to: 150-150, 154-155


167-178: Excellent implementation of the checkbox change handler.

The handler properly uses functional updates, includes a smart optimization to avoid unnecessary state updates when selection hasn't changed, and correctly creates new Set instances to maintain immutability.


188-192: Consistent and clear prop naming.

The renamed props (filterState, isRunningBatchAction, batch action handlers) improve API clarity and consistency.


227-234: Consistent batch confirmation dialog implementation.

Both confirmation dialogs properly use the unified activeBatchAction state and correctly reset it after completion or cancellation.

Also applies to: 238-248


257-270: Clean refactoring of the filter hook interface.

The explicit searchParams and onSearchParamsChange parameters improve testability and decouple the hook from React Router, consistent with the overall refactoring approach.

Comment on lines +29 to +39
goToPage: (newPage) => {
const abortNavigation =
page === newPage || !Number.isFinite(page) || !Number.isInteger(page);
if (abortNavigation) {
return;
}

const copy = new URLSearchParams(searchParams);
copy.set("page", page.toString());
onSearchParamsChange(copy);
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical bug: setting wrong page value

The goToPage function has a bug where it sets the current page instead of newPage. Also, the validation should check newPage instead of page.

Apply this fix:

 goToPage: (newPage) => {
   const abortNavigation =
-    page === newPage || !Number.isFinite(page) || !Number.isInteger(page);
+    page === newPage || !Number.isFinite(newPage) || !Number.isInteger(newPage);
   if (abortNavigation) {
     return;
   }

   const copy = new URLSearchParams(searchParams);
-  copy.set("page", page.toString());
+  copy.set("page", newPage.toString());
   onSearchParamsChange(copy);
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
goToPage: (newPage) => {
const abortNavigation =
page === newPage || !Number.isFinite(page) || !Number.isInteger(page);
if (abortNavigation) {
return;
}
const copy = new URLSearchParams(searchParams);
copy.set("page", page.toString());
onSearchParamsChange(copy);
},
goToPage: (newPage) => {
const abortNavigation =
page === newPage || !Number.isFinite(newPage) || !Number.isInteger(newPage);
if (abortNavigation) {
return;
}
const copy = new URLSearchParams(searchParams);
copy.set("page", newPage.toString());
onSearchParamsChange(copy);
},
🤖 Prompt for AI Agents
In site/src/hooks/usePagination.ts around lines 29 to 39, the goToPage function
incorrectly validates and sets the current page variable instead of the newPage
parameter. To fix this, update the validation conditions to check newPage for
finiteness and integer status, and set the "page" query parameter to
newPage.toString() instead of page.toString() before calling
onSearchParamsChange.

@github-actions github-actions bot removed the stale This issue is like stale bread. label Jul 25, 2025
* ambient types defined in `storybook.d.ts` to provide extra type-safety.
* Extracting main key to avoid typos.
*/
const queryParametersKey = "queries";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually it looks like this can be a tsx file in Storybook 9. I'm gonna upgrade us.

@@ -121,9 +106,11 @@ export const templateExamples = () => {
};
};

export const templateVersionRoot: string = "templateVersion";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am very interested to hear you elaborate on all of this because I am not sure I follow. 😅

Comment on lines +30 to +34
const abortNavigation =
page === newPage || !Number.isFinite(page) || !Number.isInteger(page);
if (abortNavigation) {
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdym edge cases with Object.is? the only "edge case" I'm aware of is Object.is(0, -0) === false

@@ -121,9 +106,11 @@ export const templateExamples = () => {
};
};

export const templateVersionRoot: string = "templateVersion";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to just name them like normal variables personally. an example of why in this context is that for an actual constant key vs a function that returns the key we get...

const aQueryKey = ["a"];
const bQueryKey = (id: string) => ["b"];

({ queryKey: aQueryKey });
({ queryKey: bQueryKey(bId) });

a small benefit of this aside from the visual consistency is that if a query key ever needs to go from truly constant to a function the amount of typing required is slightly smaller.

I think SCREAMING_CASE served more of a purpose in the days before LSPs. now it's just so much easier to tell what something is and where it's defined.


/**
* @todo 2025-07-15 - We have a slightly nasty bug here, where trying to
* update state the "React way" causes our code to break.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* update state the "React way" causes our code to break.
* update state immutably causes our code to break.

Comment on lines +125 to 133
const initialTouched = parameters.reduce<Record<string, boolean>>(
(touched, parameter) => {
if (autofillByName[parameter.name] !== undefined) {
touched[parameter.name] = true;
}
return touched;
},
{} as Record<string, boolean>,
{},
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better yet can we just get rid of the reduce here?

	const initialTouched = Object.fromEntries(
		parameters
			.filter((it) => autofillByName[it.name])
			.map((it) => [it.name, true]),
	);

* workspace is in the middle of a transition and will eventually reach a more
* stable state/status.
*/
const ACTIVE_BUILD_STATUSES: readonly WorkspaceStatus[] = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe worth moving to modules/workspaces when you do

Comment on lines +81 to +87
// Not a great idea to return the promises from the Promise.all calls below
// because that then gives you a void array, which doesn't make sense with
// TypeScript's type system. Best to await them, and then have the wrapper
// mutation function return its own void promise

const favoriteAllMutation = useMutation({
mutationFn: (workspaces: readonly Workspace[]) => {
return Promise.all(
mutationFn: async (workspaces: readonly Workspace[]) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd rather just specify a : Promise<void> return type to prevent it from getting changed back, and get rid of the comment. it's hopefully clear enough with just that why returning Promise<Array<void>> is weird.

@@ -77,8 +98,8 @@ export function useBatchActions(options: UseBatchActionsProps) {
});

const unfavoriteAllMutation = useMutation({
mutationFn: (workspaces: readonly Workspace[]) => {
return Promise.all(
mutationFn: async (workspaces: readonly Workspace[]) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then same here

Suggested change
mutationFn: async (workspaces: readonly Workspace[]) => {
mutationFn: async (workspaces: readonly Workspace[]): Promise<void> => {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants