UI composition and layout

The shell

Two nested components form the frame. AppComponent provides the theme classes and the outer outlet; MainLayoutComponent provides the page structure.

<!-- app.component.html -->
<div class="preloaderbar active" appPreloader><span class="bar"></span></div>
<div id="app-main" class="app-main full-height" [ngClass]="{ … }">
  <router-outlet></router-outlet>
</div>
<!-- shared/layout/main-layout.html -->
<app-stage-info></app-stage-info>
<app-layout-sidenav></app-layout-sidenav>

<section id="page-container" class="app-page-container">
  <div class="app-content-wrapper">
    <div class="app-content {{ customClasses }}">
      <div class="full-height"><router-outlet></router-outlet></div>
    </div>
    <a class="mat-fab mat-accent d-lg-none" appToggleOffcanvasNav>…</a>
  </div>
</section>

<app-search-overlay></app-search-overlay>

Consequences worth knowing:

  • /login, /logout and /reset-password are declared outside MainLayoutComponent, so they render into the outer outlet with no sidebar, stage banner or search overlay.

  • The search overlay is a singleton in the shell, opened by the appOpenSearchOverlay directive from the sidebar.

  • app-stage-info shows which environment/back-end build you are pointed at — see versions.

Theme classes

AppComponent binds a dozen classes on #app-main from two sources:

  • ThemeConfig (from themeConfigData(env.envName) in src/app/config.ts) — fixed-header, nav-collapsed, nav-behind, layout-boxed, theme-gray, theme-dark, sidebar-sm, sidebar-lg.

  • The layout store slice — crm-fixed, quickview-open, supplier-info-open, supplier-info-close.

ThemeConfig also carries colorOption (a two-digit code driving the sidebar palette; 11 on prod, 12 elsewhere), sidebarWidth, productLink, and specialOccasion from shared/utils/functions/check-for-special-occasion.ts — seasonal theming.

Additionally AppComponent toggles a restricted-access class on document.body for REST_UW_ACCESS users, which SCSS uses to suppress UI wholesale.

Global stylesheets

Eight SCSS entry points are listed in angular.json, loaded in this order: styles.scss, bootstrap.scss, font-awesome.scss, layout.scss, loader.scss, material2-theme.scss, theme.scss, ui.scss, then the two icon fonts. stylePreprocessorOptions.includePaths adds src/assets, so partials there can be imported by bare name.

Component SCSS should follow ABEM without prefixes — camelCase blocks and elements with separately-applied modifiers:

<section class="uwCard__subtitle -active -selected -primary">…</section>

See conventions for the full rule and its rationale.

The panels around the outlet

Three regions surround the routed content and are controlled by the layout slice rather than by routing:

Region Behaviour

Sidebar (shared/sidenav/)

Brand, menu, front-end and back-end version labels, logged-in user menu, help link. Collapsible via toggleCollapsedNav(); auto-closes on mobile route change via appAutoCloseMobileNav. The menu tree itself is sidenav-menu.component.html — hand-maintained, permission- and flag-gated.

CRM panel (crm-tools/crm-sidebar/)

Four states: HIDDEN, OPEN, CLOSED, FIXED (CrmStates). Driven by CRM_PANEL_OPEN / _CLOSE / _FIX / _HIDE actions and by the route-pattern defaults.

Supplier info panel

Three states: HIDDEN, OPEN, CLOSED (SupplierInfoStates). Same mechanism.

Route-pattern layout defaults

shared/layout/store/layout.reducers.ts holds a userLayoutConfig keyed by three URL regexes:

Pattern Regex Defaults

supplierList

^/suppliers/list(.*)$

CRM hidden, supplier info hidden, search visible, actions hidden.

supplierGeneral

^/suppliers/(.)/general(.)$

CRM fixed, supplier info open, search visible, actions visible.

supplierDefault

^/suppliers/(.*)$

CRM closed, supplier info open, search visible, actions visible.

LayoutUpdate(urlAfterRedirects) — dispatched from AppComponent on NavigationEnd — selects the first matching pattern and applies its config, while also remembering per-pattern user overrides. So "the CRM panel is pinned on the supplier General tab but merely available elsewhere" is a configuration fact, not component code.

LayoutService supplies customClasses$ for per-page body classes; FabHolderNavComponent hosts the floating action buttons.

Table generations

Four table implementations coexist under shared/. Knowing which is which saves a lot of time.

Folder Status and use

pyb-ag-grid/

Current. The wrapper module to use for new grids. Wraps AG Grid 23.x with the project’s defaults.

data-table-ag-grid/

AG Grid-based table with header-template rendering (data-table-ag-grid-header-template-renderer.service.ts) and summed-value headers (services/headers-with-summed-values-setting.service.ts). Widely used; effectively the previous generation.

data-table-grid/

Includes ag-greed-data-reshaper.service.ts (sic) which reshapes API responses into grid rows. Transitional.

data-table/

