entity-conventions
Rule under the entity-editing law. Load when editing any admin store and any panel that creates or edits a record. Names the shared base of the aside and of the list store, and that the aside opens by
SKILL.md.claude/skills/entity-conventions/SKILL.md
GitHub<!-- rt-kit v0.35.0 · rules/entity-conventions.needs-admin.md · 0d25dce69297 · правится надстройкой, не здесь -->
Entity editing — how it works here
Rule under the law docs/constitution/entity-editing.md. The law says how the application behaves
when a record is created and edited; here — what that is assembled from in this tree and how it
looks. The look is spoken of by the rule: the law is silent on it by design.
What it is called here
| In the law | Here |
|---|---|
| record edit panel | an aside; opens by a route with outlet: 'ro' |
| shared base of the aside | RtRouteAsideComponent<T> — a directive without a selector |
| shared base of the list store | BaseListStoreService |
| record | entity, entityId, isCreateMode — names from the entity, not from the domain |
| save harness | runMutation in the panel, mutate in the store |
| form pristineness | pristineSignal(control) |
Where it lives
In this tree — the table in implementation.md next to it. Paths live there, not here: the rule
travels between repositories, the layout does not, and a path named in the rule lies in the first
tree that keeps its code differently.
Flow
The flow of editing a record from the panel: what opens the panel, what happens to the mutation and how leaving the panel ends.
flowchart TD
A[A create or edit panel is needed] --> B[Opens by a route in the auxiliary outlet]
B --> C[The record is read by the id from the address as the full model, not taken from the list]
C --> D[The panel inherits the shared base and hands it the mutation stream]
D --> E{How the mutation ended}
E -->|A value| F[The list is re-read, and only then the work counts as done]
E -->|A stream error| G[The panel shows the refusal and stays open]
E -->|Empty| H[That does not happen: an empty stream freezes the panel forever]
F --> I{Leaving the panel}
G --> I
I -->|There are unsaved edits| J[The guard asks — on all closing paths at once]
I -->|No| K[Leaving goes by the shared transition to a related record, not by a route of its own]
J --> K
How the law applies here
- The aside opens by a route in the
rooutlet, not by a service call. There is no programmatic opening throughRtAsideService.open()in the admin. This way the panel survives a reload, is passed by link and lands in the browser history. - Saving goes through
runMutation, and the panel hands the base the mutation stream and the keys. The busy state, clearing the previous error, the success toast and closing are held by the base. - The mutation stream must give a value or an error. An empty stream freezes the panel forever: it waits for one or the other, and the owner cannot close it while the save is running.
- A mutation ends with a re-read list, not with a sent request. A list re-read after closing would show the previous value.
- The store answers with a stream: success is a value, refusal is a stream error. No boolean answer is left in admin stores: it lost both the saved record and the reason of the refusal.
- Names come from the entity, not from the domain.
save,remove,load— notcreateBooking,loadBookings: the domain name is already in the store name. - The unsaved-edits guard is set by the panel itself, on all four closing paths. Checking one path is pointless — Esc bypasses what the button catches. The four paths are the button in the header, the button in the footer, a press outside the panel and Esc.
- Leaving the panel goes through
openRelated, not through an ownrouter.navigate. Absolute commands change only the primary branch, therooutlet stays in the address, and the router rejects the navigation silently. - The record is read by the identifier from the address as the full model, not taken from the list. The list gives the short one.
What of the law is not here
Reading a record by a separate procedure is not set up everywhere — debt Q-M-3. The store file is
called <entity>.store.ts; the name <entity>-store.service.ts takes it out of both the linter
rule and the rule gate, and the shared stores of requests and objects are edited without the entity
rules at all.
Patterns
entity-aside— assemble the edit panel: route, base,runMutation, header and footer, guard.entity-store— assemble the entity store: heir of the shared base,mutate, refusal keys.
Pitfalls
- An action with a busy state of its own does not go through the base: subscription polling keeps
its own
pollingId, because the panel is not frozen for the minute of polling. - A tail with
EMPTYglued to a mutation is handled bydefaultIfEmpty: otherwise a refusal of the glued stream turns a successful save into an endless spinner. routerLinkin the panel is no good: the directive navigates itself,preventDefaultdoes not stop it, and it bypasses the question about unsaved edits.- Angular does not accept
viewChildon a#field — the field is declaredprotected. - Field skeletons go by
resolving(), not bybusy():busycovers both saving and reading. - A panel that stays open after success resets pristineness itself.
- A branch where the ro-route constant was not mixed in differs only in that the header button opens nothing there: the build, the lint and the routes of the other branches stay intact.