Skip to content

Commit a06a4d9

Browse files
Bill Leoutsakoscursoragent
authored andcommitted
docs(e2e): add settings maintenance guidance
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e60658d commit a06a4d9

7 files changed

Lines changed: 253 additions & 43 deletions

File tree

.claude/rules/sim-settings-e2e.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
paths:
3+
- "apps/sim/**"
4+
- "packages/platform-authz/src/workspace.ts"
5+
- "packages/platform-authz/src/predicates.ts"
6+
---
7+
8+
# Settings E2E Maintenance
9+
10+
When a change can affect observable settings routes, copy, visibility,
11+
authorization, entitlements, mutations, or persisted workflows, read
12+
`apps/sim/e2e/MAINTENANCE.md` before finishing and assess every applicable
13+
literal Playwright contract.
14+
15+
Keep acceptance expectations independent of production navigation and
16+
authorization implementations. Behavior-preserving refactors need focused
17+
verification, not artificial contract edits.

.claude/rules/sim-settings-pages.md

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
---
22
paths:
3+
- "apps/sim/app/account/settings/**"
4+
- "apps/sim/app/organization/*/settings/**"
35
- "apps/sim/app/workspace/*/settings/**"
6+
- "apps/sim/components/settings/**"
47
- "apps/sim/ee/**/components/**"
58
---
69

710
# Settings Pages
811

9-
The Next.js `settings/[section]/layout.tsx` owns all settings page chrome via
10-
`SettingsHeaderShell` — a fixed header bar (a left back chip + right-aligned
11-
action chips), a scroll region, and a centered `max-w-[48rem]` content column led
12-
by a **title + description from navigation metadata**. The chrome stays mounted
13-
across section navigation (it never re-renders or re-lays-out). Each section
14-
renders through the **`SettingsPanel`** registrar
15-
(`@/app/workspace/[workspaceId]/settings/components/settings-panel`), which feeds
16-
the shell its header data and renders only the section body. Sections supply
17-
**data**, never chrome.
12+
All three settings planes share `SettingsHeaderShell` and the canonical
13+
`SettingsPanel` implementation in `@/components/settings/settings-panel`, but
14+
their chrome is mounted at different boundaries:
15+
16+
- Account and organization `settings/layout.tsx` mount
17+
`StandaloneSettingsShell`, which owns their sidebar, header shell, section
18+
provider, and unload guard.
19+
- Workspace `settings/[section]/layout.tsx` mounts the persistent header shell;
20+
the outer workspace `settings/layout.tsx` owns its unload guard.
21+
22+
The active shell owns the fixed header bar (a left back chip + right-aligned
23+
action chips), scroll region, and centered `max-w-[48rem]` content column led by
24+
a **title + description from the shared settings registry**. Each section
25+
renders only its body through `SettingsPanel`; sections supply **data**, never
26+
chrome.
1827

1928
Do NOT hand-roll any of these in a settings page — they are owned by the layout
2029
shell (fed through `SettingsPanel`):
@@ -29,6 +38,8 @@ shell (fed through `SettingsPanel`):
2938
## Canonical page shape
3039