The oldest, Material-table-based implementation. Its data-table.common.ts contributes DataTableQueryParams, which is still the shared query-parameter contract used by services such as BalanceApprovalService.

simple-table/, data-grid/

Lightweight non-AG-Grid tables for small fixed lists.

cell-components/

Reusable AG Grid cell renderers shared across grids.

pyb-pagination/

Pagination control used with the non-AG-Grid tables.

For a new grid use pyb-ag-grid. Before changing a shared cell renderer, grep for its usages — the same renderers are consumed by grids in all four generations.

AG Grid enterprise features are in use, and the licence key is set in main.ts with an expired ExpiryDate. Expect a console warning; it does not disable functionality.

Dialogs

shared/dialog/ provides DialogModule + DialogService, with dialog.config.ts holding shared MatDialogConfig defaults. Purpose-built dialogs sit alongside:

Component Use

confirm-dialog/

Generic yes/no confirmation.

confirm-delete/

Delete confirmation with the item name.

information-dialog/

Informational, single-button.

reason-dialog/

Confirmation that requires a free-text reason — used by supplier lifecycle actions where the reason is persisted.

summary-dialog/

Shows a computed summary before committing an operation.

manage/permission-error-pop-up/

The shared "access denied" dialog.

Domain modals live with their feature, not in shared/ — the ~20 folders under suppliers/supplier/ such as add-receivable/, add-chargeback/, cancel-supplier/, transaction-modal/ are each a dialog plus its service.

Notifications are toasts (ngx-toastr), never dialogs, and go through ErrorVisualizationService for errors. See error handling.

Forms

Angular reactive forms throughout. Shared form building blocks:

Component/service Purpose

forms/dynamic-form-control.service.ts

Builds form controls from a configuration object — used where the field set is data-driven (report configuration, onboarding templates).

card-formgroup/

The recurring "editable card" pattern: a titled panel with edit/save/cancel that submits one formGroupKey. card-formgroup-message.service.ts carries save/error feedback. This is the UI half of partial editing.

inline-edit/

Single-field in-place editing.

input-file/

File selection for uploads.

radiobuttons/, checkbox-filters/, chip/, time-select-picker/

Input primitives.

editor/

TinyMCE wrapper (EditorModule) for email bodies and rich notes.

text-value/, title-with-icon/, no-data/, spinner-button/

Display primitives — read-only value pairs, section headers, empty states, and a button with an inline progress state.

drag-and-drop-list/

Ordering UI (onboarding steps, report columns).

@ultimate/ngxerrors

Declarative validation-message display; legacy, used in older forms.

Area Detail

filters/, checkbox-filters/

Reusable filter controls bound to grid query parameters.

filtering-panel/

A composite panel with services/filtering-panel-service/filtering-panel.service.ts coordinating multiple filter controls.

search-overlay/

The global search modal, opened by the appOpenSearchOverlay directive. search-engine-service-3-0/search-engine.service.ts calls the internal API’s search3 parameter (with a legacy search2 path still in the file) and elastic-highlights.service.ts renders match fragments. search-overlay-single-result/ renders one hit.

SearchEngineService.pybEncode percent-escapes ?, =, & and + as ~F, ~3D, ~26, ~2B before sending the phrase — a backend-specific encoding. Do not URL-encode instead; the backend expects this scheme.

The layout slice’s choosenSearch (NEW_SEARCH / OLD_SEARCH) still exists with CHOOSE_NEW_SEARCH / CHOOSE_OLD_SEARCH actions; NEW_SEARCH is the default and SearchEngineService.search() has its feature check short-circuited to if (1 === 1).

Directives

18 directives, mostly layout and behaviour glue:

Directive Effect

appAutoCloseMobileNav

Closes the sidebar on navigation when AutoCloseMobileNav is set.

appToggleOffcanvasNav

The mobile menu FAB.

appAccordionNav, appAppendSubmenuIcon, appHighlightActiveItems

Sidebar menu behaviour — accordion expansion, chevron injection, active-route highlighting.

appOpenSearchOverlay

Opens the global search overlay.

appEnabledByFeatureFlag

Structural directive wrapping NgIf with a feature-flag check. See feature flags.

appPreloader

Binds the top progress bar to PendingInterceptorService.

disabled-reason.directive.ts

Shows why a control is disabled — the tooltip pattern used across the supplier workspace where a field is read-only due to status or permission.

Component base class

shared/component-subscriber/subscriber.component.ts provides SubscriberComponent, which collects this.subscriptions and unsubscribes in ngOnDestroy. AppComponent, MainLayoutComponent, ResizeIframeService and many feature components extend it.

Extend SubscriberComponent and push into this.subscriptions rather than hand-rolling takeUntil — it is the established pattern and reviewers expect it.

Animations

shared/animations/ holds the shared Angular animation triggers (panel slide, expand/collapse) used by the CRM panel and expandable cards. BrowserAnimationsModule is imported in AppModule.