CLAUDE.md
core-web/libs/portlets/edit-ema/portlet/src/lib/store/CLAUDE.mdCLAUDE.md
Quality
65/100
Scores the file, not the repository.Length
2,155 words
44 headings · 7 code blocksRepository
949
— · pushed 0 days agoLast changed
3 days ago
First indexed 3 days ago.1# UVEStore — Architecture & Scaling Rules23## Structure45The store is built with `@ngrx/signals` and split into feature slices using `signalStoreFeature`.67```8store/9├── dot-uve.store.ts # Root store — composes all features10├── models.ts # UVEState and top-level types11└── features/12 ├── withPageContext.ts # Shared computed hub (loaded first)13 ├── editor/14 │ ├── withEditor.ts # Editor UI state and methods15 │ ├── withLock.ts # Lock management16 │ ├── save/withSave.ts # Save operations17 │ └── toolbar/withUVEToolbar.ts18 ├── flags/withFlags.ts # Feature flag signals19 ├── layout/withLayout.ts # Layout tab computeds20 ├── track/withTrack.ts # Analytics tracking21 ├── workflow/withWorkflow.ts22 ├── timeMachine/withTimeMachine.ts23 ├── client/withClient.ts24 └── load/withLoad.ts25```2627## State Design Rules2829### Flat State Structure (Non-Negotiable)3031NgRx Signal Store wraps each root-level property in its own Signal. Nested objects break fine-grained reactivity.3233```typescript34// ✅ CORRECT: Flat with prefixes35interface UVEState {36 uveStatus: UVE_STATUS;37 pageParams: DotPageAssetParams | null;38 editorDragItem: EmaDragItem | null;39 viewZoomLevel: number;40}4142// ❌ WRONG: Nested objects break reactivity43interface UVEState {44 uve: { status: UVE_STATUS };45 editor: { panels: { palette: boolean } };46}47```4849### Domain Prefixes (Required)5051All state properties must use a domain prefix matching their feature file:5253| Prefix | Domain |54|--------|--------|55| `uve*` | Global editor system (status, user, enterprise) |56| `page*` | Page content and metadata |57| `workflow*` | Workflow and lock state |58| `editor*` | Editor UI state |59| `view*` | View modes and preview |6061Cross-domain state (a property with the wrong prefix for its feature) is not allowed.6263### Property Naming Conventions6465| Pattern | Example | Usage |66|---------|---------|-------|67| `{domain}{Property}` | `editorDragItem` | State property |68| `{domain}Can{Action}` | `editorCanEditContent()` | Boolean computed |69| `${domain}{Computed}` | `$showContentletControls()` | UI-specific computed |70| `{domain}{Action}` | `viewZoomIn()` | Action method |7172---7374## Key Conventions7576**Computed signals are prefixed with `$`**77```ts78$isEditMode: computed(() => pageParams()?.mode === UVE_MODE.EDIT)79```8081**Each feature exports a `with*()` function** using `signalStoreFeature` with explicit type constraints:82```ts83export function withLayout() {84 return signalStoreFeature(85 { state: type<UVEState>() },86 withComputed(...),87 withMethods(...)88 );89}90```9192**`withPageContext` is the shared computed hub.** It is composed first in the root store, before any other feature. Features that need common computed values (mode, page lock, variant, etc.) declare `props: type<PageContextComputed>()` in their type constraints.9394**State mutations go through `patchState` only.** Never mutate state directly inside `withComputed` or `withMethods`.9596## Rules for Scaling9798### 1. One domain per feature99Each `with*` function owns one domain (editor UI, layout, workflow, etc.). Do not add state or methods to an existing feature if they belong to a different concern — create a new feature instead.100101### 2. Shared computeds belong in `withPageContext`102If a computed signal is needed by more than one feature, add it to `withPageContext` and expose it via `PageContextComputed`. Features consume it through `props: type<PageContextComputed>()`. Do not duplicate computed logic across features.103104### 3. Feature-local types live next to the feature105Each feature keeps its own `models.ts` for state shape, prop types, and enums. Do not import feature-local types into `store/models.ts` — that file is for `UVEState` only.106107### 4. Declare type constraints explicitly108Always declare `{ state: type<UVEState>(), props: type<PageContextComputed>() }` at the top of `signalStoreFeature`. This enforces that the feature is only composed in the right context and makes dependencies explicit and compiler-checked.109110### 5. Never add logic to the root store111`dot-uve.store.ts` is a composition file only. It holds `initialState`, wires features together, and adds top-level computeds that genuinely span multiple features (e.g. `$shellProps`). Business logic goes inside the feature, not the root.112113### 6. Sub-features for nested concerns114Features can nest sub-features (e.g. `withEditor` composes `withUVEToolbar`). Use this when a feature grows large enough that its computeds or methods split into a clearly separate sub-concern. Keep nesting shallow (max 2 levels).115116### 7. `untracked` for side-effect reads117When a `computed` or method needs to read a signal without creating a reactive dependency, use `untracked()`. Only do this when you have a deliberate reason to break the dependency chain — document why inline.118119### 8. `protectedState: false` is temporary120The root store has `{ protectedState: false }` to allow state access in unit tests. This must be removed once the tests are updated to use proper store testing patterns. Do not add new tests that rely on direct state mutation.121122---123124## Patterns in Use125126### Accessing State127128Always use computed signals — they are the public API. Never reach into internal raw state.129130```typescript131// ✅ Use computeds132const page = store.pageData();133const canEdit = store.editorCanEditContent();134135// ❌ Do not access internal state signals directly136const page = store.pageAssetResponse()?.pageAsset.page;137```138139### `rxMethod` for async operations140All async flows (`loadPageAsset`, `reloadCurrentPage`, `savePage`, `getWorkflowActions`, `trackUVEModeChange`) are wrapped in `rxMethod`. It binds an RxJS pipeline to the store lifecycle and handles cancellation automatically. Use it for any method that triggers an HTTP call or a side-effect stream.141142```ts143loadPageAsset: rxMethod<Partial<DotPageAssetParams>>(144 pipe(145 tap(() => patchState(store, { status: UVE_STATUS.LOADING })),146 switchMap((params) => dotPageApiService.get(params).pipe(...))147 )148)149```150151### Optimistic update + rollback via `withTimeMachine`152`withClient` saves a GraphQL response snapshot before mutating it (`setGraphqlResponseOptimistic`). On save failure, `rollbackGraphqlResponse` restores the previous snapshot using `withTimeMachine`. This is the only place `withTimeMachine` is used — it was designed as a generic undo/redo primitive.153154### Implicit feature composition chain155`withLoad` internally composes `withClient` and `withWorkflow`. `withSave` internally composes `withLoad`. The root store only calls `withSave`, but it silently gets the full chain. This is intentional but invisible from the root store.156157```158withSave → withLoad → withClient159 → withWorkflow160```161162### Split `withMethods` blocks for DI ordering163`withLoad` uses two `withMethods` calls: the first adds `updatePageParams` (no DI needed), the second injects all services. This is a workaround for ngrx/signals requiring methods to be available before they are used inside other methods. Do not collapse them into one block.164165### Feature flag signals as a typed map166`withFlags` fetches all feature flags from `DotPropertiesService` once and exposes them as a `flags()` signal: a strongly-typed record `UVEFlags`. Consuming features read individual flags via `flags().FEATURE_FLAG_*`. Never inject `DotPropertiesService` inside other features to read flags — always go through `flags()`.167168### Debounced analytics169`withTrack` wraps tracking calls in `DEBOUNCE_FOR_TRACKING` (5000ms) to avoid noise on rapid state changes. Always apply this wrapper to new tracking methods — raw analytics events on every signal change will flood the analytics backend.170171### `forkJoin` for parallel page bootstrap172`loadPageAsset` uses two nested `forkJoin` blocks: the first fetches `pageAsset + isEnterprise + currentUser` in parallel, the second (inside the first's `switchMap`) fetches `experiment + languages` in parallel. Each has its own `catchError`. Do not flatten these into sequential calls.173174### `tapResponse` for RxJS next/error handling175`withWorkflow` uses `tapResponse` instead of `tap` + `catchError` when both the success and error paths need clean inline handling inside a pipeline. Use `tapResponse` when you want to handle both cases close together without breaking the observable chain.176177### `new String('')` for forced iframe refresh178In `withEditor.$iframeURL`, TRADITIONAL pages return `new String('')` (a String object, not a primitive) instead of a plain `''`. This forces Angular to treat it as a new reference on every recompute, triggering an iframe reload. HEADLESS pages build a full URL with `clientHost`. This distinction is intentional — do not normalize to a plain string.179180### Device and SEO mode are mutually exclusive181`withView` enforces that device preview and SEO social-media preview cannot be active at the same time. `viewSetDevice()` clears `viewSocialMedia`; `viewSetSEO()` clears `viewDevice`. When adding new preview modes, maintain this mutual exclusion via the same clear-on-set pattern.182183### Type assertion for cross-feature method access184Features that depend on methods from sibling features (not yet in scope at composition time) use a typed cast: `store as StoreWithDeps<typeof store>`. This is only acceptable for documented circular or out-of-order dependencies. Always define a dedicated `StoreWith*Deps` interface — do not use `as any` except for the documented `withWorkflow` ↔ `withPageApi` circular case.185186---187188## Things to Improve189190### 1. `protectedState: false` (tracked: test infrastructure)191Root store disables state protection so tests can directly mutate state. Every new test written against this is technical debt. Fix: migrate tests to use `patchState` or store method calls, then remove this flag.192193### 2. Vanity URL redirect inside `withLoad` (tracked: existing TODO in code)194The `router.navigate()` call in `loadPageAsset` is inside the store. The comment already says it should move to a Shell component effect. Store features should not navigate — they should expose an event or computed that the component reacts to.195196### 3. `isEnterprise` and `currentUser` re-fetched on every page load197These are session-level facts fetched inside `loadPageAsset`'s `forkJoin`. Every page navigation re-fetches them. The comment in the code references issue #30760 and suggests moving this to an `onInit` lifecycle hook. Until then, every soft navigation pays an unnecessary HTTP round-trip.198199### 4. `$shellProps` computed is too large for the root store200The root store's `$shellProps` contains the full navigation bar items array with all conditional business logic (enterprise check, layout disabled, rules visibility, experiments). This belongs in a dedicated `withShell` feature, not in the composition root.201202### 5. `saveStyleEditor` does not use `rxMethod`203Unlike `savePage`, `saveStyleEditor` returns a raw `Observable` that the caller must subscribe to. This breaks the uniform pattern and shifts error handling responsibility to the component. It should be converted to `rxMethod`.204205### 6. Dead reactive dependency in `$editorProps`206Inside `withEditor`, `$editorProps` calls `store.pageAPIResponse()` at the top of the computed body without using the return value — just to create a reactive dependency. The inline comment says "need more testing before removing this dependency." This is a smell: computed dependencies should be explicit and intentional.207208### 7. `isTraditionalPage` detection is an implicit convention209`isTraditionalPage` is set as `!pageParams.clientHost` — a boolean derived from the absence of a URL parameter. There is no explicit type guard or enum. If the convention ever changes, this silently breaks every feature that branches on it.210211### 8. `console.error` / `console.warn` scattered across features212Error handling across `withLoad`, `withSave`, `withWorkflow`, and `withClient` logs directly to the console with no consistent strategy. A store-level error handler or error bus would centralize this.213214### 9. Double `withFlags` composition215`withFlags(UVE_FEATURE_FLAGS)` is called inside `withPageContext` **and** again in the root store (`dot-uve.store.ts`). The root store call is redundant and confusing — `withPageContext` already includes it.216217### 10. `reloadPageAfterLockChange()` uses raw `.subscribe()`218Inside `withWorkflow`, the method that re-fetches the page after a lock/unlock uses a manual `.subscribe()` call instead of `rxMethod`. There is no automatic cancellation — if the user navigates away mid-lock, the subscription keeps running. Fix: convert to `rxMethod` or add `takeUntilDestroyed()`.219220### 11. Lock/unlock concern mixed into `withWorkflow`221`withWorkflow` manages two unrelated things: workflow action fetching (`DotWorkflowsActionsService`) and page lock toggling (`DotContentletLockerService`). The feature's own comments flag this. Lock state and behavior belong in a dedicated feature that focuses only on lock ownership and transitions.222223### 12. `withTimeMachine` is unused224A generic undo/redo feature exists at `features/timeMachine/withTimeMachine.ts` and is never composed into the store. `withPage` uses `withHistory` instead, which has nearly identical logic. Either delete `withTimeMachine` or replace `withHistory` with it. Having two implementations of the same primitive is maintenance debt.225226### 13. Commented-out permission logic in `withEditor`227A block of style-editor permission logic in `withEditor` is commented out with no explanation of whether it is WIP or abandoned. It adds noise and creates ambiguity about the intended permission model. Delete it if it is dead code, or activate and document it if it is needed.228229---230231## Next Move232233These are ordered by impact-to-effort ratio:2342351. **Remove the redundant `withFlags` in the root store.** One-line deletion, no behavior change, reduces confusion.2362372. **Convert `saveStyleEditor` to `rxMethod`.** Straightforward refactor. Removes component-side subscription responsibility and aligns with the `savePage` pattern.2382393. **Extract `$shellProps` nav items into a `withShell` feature.** The root store should be a thin composition file. This is the largest source of business logic leaking into the root.2402414. **Move the vanity URL redirect to the Shell component.** Replace the `router.navigate` inside `withLoad` with a dedicated computed or event that the component listens to. This is already documented as a TODO — execute it.2422435. **Fix `protectedState: false`.** Requires updating test setup. High effort but it's the foundation for reliable signal store testing going forward.2442456. **Hoist `isEnterprise` and `currentUser` to `onInit`.** Requires the ngrx/signals lifecycle hook support (issue #30760). When that ships, move these out of `loadPageAsset` to eliminate the per-navigation round-trips.2462477. **Extract lock/unlock to a dedicated `withLock` feature.** The code and its own comments already say this should not live inside `withWorkflow`. Separate the two services (`DotWorkflowsActionsService` vs `DotContentletLockerService`) into their own features for single-responsibility compliance.2482498. **Convert `reloadPageAfterLockChange()` to `rxMethod`.** The raw `.subscribe()` inside `withWorkflow` has no cancellation. It should use `rxMethod` like every other async operation in the store.2502519. **Delete `withTimeMachine` or replace `withHistory` with it.** Two nearly identical undo/redo implementations exist. Pick one and remove the other. `withTimeMachine` has better JSDoc but is unused; `withHistory` is actively composed. Consolidate before the divergence grows.252
Also in dotCMS/core
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| dotCMS/core.github/copilot-instructions.md · 949 | Copilot instructions | setupbuildtestlint-format+11 | 84/100 | today | |
| dotCMS/corecore-web/apps/dotcms-ui-e2e/AGENTS.md · 949 | AGENTS.md | setupstylearchtesting-strategy+2 | 78/100 | 3 days ago | |
| dotCMS/corecore-web/apps/mcp-server/CLAUDE.md · 949 | CLAUDE.md | setupbuildtestlint-format+5 | 89/100 | 3 days ago | |
| dotCMS/core.cursor/rules/doc-updates.mdc · 949 | Cursor rules | docs | 30/100 | 3 days ago | |
| dotCMS/core.cursor/rules/dotcms-guide.mdc · 949 | Cursor rules | archdo-notdocs | 69/100 | 3 days ago | |
| dotCMS/core.cursor/rules/e2e-rules.mdc · 949 | Cursor rules | setupteststylearch+5 | 89/100 | 3 days ago | |
| dotCMS/core.cursor/rules/frontend-context.mdc · 949 | Cursor rules | teststyledocs | 78/100 | 3 days ago | |
| dotCMS/core.cursor/rules/java-context.mdc · 949 | Cursor rules | buildstyle | 44/100 | 3 days ago | |
| dotCMS/core.cursor/rules/test-context.mdc · 949 | Cursor rules | testtesting-strategy | 54/100 | 3 days ago | |
| dotCMS/core.github/instructions/frontend.instructions.md · 949 | Copilot instructions | testlint-formatstylearch+3 | 69/100 | 3 days ago | |
| dotCMS/coreCLAUDE.md · 949 | CLAUDE.md | setupbuildteststyle+7 | 99/100 | today | |
| dotCMS/corecore-web/AGENTS.md · 949 | AGENTS.md | style | 63/100 | 3 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 3 days ago | |
| dotCMS/corecore-web/apps/dotcms-ui/AGENTS.md · 949 | AGENTS.md | buildteststyledependencies+3 | 94/100 | 3 days ago | |
| dotCMS/corecore-web/libs/block-editor/CLAUDE.md · 949 | CLAUDE.md | archdo-not | 69/100 | 3 days ago | |
| dotCMS/corecore-web/libs/new-block-editor/CLAUDE.md · 949 | CLAUDE.md | lint-formatstyledo-notagent-behaviour | 61/100 | 3 days ago | |
| dotCMS/corecore-web/libs/portlets/CLAUDE.md · 949 | CLAUDE.md | setupteststyleui+1 | 77/100 | 3 days ago | |
| dotCMS/corecore-web/libs/sdk/client/CLAUDE.md · 949 | CLAUDE.md | setupbuildtestlint-format+9 | 97/100 | 3 days ago | |
| dotCMS/corecore-web/libs/sdk/react/CLAUDE.md · 949 | CLAUDE.md | setupbuildtestlint-format+9 | 97/100 | 3 days ago | |
| dotCMS/coredotCMS/src/main/java/com/dotcms/rest/CLAUDE.md · 949 | CLAUDE.md | typesdatabaseapido-not+1 | 57/100 | 3 days ago |
Diff against .github/copilot-instructions.md Diff against core-web/apps/dotcms-ui-e2e/AGENTS.md Diff against core-web/apps/mcp-server/CLAUDE.md Diff against .cursor/rules/doc-updates.mdc Diff against .cursor/rules/dotcms-guide.mdc Diff against .cursor/rules/e2e-rules.mdc Diff against .cursor/rules/frontend-context.mdc Diff against .cursor/rules/java-context.mdc Diff against .cursor/rules/test-context.mdc Diff against .github/instructions/frontend.instructions.md Diff against CLAUDE.md Diff against core-web/AGENTS.md Diff against core-web/CLAUDE.md Diff against core-web/apps/dotcms-ui/AGENTS.md Diff against core-web/libs/block-editor/CLAUDE.md Diff against core-web/libs/new-block-editor/CLAUDE.md Diff against core-web/libs/portlets/CLAUDE.md Diff against core-web/libs/sdk/client/CLAUDE.md Diff against core-web/libs/sdk/react/CLAUDE.md Diff against dotCMS/src/main/java/com/dotcms/rest/CLAUDE.md
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| Adit-Jain-srm/NightmareNetCLAUDE.md · 45 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 3 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.4k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 3 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 3 days ago | |
| microsoft/playwrightCLAUDE.md · 94k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 3 days ago | |
| filamentphp/filamentCLAUDE.md · 32k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 3 days ago | |
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 3 days ago | |
| dotCMS/coreCLAUDE.md · 949 | CLAUDE.md | setupbuildteststyle+7 | 99/100 | today | |
| carrot-foundation/middle-earthCLAUDE.md · 0 | CLAUDE.md | setupbuildtestlint-format+6 | 97/100 | 3 days ago |