3140
```tsx
41+
// Established section-component import; this compatibility barrel re-exports
42+
// the canonical @/components/settings/settings-panel implementation.
3243
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
3344

3445
return (
@@ -80,21 +91,29 @@ return (
8091
- `scrollContainerRef?: React.Ref<HTMLDivElement>` — forwards a ref to the scroll
8192
region (e.g. programmatic scroll-to-bottom).
8293

83-
## Title + description live in navigation metadata
94+
## Title + description live in the shared settings registry
8495

85-
`apps/sim/app/workspace/[workspaceId]/settings/navigation.ts` is the single source
86-
of truth. Every `NavigationItem` carries a one-line `description`; `SettingsPanel`
87-
resolves both via `getSettingsSectionMeta(section)` and the
88-
`SettingsSectionProvider` the settings shell wraps around the active section.
96+
`apps/sim/components/settings/navigation.ts` is the single source of truth.
97+
Each `SettingsSectionRegistryEntry` owns a `unified` projection with the default
98+
description and gating metadata plus optional account, organization, and
99+
workspace plane projections. Account and organization shells provide `plane`
100+
and `section`, so the shared `SettingsPanel` resolves metadata with
101+
`getSettingsSectionMeta(plane, section)`. The workspace adapter at
102+
`app/workspace/[workspaceId]/settings/navigation.ts` resolves the mandatory
103+
unified projection with its one-argument `getSettingsSectionMeta(section)` and
104+
the workspace renderer passes that metadata to `SettingsSectionProvider`.
89105

90106
Adding a new settings page:
91107

92-
1. Add the `SettingsSection` id + a `NavigationItem` (with `label` **and**
93-
`description`) in `navigation.ts`. Keep descriptions verb-first, one line,
94-
~40–55 chars, in the product voice (see `.claude/rules/constitution.md`).
95-
2. Render the component inside the shell's `effectiveSection` switch in
96-
`settings/[section]/settings.tsx`.
97-
3. Build the component body inside `<SettingsPanel>` — no shell, no title block.
108+
Follow the procedure in `.claude/skills/add-settings-page/SKILL.md`. The
109+
non-negotiable architecture invariants are:
110+
111+
- Every registry entry has a mandatory unified projection, workspace renderer,
112+
and server-side direct-route outcome.
113+
- Every optional account, organization, or workspace plane projection has its
114+
corresponding renderer and access/feature gate.
115+
- The component body uses `SettingsPanel`; associated unit tests and every
116+
applicable literal browser contract change in the same PR.
98117

99118
## Text-scale tokens (no literal pixel sizes)
100119

@@ -187,8 +206,9 @@ changes" modal:
187206
(from `@/app/workspace/[workspaceId]/components/credential-detail`). The
188207
in-view header **Discard** chip (via `SaveDiscardActions onDiscard`) is a
189208
*reset to original* — distinct from the back-confirm's discard, which leaves.
190-
- **`useSettingsBeforeUnload`** is mounted **once** in the settings shell
191-
(`settings/[section]/settings.tsx`) — never add a per-page `beforeunload`.
209+
- **`useSettingsBeforeUnload`** is mounted once per active shell boundary:
210+
workspace `settings/layout.tsx` and `StandaloneSettingsShell` for account or
211+
organization. Never add a per-page `beforeunload`.
192212
- **Dirty *computation* stays local** (shapes differ: field-compare vs
193213
normalize+stringify) — only how dirty is *consumed* is shared. Derive it (a
194214
`const`/`useMemo`), never store it in `useState`.
@@ -218,9 +238,9 @@ exception — it lives outside `[section]` and keeps its own `CredentialDetailLa
218238
A settings page is design-system-clean when:
219239

220240
- [ ] Its main return is a `<SettingsPanel>` (or `<>…<SettingsPanel>…</>` with modal siblings) — no hand-rolled shell/header/scroll/column.
221-
- [ ] It renders **no** hand-rolled `<h1>`/description title block — the title comes from nav metadata.
241+
- [ ] It renders **no** hand-rolled `<h1>`/description title block — the title comes from the shared settings registry.
222242
- [ ] Header chips are in `actions`; a standalone search is in the `search` prop.
223-
- [ ] Its `NavigationItem` has an accurate, consistent-length `description`.
243+
- [ ] Its `SettingsSectionRegistryEntry` has an accurate, consistent-length `unified.description` and only the plane projections it supports.
224244
- [ ] Detail sub-views and entitlement/loading gates keep their own chrome (intentional).
225245
- [ ] If it has editable state: Save/Discard go through `SaveDiscardActions`, dirty is wired via `useSettingsUnsavedGuard` (called before any early-return gate), and there is **no** hand-rolled Save button / `beforeunload` / "Unsaved changes" modal.
226246
- [ ] No business logic, handlers, or conditional rendering changed by the migration.

.claude/skills/add-settings-page/SKILL.md

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,79 @@ description: Add a new Sim settings page, or audit existing settings pages for d
55

66
# Settings Page (add / audit)
77

8-
Sim settings pages all render through the shared **`SettingsPanel`** primitive,
9-
which owns the page chrome and renders a nav-driven title + description. The full
10-
convention lives in `.claude/rules/sim-settings-pages.md` — read it first; this
11-
skill is the procedure.
8+
Sim settings pages render their bodies through the shared **`SettingsPanel`**
9+
registrar, which publishes header metadata and actions to the active
10+
`SettingsHeaderShell`. The shell owns and renders the page chrome, including the
11+
registry-driven title and description. The full convention lives in
12+
`.claude/rules/sim-settings-pages.md` — read it first; this skill is the
13+
procedure.
1214

