spiderly 19.13.0 → 19.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/docs/angular-customization/index.md +17 -0
- package/agent/docs/frontend-localization/index.md +29 -16
- package/agent/skills/deployment/SKILL.md +11 -6
- package/fesm2022/spiderly.mjs +519 -497
- package/fesm2022/spiderly.mjs.map +1 -1
- package/lib/components/spiderly-data-table/spiderly-data-table.component.d.ts +31 -1
- package/package.json +1 -1
- package/public-api.d.ts +0 -1
- package/lib/services/spiderly-transloco-loader.d.ts +0 -12
|
@@ -274,6 +274,23 @@ The table renders the store as a **chip bar** above the header: one chip per *ap
|
|
|
274
274
|
this.views = this.views.map((view) => ({ ...view, count: counts[view.id] }));
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
+
**A count can honor each view's saved filters**: the table's `viewFilterPayload(viewId)` returns the `filters` payload that view's own list request carries, so a count query that applies it counts the rows that tab lists (on screen for the active view, after a click for the others), including bar filters the user saved on that view. The active view answers with the live filters; any other view with what a click would enter: its saved filters, else its `apply` (always `apply` for a transient view). It switches nothing and sends no request. `apply` runs on a scratch store for this, so it must write only into the store it receives. It returns `{}` on a table without `[filters]` and `null` for an id not in `[views]` (yet). Call it beside each list load; a non-static `@ViewChild` is still unresolved during the first one, so hand the table in from a template reference:
|
|
278
|
+
|
|
279
|
+
```html
|
|
280
|
+
<spiderly-data-table #table [views]="views" (onLazyLoad)="refreshCounts(table)" ...></spiderly-data-table>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
refreshCounts(table: SpiderlyDataTableComponent): void {
|
|
285
|
+
const filtersByView: Record<string, Record<string, FilterRule[]>> = {};
|
|
286
|
+
for (const view of this.views) {
|
|
287
|
+
const filters = table.viewFilterPayload(view.id);
|
|
288
|
+
if (filters) filtersByView[view.id] = filters; // null: the table does not hold this view yet
|
|
289
|
+
}
|
|
290
|
+
// send filtersByView with your count query, then rebuild [views] with the counts
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
277
294
|
**A view can declare its columns**: `columns` is a field → visible map that overrides `Column.visible` in that view only, naming just the deviation — a column it does not name keeps its `Column.visible`. Visibility resolves per field as the user's own per-view choice ?? the view's `columns` ?? `Column.visible`; the user's choice is stored only where it differs from the view's declaration, *Reset to default* returns to the declaration, and `lockVisible` columns always show. To swap one column for another, declare them next to each other in `cols` (the swapped-in one `visible: false`) and the view's column takes the other's place. Type the row as the second generic to check the field names.
|
|
278
295
|
|
|
279
296
|
```typescript
|
|
@@ -1,33 +1,46 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: frontend-localization
|
|
3
|
-
description: How Spiderly localizes Angular admin-panel UI strings — labels, buttons, menu items, validation messages, any user-facing text. Spiderly uses Transloco with flat assets/i18n/{lang}.json files
|
|
3
|
+
description: How Spiderly localizes Angular admin-panel UI strings — labels, buttons, menu items, validation messages, any user-facing text. Spiderly uses Transloco with flat assets/i18n/{lang}.json files imported into the build by the app's ImportedTranslocoLoader. Use whenever you add or translate a UI string, hit a raw translation key rendering on screen, set up or change languages (provideTransloco / availableLangs), use translocoService.translate or the *transloco template directive, run i18n:extract, or wonder how form labels get auto-translated. For .NET backend strings (error messages, Excel names, IStringLocalizer), use the backend-localization skill instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Frontend Localization
|
|
7
7
|
|
|
8
|
-
The Spiderly Angular admin panel localizes every user-facing string through **Transloco**. Translation files are flat key→value JSON, one per language,
|
|
8
|
+
The Spiderly Angular admin panel localizes every user-facing string through **Transloco**. Translation files are flat key→value JSON, one per language, imported into the build: each becomes a content-hashed chunk that ships with the code reading its keys, loaded when Transloco first needs the language. Never hardcode user-facing English in a template; route it through a translation key so it localizes with the rest of the panel (e.g. a PACMS app runs entirely in Serbian).
|
|
9
9
|
|
|
10
10
|
This is a **separate system from the backend**: a string shown by the API (a `BusinessException` message) is translated on the .NET side via `IStringLocalizer` (see the **backend-localization** skill); a string rendered by the admin UI is translated here.
|
|
11
11
|
|
|
12
|
-
## Setup (`
|
|
12
|
+
## Setup (`translations.ts`)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
`spiderly init` writes the loader and the Transloco config to `src/app/business/services/translations.ts`; `app.config.ts` only calls `provideTranslations()`:
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
@Injectable({ providedIn: 'root' })
|
|
18
|
+
export class ImportedTranslocoLoader implements TranslocoLoader {
|
|
19
|
+
getTranslation(lang: string): Promise<Translation> {
|
|
20
|
+
return import(`../../../assets/i18n/${lang}.json`).then((file) => file.default);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function provideTranslations() {
|
|
25
|
+
return provideTransloco({
|
|
26
|
+
config: {
|
|
27
|
+
availableLangs: ['en'], // every language you ship a JSON file for
|
|
28
|
+
defaultLang: 'en',
|
|
29
|
+
reRenderOnLangChange: true, // false if users never switch language at runtime
|
|
30
|
+
},
|
|
31
|
+
loader: ImportedTranslocoLoader,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
25
34
|
```
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
So:
|
|
28
37
|
|
|
29
38
|
- Files live in **`src/assets/i18n/{lang}.json`**, named **exactly** after a `lang` in `availableLangs` (e.g. `sr-Latn-RS.json`, `en.json`).
|
|
30
|
-
-
|
|
39
|
+
- The build bundles them; they are **not** published as `/assets/i18n/*.json`. `angular.json` leaves them out of the assets glob (`"ignore": ["i18n/**"]`), so nothing can fetch them back.
|
|
40
|
+
|
|
41
|
+
### Why imported, not fetched
|
|
42
|
+
|
|
43
|
+
A file at an unchanging URL outlives a deploy in the browser cache. A server that sends no `Cache-Control` lets the browser guess a freshness lifetime (Chromium: a tenth of the time since `Last-Modified`), and a hard reload does not help: it bypasses the cache only for requests made before the page's load event, and Transloco asks for the file after it. The user then runs the new code against the old file, and every key added by the deploy renders raw. An imported file gets a new URL whenever its content changes, so this cannot happen. Spiderly's former `SpiderlyTranslocoLoader` fetched `${frontendUrl}/assets/i18n/{lang}.json` and was removed for this reason.
|
|
31
44
|
|
|
32
45
|
## File format
|
|
33
46
|
|
|
@@ -111,8 +124,8 @@ If you build a reusable control/component with text baked into its template, rou
|
|
|
111
124
|
## Gotchas
|
|
112
125
|
|
|
113
126
|
- **A missing key renders the raw key.** Transloco's default `missingHandler` returns the key string, so a typo'd or unseeded key leaks the identifier (e.g. `InvalidSKU`) onto the screen instead of erroring. Treat a raw-key sighting as a missing-translation bug. Keep keys in sync across all `{lang}.json` files and lean on `i18n:extract`.
|
|
114
|
-
- **Filename must match `availableLangs`.** `
|
|
115
|
-
- **
|
|
127
|
+
- **Filename must match `availableLangs`.** `ImportedTranslocoLoader` imports `{lang}.json` for the active lang; a mismatch (`sr.json` while `availableLangs` is `['sr-Latn-RS']`) fails the import and the whole file falls through to raw keys.
|
|
128
|
+
- **New keys render raw for some users right after a deploy, and a hard reload does not fix it?** Something still fetches the translations from a URL that survives deploys (see *Why imported, not fetched*). Check that `app.config.ts` calls `provideTranslations()` and that the network panel shows no `/assets/i18n/` request.
|
|
116
129
|
- **`reRenderOnLangChange: false`** means a runtime language switch won't re-render already-rendered text. Set it to `true` only if you actually offer in-session language switching.
|
|
117
130
|
|
|
118
131
|
## Backend strings are a separate system
|
|
@@ -183,13 +183,16 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
|
183
183
|
root * /srv
|
|
184
184
|
encode zstd gzip
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
# Content-hashed build output (main-QCWZPP5O.js, media/primeicons-4GST5W3O.woff2) that exists
|
|
187
|
+
# on disk; a missing chunk falls back to index.html and must not be marked immutable.
|
|
188
|
+
@hashed {
|
|
189
|
+
path_regexp ^/(media/)?[^/]+-[A-Z0-9]{8}\.[a-z0-9]+$
|
|
190
|
+
file
|
|
188
191
|
}
|
|
189
|
-
header @
|
|
192
|
+
header @hashed Cache-Control "public, max-age=31536000, immutable"
|
|
190
193
|
|
|
191
|
-
|
|
192
|
-
header
|
|
194
|
+
# Everything else (index.html under every route, /assets/*) is revalidated on every use.
|
|
195
|
+
header ?Cache-Control "no-cache"
|
|
193
196
|
|
|
194
197
|
try_files {path} /index.html
|
|
195
198
|
file_server
|
|
@@ -198,6 +201,8 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
|
198
201
|
|
|
199
202
|
The `try_files {path} /index.html` line is the SPA fallback that lets the Angular router handle deep links.
|
|
200
203
|
|
|
204
|
+
Two details in the cache rules matter. Angular's esbuild builder names hashed files `name-HASH.js` (eight uppercase letters and digits), so a `\.[a-f0-9]{8,}\.js` pattern written for the old webpack names matches nothing. And `header` runs before `try_files`, so a `path /index.html` matcher never sees a navigation to `/` or `/orders`: the `?Cache-Control` default covers them. Without any `Cache-Control` a browser guesses a freshness lifetime (Chromium: a tenth of the time since `Last-Modified`) and can open the app from a pre-deploy `index.html`.
|
|
205
|
+
|
|
201
206
|
**Lockfile gotcha:** `npm ci` requires `package.json` and `package-lock.json` to be in sync. If a stale dev-dep pins an older Angular major as a peer (e.g. `@jsverse/transloco-keys-manager` 5.x pins Angular 17 while your project is on Angular 19), `npm ci` fails. Bump the dev-dep to the matching major (e.g. `transloco-keys-manager` 6.x for Angular 19) rather than reaching for `--legacy-peer-deps`.
|
|
202
207
|
|
|
203
208
|
## Terraform layout
|
|
@@ -406,7 +411,7 @@ Postgres data lives in a Docker volume on a single VPS — disk failure, acciden
|
|
|
406
411
|
- **CORS `FrontendUrl`.** The backend's `AppSettings__Spiderly.Shared__FrontendUrl` must point to the admin subdomain, not a build-preview URL. Update it when you cut over from staging hosting.
|
|
407
412
|
- **First deploy ordering.** The backend compose references `${ADMIN_IMAGE}`. On a fresh VPS, that image must exist in GHCR before backend deploys, or the admin service definition fails to pull. Push admin first, or run the admin workflow once before the first backend deploy.
|
|
408
413
|
- **`docker compose up -d` scoping.** Always scope to the services you're updating: `docker compose up -d backend caddy` for backend deploys, `docker compose up -d admin` for admin deploys. Unscoped `up -d` bounces every service that's currently down or has a config change — including admin and postgres on a backend-only commit. The healthcheck-gated `depends_on` makes the unscoped form *safe* but not *desirable*.
|
|
409
|
-
- **Static assets caching.** Hashed Angular bundles (e.g. `main.
|
|
414
|
+
- **Static assets caching.** Hashed Angular bundles (e.g. `main-QCWZPP5O.js`) can be cached forever; `index.html` and every other unhashed file must be revalidated on each use, or users get a stale shell after a deploy. The Caddyfile in this skill handles both, and translations are imported into the build rather than served from `/assets/i18n/` (see the **frontend-localization** doc).
|
|
410
415
|
- **`tls_private_key` lives in Terraform state in cleartext.** R2 encryption-at-rest is bucket-level; anyone with R2 API access (or a leaked state file) can read the key. Scope the R2 token tightly, audit who can pull state, and never copy `terraform.tfstate` to laptops or shared drives. Rotating the cert means a fresh `terraform apply` followed by re-pasting the new outputs into GitHub Secrets — plan a maintenance window. Add `lifecycle { prevent_destroy = true }` to the `tls_private_key` and `cloudflare_origin_ca_certificate` resources so accidental config changes can't trigger a silent rotation. Also add `ignore_changes = [hostnames, csr]` on the cert — Cloudflare normalizes hostname/CSR ordering server-side, otherwise every plan would show a phantom recreate.
|
|
411
416
|
- **Postgres data on Docker named volumes is not durable across server replacement.** If the VPS itself is Terraform-managed (`hcloud_server`) and gets recreated for any reason — image change, server_type change, accidental destroy — the local Docker named volume `postgres_data` goes with it. Add `lifecycle { prevent_destroy = true }` to the `hcloud_server` resource. For genuine durability, attach an `hcloud_volume` and bind-mount it into postgres (`/var/lib/postgresql` on the 18+ image); volumes survive server destruction and snapshot independently.
|
|
412
417
|
- **Cloudflare in front of Vercel must stay "DNS only".** When the Next.js storefront (or a Vercel-hosted admin) deploys to Vercel and DNS is on Cloudflare, those records must be **DNS only** (grey cloud), not proxied. Vercel terminates TLS and runs its own edge — CDN, image optimization, ISR, PPR — so adding the Cloudflare proxy on top breaks the TLS handshake and double-caches/conflicts with Vercel's edge features. Cloudflare's WAF/cache/analytics/Turnstile belong on the **Hetzner-served** records (`api.*`, `admin.*`) where the orange cloud stays on. If you want Cloudflare features in front of Vercel anyway, that's an advanced setup ("Full (strict)" SSL + custom hostnames) that Vercel does not officially recommend.
|