Document the development rules imported from b2b.next
The new "Development rules" section covers the rules and the patterns that go with them: - the quick-reference table; - CancellationToken and its exemptions; - state initialisation; - the page loading/offline pattern; - early return with toasts; - client service patterns, ErrorViewer, autocomplete values, localization, formatting and JSON; - the steps to wire a feature to PrivaPub; - what was deliberately not adopted, and why: the tw: prefix, server-only rules, b2b.next-specific features, and mandatory parameter docs. The architecture notes now describe the component base, toasts, ErrorViewer, HttpService, the mock data and the two cultures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
e888e899d4
commit
b9040bbd80
1 file changed
+160
-13
@@ -10,10 +10,10 @@ https://decepub.thepra.dev, and its API base address is `https://privapub.thepra
|
||||
(`AppConfiguration:ApiBaseAddress` in `wwwroot/appsettings.json`).
|
||||
|
||||
**It is still a prototype and is not connected to the server:**
|
||||
- The feed comes from `Helpers/Faker.cs`. `Storage.GetMessages()` (`Services/IStorage.cs`) and `Pages/ExpandMessage.razor`
|
||||
create mock posts.
|
||||
- Every method in `Services/MessagesService.cs` is a `//TODO` stub.
|
||||
- `HttpService` (`Services/IHttpService.cs`) is written but not registered in DI.
|
||||
- The feed and threads come from `Helpers/Faker.cs`: `Storage.GetMessages()` returns the seed feed while IndexedDB is
|
||||
empty, and `Faker.Thread` builds the messages around an expanded one.
|
||||
- `MessagesService` has the real `Task<WebResult>` signatures but builds its results in memory.
|
||||
- `HttpService` is registered but nothing calls it yet.
|
||||
- The nginx vhost sends `X-Robots-Tag: noindex` while the site shows mock posts.
|
||||
|
||||
`Docs/Insomnia_APIs.json` is an old API collection for a localhost backend. Don't treat it as PrivaPub's API.
|
||||
@@ -118,23 +118,57 @@ all of them in every theme. When you add or change one:
|
||||
- The named `HttpClient` "default" uses `ApiBaseAddress` as its base address. It falls back to the host's base
|
||||
address when that setting is empty.
|
||||
- `SetDefaultCulture()` (`Extensions/ExtensionMethods.cs`) runs before the app starts. It picks the culture from the
|
||||
saved `PublicCacheData` or from the browser language, and maps any `en*` to `en-GB`.
|
||||
saved `PublicCacheData`, or on a first visit from the browser language.
|
||||
- Only English (`en-GB`) and Italian (`it-IT`) have resources; any other language gets English.
|
||||
- Settings → General changes the language, saves it, and reloads the app.
|
||||
|
||||
**`LayerComponents/CascadingState.razor`** wraps the router in `App.razor`. It is the app-wide state object, and pages
|
||||
take it as `[CascadingParameter] CascadingState`. It handles:
|
||||
**`LayerComponents/CascadingState.razor`** wraps the router in `App.razor`. It is the app-wide state object. It handles:
|
||||
- `PublicCacheData` (page settings: theme colours, dark/light/system theming, language), persisted in localStorage;
|
||||
- the theme, applied through `AppStatusService.ApplyTheme` (`wwwroot/js/theme.js`);
|
||||
- an online check every 10 seconds;
|
||||
- `ProcessError` / `ProcessWarning`, which components should call to report errors and warnings;
|
||||
- `IsOnline` / `IsOffline`, polled every 10 seconds;
|
||||
- `User`, which is `Faker.CurrentUser` until the login is wired;
|
||||
- `ProcessError` / `ProcessWarning`, which log through `ILoggingService` and show a toast;
|
||||
- `LogFromJs`, which JS calls back into through a `DotNetObjectReference`.
|
||||
|
||||
**Components and pages** inherit `LocalizableComponentBase`, which supplies `Localizer`, an `IsLoading` flag and
|
||||
`AfterRenderAsyncJobs`. `PagesBase` is an empty subclass of it, and only `Authentication.razor` uses it.
|
||||
**Components and pages** inherit `LocalizableComponentBase`, the equivalent of b2b.next's `WidmannComponentBase`. It
|
||||
supplies:
|
||||
- `CascadingState`, `Localizer`, `Navigation` and `Toast`;
|
||||
- `IsLoading`, which starts true, and `IsDefaultDisabled`;
|
||||
- `AfterRenderAsyncJobs`, run once after the next render;
|
||||
- `WhenOnline` / `StartOnlinePolling`.
|
||||
|
||||
`PagesBase` is an empty subclass of it, and only `Authentication.razor` uses it. Never redeclare the base's members in a
|
||||
component.
|
||||
|
||||
**UI feedback.**
|
||||
- `Services/ToastService.cs` is the snackbar: the SUtility extensions `AddResponseError(WebResult)` (silent on
|
||||
410/cancelled; connectivity failures show as warnings), `AddResponseError(string)`, `AddResponseSuccess` (closes
|
||||
after 5 s), `AddWarning` and `AddException`. `Components/ToastHost.razor`, placed in `MainLayout`, shows the
|
||||
messages.
|
||||
- `Components/ErrorViewer.razor` shows in-page errors: the cascading `EditContext`'s validation messages,
|
||||
`AdditionalError` and `AdditionalErrors`.
|
||||
|
||||
**HTTP (`Services/IHttpService.cs`)**, registered but not called yet, since the app still runs on mock data:
|
||||
- Auth levels: `Get` / `Post` / `Delete` need a valid token and sign out on 401; `GetAnon` / `PostAnon` add the token
|
||||
if there is one; `*TotallyAnon` send none.
|
||||
- `retryOnError` backoff: 1, 2, 4, 8, 16, 29 s, then 60 s, for at most 10 minutes.
|
||||
- Failures never throw. They come back as responses carrying an invalid `WebResult`, with an `ErrorCode` from
|
||||
`Models/FailureCodes.cs`; a cancelled request comes back as HTTP 410.
|
||||
- Read failures with `ReadWebResult` and bodies with `DefaultReadFromJsonAsync` (`Extensions/ExtensionMethods.cs`).
|
||||
- JSON goes through `SUtility.DefaultSerializer`, which consults `ClientJsonContext` first.
|
||||
- PrivaPub's `WebResult` gets `IsInvalid` from `Extensions/WebResultExtensions.cs`, a C# 14 extension property.
|
||||
|
||||
**Mock data.** `MessagesService` has the real signatures (`Task<WebResult>`, `Data` = the updated entity), but builds
|
||||
its results in memory. `Helpers/Faker.cs` holds the current user, the seed feed and the thread around an expanded
|
||||
message.
|
||||
|
||||
**Localization (`Services/CoalescingStringLocalizer.cs`).**
|
||||
- Lookup order: this repo's `Resources/AllStrings.resx`, then `FieldsNameResource`, then `ErrorsResource`.
|
||||
- A missing key renders as the key itself, so a typo fails silently.
|
||||
- Lookup order: this repo's `Resources/AllStrings.resx` (+ `.it.resx`), then `FieldsNameResource`, then
|
||||
`ErrorsResource`.
|
||||
- A missing key renders as the key itself, formatted with its arguments, so a typo fails silently.
|
||||
- The class lives in namespace `collAnon.Client.Services`, a leftover from another project.
|
||||
- `Resources/ErrorMessages.resx` is read by `[Required]` / `[StringLength]` through `ErrorMessageResourceType`. A new key
|
||||
needs its `public static string` property in `ErrorMessages.Designer.cs` too, or validation throws at runtime.
|
||||
|
||||
**Client storage** comes in two layers:
|
||||
- **localStorage** (Blazored.LocalStorage, camelCase JSON) holds `AuthData` and `PublicCacheData`, keyed by
|
||||
@@ -162,6 +196,119 @@ take it as `[CascadingParameter] CascadingState`. It handles:
|
||||
|
||||
**PWA.** Published builds use `wwwroot/service-worker.published.js`, and `service-worker.js` is the dev version.
|
||||
|
||||
## Development rules
|
||||
|
||||
These rules are adapted from the owner's b2b.next project. They are enforced by review: the build doesn't check them,
|
||||
so follow them in every change.
|
||||
|
||||
| Area | Rule |
|
||||
|---|---|
|
||||
| All | `var` everywhere; uninitialised locals are `var x = default(T);` |
|
||||
| All | Primary constructors for DI (`class Foo(IBar bar)`), not constructor + fields |
|
||||
| All | Every async method takes a `CancellationToken` as its last parameter |
|
||||
| All | Early return on error: guard, handle, `return`; never if/else for success vs failure |
|
||||
| Models / state | Lists and arrays `= []`, never null; no `= string.Empty` / `""` defaults; explicit enum defaults |
|
||||
| Components | No `@{ }` blocks in markup: locals become `@code` members |
|
||||
| Components | `[Inject]` in `@code`, never `@inject` |
|
||||
| Components | Disable controls with `IsDefaultDisabled` (plus local conditions), never bare offline state |
|
||||
| Components | Every text/number input declares `autocomplete` |
|
||||
| Services | `Task<WebResult>` for mutations, `Task<List<T>>` (`[]` on error) for lists, `Task<T>` with a default for values |
|
||||
| Localization | Every user-facing string through `Localizer`, its key in **both** `AllStrings.resx` and `AllStrings.it.resx` |
|
||||
|
||||
**CancellationToken.** It goes on every async method in services, `CascadingState` and helpers, defaulting to
|
||||
`default`; pass it on to what you call. Exempt: Blazor lifecycle overrides, `[JSInvokable]` methods, framework overrides
|
||||
(`GetAuthenticationStateAsync`), and private UI handlers bound from markup.
|
||||
|
||||
**State initialisation.** Initialise page and component state (`= new()`, `= []`) and bind it directly. No passthrough
|
||||
getters and no `?.` chains over state that can't be null:
|
||||
|
||||
```csharp
|
||||
List<Message> Messages { get; set; } = []; // DO
|
||||
@Messages.Count // DO
|
||||
@(Messages?.Count ?? 0) // DON'T
|
||||
```
|
||||
|
||||
`= ""` is allowed only as a documented exception, with the reason in a comment (e.g. a decorative image's `Alt`).
|
||||
|
||||
**Pages that load data.** Pages fetch in `LoadData` and keep lifecycle concerns in the lifecycle method:
|
||||
|
||||
```csharp
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (CascadingState.IsOffline)
|
||||
{
|
||||
WhenOnline(LoadData);
|
||||
StartOnlinePolling();
|
||||
IsLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadData();
|
||||
IsLoading = false;
|
||||
}
|
||||
```
|
||||
|
||||
- Reuse the base `IsLoading`; don't add per-section loading flags.
|
||||
- A component that binds `IsDefaultDisabled` must set `IsLoading = false` once it is ready, even if it loads nothing;
|
||||
otherwise its controls stay disabled.
|
||||
- Never wrap a service call in try/catch. Services already catch and log, and return an invalid `WebResult`. Branch on
|
||||
`result.IsInvalid` instead:
|
||||
|
||||
```csharp
|
||||
var result = await MessagesService.DeleteMessage(message);
|
||||
if (result.IsInvalid)
|
||||
{
|
||||
Toast.AddResponseError(result);
|
||||
return;
|
||||
}
|
||||
|
||||
Messages.Remove(message);
|
||||
Toast.AddResponseSuccess(Localizer["Message deleted."]);
|
||||
```
|
||||
|
||||
**Client services** follow b2b.next's two patterns (see its `StoreService`):
|
||||
- Create `var result = new WebResult();`, call `httpService` inside a try/catch, and on a non-success status
|
||||
`result = await response.Content.ReadWebResult(response.StatusCode, localizer, ct)`. Log with `ILoggingService`.
|
||||
- Return `result` (mutations) or `[]` (lists), and set `result.Data` on success.
|
||||
- POST bodies are JSON payloads.
|
||||
|
||||
**In-page errors.** `ErrorViewer` takes `AdditionalError` for one message and `AdditionalErrors` for a list. Never
|
||||
join a list into one string with `<br/>` or `\n`.
|
||||
|
||||
**Autocomplete values:**
|
||||
- `autocomplete="off"` for search, filter and free text;
|
||||
- `new-password` when setting a password;
|
||||
- `one-time-code` for OTP (DOtp sets it);
|
||||
- omit it for login credentials;
|
||||
- inside `Components/Daisy`, attributes pass through from the caller.
|
||||
|
||||
**Localization:**
|
||||
- Keys are English source strings. Grep both resx files for an existing key before adding one; match keys exactly.
|
||||
- Use `Localizer[key, args]`, never `string.Format(Localizer[key], …)`. Don't write `.Value`.
|
||||
- Enum members shown through `Localizer[value.ToString()]` are keys too: when you add a member, add its key.
|
||||
- Language names in the language picker stay in their own language.
|
||||
|
||||
**Formatting.** Date formats come from `SUtility.dateTimeFormat` / `timeFormat` / `dateTimeFormatExt`. Relative times
|
||||
and sizes come from `GetPassedTime` / `GetFileSize` (`Extensions/GenericExtensions.cs`). No inline format strings for
|
||||
display.
|
||||
|
||||
**JSON.** Register every type that crosses HTTP or goes into localStorage in `ClientJsonContext.cs`. Keep it
|
||||
camelCase: `theme.js` reads `pageSettings` straight from localStorage.
|
||||
|
||||
**Wiring a feature to PrivaPub:**
|
||||
1. Add the request/response DTOs to `PrivaPub.ClientModels` in the SocialPub repo: `View*` for responses, `*Request`
|
||||
for requests, `*Form` for forms with validation. Lists `= []`, no string defaults.
|
||||
2. Register them in `ClientJsonContext`.
|
||||
3. Add a client service method that calls `IHttpService` against the server's route, following the patterns above.
|
||||
4. Wire the page with early returns, toasts and/or an `ErrorViewer`.
|
||||
|
||||
**Not adopted from b2b.next, on purpose:**
|
||||
- **The `tw:` Tailwind prefix.** b2b.next needs it next to MudBlazor's CSS; here daisyUI is the only component CSS.
|
||||
- **Server-only rules:** no 5XX responses, the controller pattern, `IOptionsMonitor`, data access and caching.
|
||||
- **b2b.next-specific features:** Excel import/export, password policy, legacy porting rules, the `{culture}` route
|
||||
placeholder (PrivaPub's routes are absolute `/clientapi/...`), and the release/API-status worker.
|
||||
- **Mandatory `/// <summary>` on every `[Parameter]`.** Document only what isn't obvious from the name.
|
||||
|
||||
## Deployment
|
||||
|
||||
Both workflows check the repo out with a plain `git fetch` from this Gitea, not `actions/checkout`, because the runner
|
||||
|
||||
Reference in new issue
Block a user