1315
Key paths:
14-
- Layout primitive: `apps/sim/app/workspace/[workspaceId]/settings/components/settings-panel/settings-panel.tsx`
15-
- Nav metadata (titles + descriptions): `apps/sim/app/workspace/[workspaceId]/settings/navigation.ts`
16-
- Section switch + provider: `apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx`
16+
- Panel registrar implementation: `apps/sim/components/settings/settings-panel.tsx`
17+
- Established section-component import: `@/app/workspace/[workspaceId]/settings/components/settings-panel` (compatibility barrel)
18+
- Section types + registry: `apps/sim/components/settings/navigation.ts`
19+
- Account renderer: `apps/sim/components/settings/account-settings-renderer.tsx`
20+
- Account route gate: `apps/sim/app/account/settings/[section]/page.tsx`
21+
- Organization renderer: `apps/sim/components/settings/organization-settings-renderer.tsx`
22+
- Organization route gate: `apps/sim/app/organization/[organizationId]/settings/[section]/page.tsx`
23+
- Workspace metadata adapter: `apps/sim/app/workspace/[workspaceId]/settings/navigation.ts`
24+
- Workspace renderer + provider: `apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx`
25+
- Workspace route gate: `apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx`
1726
- Pages: `apps/sim/app/workspace/[workspaceId]/settings/components/<name>/<name>.tsx` and EE pages under `apps/sim/ee/<feature>/components/`
27+
- Browser acceptance maintenance: `apps/sim/e2e/MAINTENANCE.md`
1828

1929
## Mode A — Add a new settings page
2030

21-
1. **Navigation.** In `navigation.ts`: add the id to the `SettingsSection` union,
22-
then a `NavigationItem` with `label` AND a one-line `description` (verb-first,
23-
~40–55 chars, product voice per `.claude/rules/constitution.md`). Place it in
24-
the right `section` group and set any gating flags (`requiresHosted`,
25-
`requiresEnterprise`, etc.).
26-
2. **Wire the switch.** Add the component to the `effectiveSection` render switch
27-
in `settings/[section]/settings.tsx` (lazy `dynamic(...)` like its siblings).
28-
3. **Build the body inside `SettingsPanel`.** Never hand-roll the shell, header
31+
1. **Registry.** In `apps/sim/components/settings/navigation.ts`, add the id to
32+
`UnifiedSettingsSection` and add a `SettingsSectionRegistryEntry` to
33+
`SETTINGS_SECTION_REGISTRY`. Its mandatory `unified` projection owns the
34+
default description and workspace gating flags (`requiresHosted`,
35+
`requiresEnterprise`, etc.). For each plane-specific projection the page
36+
needs, also add the id to `AccountSettingsSection`,
37+
`OrganizationSettingsSection`, or `WorkspaceSettingsSection` and add that
38+
optional projection. Use plane-specific copy only when its scope genuinely
39+
differs. Keep descriptions verb-first, one line, ~40–55 chars, in the product
40+
voice (see `.claude/rules/constitution.md`).
41+
2. **Always wire the unified workspace surface.** Every registry entry
42+
participates in unified workspace navigation before its gates are applied,
43+
whether or not it has `planes.workspace`. Render the `unified.id` in the
44+
workspace `settings/[section]/settings.tsx` switch so an allowed route cannot
45+
resolve to a blank page.
46+
3. **Wire optional standalone planes.** If the entry declares `planes.account`
47+
or `planes.organization`, add the component to
48+
`account-settings-renderer.tsx` or `organization-settings-renderer.tsx`.
49+
Never declare a standalone projection whose renderer cannot render it.
50+
4. **Preserve every route gate.** In the workspace
51+
`settings/[section]/page.tsx`, classify the unified section in
52+
`WORKSPACE_SECTION_MAP` or `ORGANIZATION_SECTION_MAP` when it belongs to
53+
either access plane; otherwise add an explicit direct gate when needed or
54+
verify that host-context membership is the intended boundary. Enforce the
55+
section's permission, deployment, plan, and entitlement outcome. For
56+
standalone account or organization projections, also update their
57+
`[section]/page.tsx` gate and the shared organization access/feature helpers
58+
when applicable. Sidebar gating never replaces server authorization.
59+
5. **Build the body inside `SettingsPanel`.** Never hand-roll the shell, header
2960
bar, scroll region, content column, or title block. Put header buttons in
3061
`actions`, a standalone search in `search={{ value, onChange, placeholder }}`,
3162
and the page content as `children`. Modals go beside the panel inside a `<>`.
32-
4. **If the page has editable state**, wire the shared save/discard stack — put
63+
6. **If the page has editable state**, wire the shared save/discard stack — put
3364
`SaveDiscardActions` (dirty-gated Discard+Save chips) in `actions`, and call
3465
`useSettingsUnsavedGuard({ isDirty })` **before any early-return gate**.
3566
Detail sub-views additionally route the back chip through
3667
`guard.guardBack(closeFn)` and render the shared `UnsavedChangesModal`. Never
3768
hand-roll a Save button, a `beforeunload`, or an "Unsaved changes" modal —
3869
they're centralized. See the "Save / Discard + unsaved-changes guard" section
3970
in `.claude/rules/sim-settings-pages.md`.
40-
5. **Verify:** `cd apps/sim && bunx tsc --noEmit`; `bunx biome check --write <file>`.
71+
7. **Update tests and browser contracts.** Update the shared navigation,
72+
workspace route/navigation, and access unit tests affected by the new
73+
projection or gate. Then follow `apps/sim/e2e/MAINTENANCE.md`: intended
74+
observable changes require paired literal Playwright contracts, while
75+
behavior-preserving refactors require focused verification without
76+
expectation churn.
77+
8. **Verify:** From `apps/sim`, run `bunx tsc --noEmit` and
78+
`bunx biome check --write <file>` on changed source files. Then run the
79+
affected unit tests and the focused orchestrated E2E project from
80+
`e2e/README.md`.
4181

