21 KiB
ISA Frontend – AI Assistant Working Rules
Concise, project-specific guidance so an AI agent can be productive quickly. Focus on THESE patterns; avoid generic boilerplate.
1. Monorepo & Tooling
- Nx workspace (Angular 20 + Libraries under
libs/**, main appapps/isa-app). - Scripts (see
package.json):- Dev serve:
npm start(=>nx serve isa-app --ssl). - Library tests (exclude app):
npm test(Jest + emerging Vitest). CI usesnpm run ci. - Build dev:
npm run build; prod:npm run build-prod. - Storybook:
npm run storybook. - Swagger codegen:
npm run generate:swaggerthennpm run fix:files:swagger.
- Dev serve:
- Default branch in Nx:
develop(nx.json: defaultBase). Use affected commands when adding libs. - Node >=22, TS 5.8, ESLint flat config (
eslint.config.js).
1.a Project Tree (Detailed Overview)
.
├─ apps/
│ └─ isa-app/ # Main Angular app (Jest). Legacy non-standalone root component pattern.
│ ├─ project.json # Build/serve/test targets
│ ├─ src/
│ │ ├─ main.ts / index.html # Angular bootstrap
│ │ ├─ app/main.component.ts # Root component (standalone:false)
│ │ ├─ environments/ # Environment files (prod replace)
│ │ ├─ assets/ # Static assets
│ │ └─ config/ # Runtime config JSON (read via Config service)
│ └─ .storybook/ # App Storybook config
│
├─ libs/ # All reusable code (grouped by domain / concern)
│ ├─ core/ # Cross-cutting infrastructure
│ │ ├─ logging/ # Logging service + providers + sinks
│ │ │ ├─ src/lib/logging.service.ts
│ │ │ ├─ src/lib/logging.providers.ts
│ │ │ └─ README.md # Full API & patterns
│ │ ├─ config/ # `Config` service (Zod validated lookup)
│ │ └─ storage/ # User-scoped storage + signal store feature (`withStorage`)
│ │ ├─ src/lib/signal-store-feature.ts
│ │ └─ src/lib/storage.ts
│ │
│ ├─ shared/ # Shared UI/services not domain specific
│ │ └─ scanner/ # Scandit integration (tokens, service, components, platform gating)
│ │ ├─ src/lib/scanner.service.ts
│ │ └─ src/lib/render-if-scanner-is-ready.directive.ts
│ │
│ ├─ remission/ # Remission domain features (newer pattern; Vitest)
│ │ ├─ feature/
│ │ │ ├─ remission-return-receipt-details/
│ │ │ │ ├─ vite.config.mts # Signals + Vitest example
│ │ │ │ └─ src/lib/resources/ # Resource factories (signals async pattern)
│ │ │ └─ remission-return-receipt-list/
│ │ └─ shared/ # Dialogs / shared remission UI pieces
│ │
│ ├─ common/ # Cross-domain utilities (decorators, print, data-access)
│ ├─ utils/ # Narrow utility libs (ean-validation, z-safe-parse, etc.)
│ ├─ ui/ # Generic UI components (presentational)
│ ├─ icons/ # Icon sets / wrappers
│ ├─ catalogue/ # Domain area (legacy Jest)
│ ├─ customer/ # Domain area (legacy Jest)
│ └─ oms/ # Domain area (legacy Jest)
│
├─ generated/swagger/ # Generated API clients (regen via scripts; do not hand edit)
├─ tools/ # Helper scripts (e.g. swagger fix script)
├─ testresults/ # JUnit XML (jest-junit). CI artifact pickup.
├─ coverage/ # Per-project coverage outputs
├─ tailwind-plugins/ # Custom Tailwind plugin modules used by `tailwind.config.js`
├─ vitest.workspace.ts # Glob enabling multi-lib Vitest detection
├─ nx.json / package.json # Workspace + scripts + defaultBase=develop
└─ eslint.config.js # Flat ESLint root config
Guidelines: create new code in the closest domain folder; expose public API via each lib src/index.ts; follow existing naming (feature-name.type.ts). Keep generated swagger untouched—extend via wrapper libs if needed.
1.b Import Path Aliases
Use existing TS path aliases (see tsconfig.base.json) instead of long relative paths:
Core / Cross-cutting:
@isa/core/logging,@isa/core/config,@isa/core/storage,@isa/core/tabs,@isa/core/notifications
Domain & Features:
- Catalogue:
@isa/catalogue/data-access - Customer:
@isa/customer/data-access - OMS features:
@isa/oms/feature/return-details,.../return-process,.../return-review,.../return-search,.../return-summary - OMS shared:
@isa/oms/shared/product-info,@isa/oms/shared/task-list - Remission:
@isa/remission/data-access, feature libs (@isa/remission/feature/remission-return-receipt-details,...-list) and shared (@isa/remission/shared/remission-start-dialog,.../search-item-to-remit-dialog,.../return-receipt-actions,.../product)
Shared / UI:
- Shared libs:
@isa/shared/scanner,@isa/shared/filter,@isa/shared/product-image,@isa/shared/product-router-link,@isa/shared/product-format - UI components:
@isa/ui/buttons,@isa/ui/dialog,@isa/ui/input-controls,@isa/ui/layout,@isa/ui/menu,@isa/ui/toolbar, etc. (one alias per folder underlibs/ui/*) - Icons:
@isa/icons
Utilities:
@isa/utils/ean-validation,@isa/utils/z-safe-parse,@isa/utils/scroll-position
Generated Swagger Clients:
@generated/swagger/isa-api,@generated/swagger/oms-api,@generated/swagger/inventory-api, etc. (one per subfolder). Never edit generated sources—wrap in a domain lib if extension needed.
App-local (only inside apps/isa-app context):
- Namespaced folders:
@adapter/*,@domain/*,@hub/*,@modal/*,@page/*,@shared/*(and nested:@shared/components/*,@shared/services/*, etc.),@ui/*,@utils/*,@swagger/*.
Patterns:
- Always add new reusable code as a library then expose via an
@isa/...alias; do not add new generic code under app-local aliases if it may be reused later. - When introducing a new library ensure its
src/index.tsre-exports only stable public surface; internal helpers stay un-exported. - For new generated API groups, extend via thin wrappers in a domain
data-accesslib rather than patching generated code.
2. Testing Strategy
- Legacy tests: Jest (
@nx/jest:jest). New feature libs (e.g. remission feature) use Vitest + Vite plugin (vite.config.mts). - When adding a new library today prefer Vitest unless consistency with existing Jest-only area is required.
- Do NOT mix frameworks inside one lib. Check presence of
vite.config.*to know it is Vitest-enabled. - App (
isa-app) still uses Jest.
3. Architecture & Cross-Cutting Services
- Core libraries underpin features:
@isa/core/logging,@isa/core/config,@isa/core/storage. - Feature domains grouped (e.g.
libs/remission/**,libs/shared/**,libs/common/**). Keep domain-specific code there; UI-only pieces inui/orshared/. - Prefer standalone components but some legacy components set
standalone: false(seeMainComponent). Maintain existing pattern unless doing a focused migration.
4. Logging (Critical Pattern)
- Central logging via
@isa/core/logging(files:logging.service.ts,logging.providers.ts). - Configure once in app config using provider builders:
provideLogging(withLogLevel(...), withSink(ConsoleLogSink), withContext({...})). - Use factory
logger(() => ({ dynamicContext }))(see README) rather than injectingLoggingServicedirectly unless extending framework code. - Context hierarchy: global -> component (
provideLoggerContext) -> instance (factory param) -> message (callback arg). Always pass context as lazy function() => ({ ... })for perf. - Respect log level threshold; do not perform expensive serialization before calling (let sinks handle it or gate behind dev checks).
5. Configuration Access
- Use
Configservice (@isa/core/config/src/lib/config.ts). Fetch values with Zod schema:config.get('licence.scandit', z.string())(seeSCANDIT_LICENSEtoken). Avoid deprecated untyped access.
6. Storage & State Persistence
- Storage abstraction:
injectStorage(SomeProvider)wraps aStorageProvider(local/session/indexedDB/custom user storage) and prefixes keys with current authenticated usersub(OAuthsubfallback 'anonymous'). - When adding persisted signal stores, use
withStorage(storageKey, ProviderType)feature (signal-store-feature.ts) to auto debounce-save (1s) + restore on init. Only pass plain serializable state.
7. Signals & State
- Internal state often via Angular signals & NgRx Signals (
@ngrx/signals). Avoid manual subscriptions—prefer computed/signals andrxMethodfor side effects. - When persisting, ensure objects are JSON-safe; validation via Zod if deserializing external data.
7.a NgRx Signals Deep Dive
Core building blocks we use:
signalStore(...)+ features:withState,withComputed,withMethods,withHooks,withStorage(custom feature incore/storage).rxMethod(from@ngrx/signals/rxjs-interop) to bridge imperative async flows (HTTP calls, debounce, switchMap) into store-driven mutations.getState,patchStatefor immutable, shallow merges; avoid manually mutating nested objects—spread + patch.
Patterns:
- Store Shape: Keep initial state small & serializable (no class instances, functions, DOM nodes). Derive heavy or view-specific projections with
withComputed. - Side Effects: Wrap fetch/update flows inside
rxMethodpipes; ensure cancellation semantics (switchMap) to drop stale requests. - Persistence: Apply
withStorage(key, Provider)last so hooks run after other features; persisted state must be plain JSON (no Dates—convert to ISO strings). Debounce already handled (1s) inwithStorage—do NOT add another debounce upstream unless burst traffic is extreme. - Error Handling: Keep an
errorfield in state for presentation; log vialogger()at Warn/Error levels but do not store full Error object (serialize minimal fields:message, maybecode). - Loading Flags: Prefer a boolean
loadingOR a discriminated unionstatus: 'idle'|'loading'|'success'|'error'for richer UI; avoid multiple booleans that can drift. - Computed Selectors: Name as
XComputedor just semantic (e.g.filteredItems) usingcomputed(() => ...)insidewithComputed; never cause side-effects in a computed. - Resource Factory Pattern: For remote data needed in multiple components, create a factory function returning an object with
value,isLoading,errorsignals plus areload()method; see remissionresources/directory.
Store Lifecycle Hooks:
- Use
withHooks({ onInit() { ... }, onDestroy() { ... } })for restoration, websockets, or timers. Pair cleanups explicitly.
Persistence Feature (withStorage):
- Implementation: Debounced
storeStaterxMethod listens to any state change, saves hashed user‑scoped key (seehash.utils.ts). On init it callsrestoreState(). - Extending: If you need to blacklist transient fields from persistence, add a method wrapping
getStateand remove keys beforestorage.set(extend feature locally rather than editing shared code unless broadly needed).
Typical Store Template:
// feature-x.store.ts
import {
signalStore,
withState,
withComputed,
withMethods,
withHooks,
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { debounceTime, switchMap, tap, catchError, of } from 'rxjs';
import { withStorage } from '@isa/core/storage';
import { logger } from '@isa/core/logging';
interface FeatureXState {
items: ReadonlyArray<Item>;
query: string;
loading: boolean;
error?: string;
}
const initialState: FeatureXState = { items: [], query: '', loading: false };
export const FeatureXStore = signalStore(
withState(initialState),
withProps((store, logger = logger(() => ({ store: 'FeatureX' }))) => ({
_logger: logger,
})),
withComputed(({ items, query }) => ({
filtered: computed(() => items().filter((i) => i.name.includes(query()))),
hasError: computed(() => !!query() && !items().length),
})),
withMethods((store) => ({
setQuery: (q: string) => patchState(store, { query: q }),
// rxMethod side effect to load items
loadItems: rxMethod<string | void>(
pipe(
debounceTime(150),
tap(() => patchState(store, { loading: true, error: undefined })),
switchMap(() =>
fetchItems(store.query()).pipe(
tap((items) => patchState(store, { items, loading: false })),
catchError((err) => {
store._logger.error('Load failed', err as Error, () => ({
query: store.query(),
}));
patchState(store, {
loading: false,
error: (err as Error).message,
});
return of([]);
}),
),
),
),
),
})),
withHooks((store) => ({
onInit() {
store.loadItems();
},
})),
withStorage('feature-x', LocalStorageProvider),
);
Testing Signal Stores (Vitest or Jest):
- Use
runInInjectionContext(TestBed.inject(Injector), () => FeatureXStore)or instantiate via exported factory if provided. - For async rxMethod flows, flush microtasks (
await vi.runAllTimersAsync()if timers used) or rely on returned observable completion when you subscribe inside the test harness. - Snapshot only primitive slices (avoid full object snapshots with volatile ordering).
Migration Tips:
- Converting legacy NgRx reducers: Start by lifting static initial state + selectors into
withState+withComputed; replace effects withrxMethodmaintaining cancellation semantics (switchMapmirrors effect flattening strategy). - Keep action names only if externally observed (analytics, logging). Otherwise remove ceremony—call store methods directly.
Anti-Patterns to Avoid:
- Writing to signals inside a computed or inside another signal setter (causes cascading updates).
- Storing large unnormalized arrays and then repeatedly filtering/sorting in multiple components—centralize that in computed selectors.
- Persisting secrets or PII directly; hash keys already user-scoped but content still plain—sanitize if needed.
- Returning raw subscriptions from store methods; expose signals or idempotent methods only.
7.b Prefer Signals over Observables (Practical Rules)
Default to signals for all in-memory UI & derived state; keep Observables only at I/O edges.
Use Observables for:
- HTTP / WebSocket / SignalR streams at the boundary.
- Timer / interval / external event sources.
- Interop with legacy NgRx store pieces not yet migrated.
Immediately convert inbound Observables to signals:
// Legacy service returning Observable<Item[]>
items$ = http.get<Item[]>(url);
// New pattern
const items = toSignal(http.get<Item[]>(url), { initialValue: [] });
Expose signals from stores & services:
// BAD (forces template async pipe + subscription mgmt)
getItems(): Observable<Item[]> { return this.http.get(...); }
// GOOD
items = toSignal(this.http.get<Item[]>(url), { initialValue: [] });
Bridge when needed:
// Signal -> Observable (rare):
const queryChanges$ = fromSignal(query, { requireSync: true });
// Observable -> Signal (preferred):
const data = toSignal(data$, { initialValue: undefined });
Side-effects: never subscribe manually—wrap in rxMethod (cancels stale work via switchMap).
loadData: rxMethod<void>(
pipe(
switchMap(() =>
this.api.fetch().pipe(tap((r) => patchState(store, { data: r }))),
),
),
);
Template usage: reference signals directly ({{ item.name }}) or in control flow; no | async needed.
Replacing combineLatest / map chains:
// Before (Observable)
vm$ = combineLatest([a$, b$]).pipe(map(([a, b]) => buildVm(a, b)));
// After (Signals)
const vm = computed(() => buildVm(a(), b()));
Debounce / throttle user input: Keep raw form value as a signal; create an rxMethod for debounced fetch instead of debouncing inside a computed.
search = signal('');
runSearch: rxMethod<string>(
pipe(
debounceTime(300),
switchMap((term) =>
this.api
.search(term)
.pipe(tap((results) => patchState(store, { results }))),
),
),
);
effect(() => {
runSearch(this.search());
});
Avoid converting a signal back to an Observable just to use a single RxJS operator; prefer inline signal computed or small helper.
Migration heuristic:
- Identify component
foo$fields used only in template -> convert to signal viatoSignal. - Collapse chains of
combineLatest+mapintocomputed. - Replace imperative
subscribeside-effects withrxMethod+patchState. - Add persistence last via
withStorageif state must survive reload.
Performance tip: heavy derived computations (sorting large arrays) belong in a memoized computed; if expensive & infrequently needed, gate behind another signal flag.
8. Scanner Integration (Scandit)
- Barcode scanning encapsulated in
@isa/shared/scanner(scanner.service.ts). Use provided injection tokens for license & defaults (override via DI if needed). Service auto-configures once;readysignal triggersconfigure()lazily. - Always catch and log errors with proper context; platform gating throws
PlatformNotSupportedErrorwhich is downgraded to warn.
9. Styling
- Tailwind with custom semantic tokens (
tailwind.config.js). Prefer design tokens liketext-isa-neutral-700, spacing utilities with custompx-*scales rather than ad‑hoc raw values. - Global overlays rely on CDK classes; retain
@angular/cdk/overlay-prebuilt.cssin style arrays when creating new entrypoints or Storybook stories.
10. Library Conventions
- File naming: kebab-case; feature first then type (e.g.
return-receipt-list.component.ts). - Provide public API via each lib
src/index.ts. Export only stable symbols; keep internal utilities in subfolders not re-exported. - Add
project.jsonwithtest&linttargets; for new Vitest libs includevite.config.mtsand adjusttsconfig.spec.jsonreferences to vitest types.
11. Adding / Modifying Tests
- For Jest libs: standard
*.spec.tswithTestBed. Spectator may appear in legacy code—do not introduce Spectator in new tests; use Angular Testing Utilities. - For Vitest libs: ensure
vite.config.mtsincludessetupFiles. Usedescribe/itfromvitestand Angular TestBed (see remission resource spec for pattern of usingrunInInjectionContext). - Prefer resource-style factories returning signals for async state (pattern in
createSupplierResource).
12. Performance & Safety
- Logging: rely on lazy context function; avoid
JSON.stringify()unless behind a dev guard. - Storage: hashing keys (see
hash.utils.ts) ensures stable key space; do not bypass if you need consistent per-user scoping. - Scanner overlay: always clean up overlay + event listeners (follow existing
openimplementation for pattern).
13. CI / Coverage / Artifacts
- JUnit XML placed in
testresults/(Jest configured withjest-junit). Keep filename stability for pipeline consumption; do not rename those outputs. - Coverage output under
coverage/libs/...; respect Nx caching—avoid side effects outside project roots.
14. When Unsure
- Search existing domain folder for analogous implementation (e.g. new feature under remission: inspect sibling feature libs for structure).
- Preserve existing DI token patterns instead of introducing new global singletons.
15. Quick Examples
// New feature logger usage
const log = logger(() => ({ feature: 'ReturnReceipt', action: 'init' }));
log.info('Mount');
// Persisting a signal store slice
export const FeatureStore = signalStore(
withState(initState),
withStorage('return:filters', LocalStorageProvider),
);
// Fetch config value safely
const apiBase = inject(Config).get('api.baseUrl', z.string().url());
Let me know if any area (e.g. auth flow, NgRx usage, Swagger generation details) needs deeper coverage and I can extend this file.