4282
## Mode B — Audit existing settings pages
4383

@@ -54,8 +94,10 @@ For each page component, confirm the checklist in `.claude/rules/sim-settings-pa
5494
`.claude/rules/sim-settings-pages.md` for the token map and the row
5595
title/subtitle pairing convention):
5696
`git grep -n "text-\[1[0-8]px\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
57-
4. Confirm each page imports `SettingsPanel` and that its `NavigationItem` has an
58-
accurate `description` of consistent length with its peers.
97+
4. Confirm each page imports `SettingsPanel` and that its
98+
`SettingsSectionRegistryEntry` has an accurate `unified.description` of
99+
consistent length with its peers, plus only the plane projections it
100+
supports.
59101
- Editable pages: confirm Save/Discard go through `SaveDiscardActions` and
60102
dirty is wired via `useSettingsUnsavedGuard` (called before early-return
61103
gates) — flag any hand-rolled Save button, `beforeunload`, or unsaved modal.

.cursor/rules/sim-settings-e2e.mdc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
description: Keep settings browser acceptance contracts aligned with observable behavior
3+
globs: ["apps/sim/**", "packages/platform-authz/src/workspace.ts", "packages/platform-authz/src/predicates.ts"]
4+
---
5+
6+
# Settings E2E Maintenance
7+
8+
When a change can affect observable settings routes, copy, visibility,
9+
authorization, entitlements, mutations, or persisted workflows, read
10+
`apps/sim/e2e/MAINTENANCE.md` before finishing and assess every applicable
11+
literal Playwright contract.
12+
13+
Keep acceptance expectations independent of production navigation and
14+
authorization implementations. Behavior-preserving refactors need focused
15+
verification, not artificial contract edits.

apps/sim/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export function useEntityList(workspaceId?: string) {
221221
- Use `vi.hoisted()` + `vi.mock()` + static imports; do not use `vi.resetModules()` + `vi.doMock()` + dynamic imports except for true module-scope singletons.
222222
- Do not use `vi.importActual()`.
223223
- Prefer mocks and factories from `@sim/testing`.
224+
- Before finishing a change that can affect observable settings routes, copy, visibility, authorization, entitlements, mutations, or persisted workflows, follow [`e2e/MAINTENANCE.md`](e2e/MAINTENANCE.md) and assess every applicable literal Playwright contract. Behavior-preserving refactors need focused verification, not artificial expectation edits.
224225

225226
## Utils Rules
226227

0 commit comments

Comments
 (0)