Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
120 changes: 79 additions & 41 deletions docs/arch.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,97 @@
# 📂 Folder Structure

Project based on principle **Feature-based Architecture**, this approach provides reusable and consistant
features across the application.
Project based on principle **Feature-based Architecture**: each feature owns its UI, routes, models, mappers, services, and store when needed. Shared and core code live outside features.

```bash
📦 src/
├── 📂 features/ # Каталог із функціональними модулями
│ ├── 📂 feature-name/
│ │ ├── 📂 feature.component.ts/html/scss # Component with template and styles, and base logic file
│ │ ├── 📂 feature-service.ts # Service or Facade to provide data for NGXS
│ │ ├── 📂 feature.store.ts # NGXS Store for feature
│ │ ├── 📂 feature.entitity.ts # Feature Interface for data, Types, Enums
│ │ ├── 📂 feature.guards.ts # Guard's for feature routing and permissions
│ │ ├── 📂 feature.resolvers.ts # Resolvers for data fetching and preloading
│ │ ├── 📂 feature.utils.ts # Additinal utils for feature (formBuilders, converters, mappers)
│ │ ├── feature.module.ts # Optional, if standalone
│ │ ├── feature.routing.ts # Export of standalone components by path
├── 📂 app/
│ ├── 📂 features/ # Feature modules
│ │ └── 📂 feature-name/
│ │ ├── 📂 components/ # Feature UI pieces
│ │ ├── 📂 pages/ # Route-level pages (when used)
│ │ ├── 📂 models/ # Feature-local *.model.ts types
│ │ ├── 📂 mappers/ # Feature API ↔ domain mappers
│ │ ├── 📂 services/ # Feature HTTP / facade services
│ │ ├── 📂 store/ # Feature NGXS state (actions, model, state, selectors)
│ │ ├── 📂 enums/ # Feature enums
│ │ ├── 📂 constants/ # Feature constants
│ │ ├── feature.routes.ts # Feature routes
│ │ └── feature.component.ts # Feature shell / entry component
│ │
│ ├── 📂 core/ # App-wide infrastructure
│ │ ├── 📂 components/ # Shell UI (header, banners, …)
│ │ ├── 📂 services/ # Global services
│ │ ├── 📂 store/ # Core NGXS state (user, emails, …)
│ │ ├── 📂 models/ # Core config / routing types
│ │ ├── 📂 guards/
│ │ ├── 📂 interceptors/
│ │ ├── 📂 helpers/
│ │ └── 📂 provider/
│ │
│ ├── 📂 shared/ # Cross-feature reusable code
│ │ ├── 📂 components/ # Shared UI
│ │ ├── 📂 directives/
│ │ ├── 📂 pipes/
│ │ ├── 📂 services/ # Shared HTTP / helpers
│ │ ├── 📂 stores/ # Shared NGXS domains
│ │ ├── 📂 models/ # Shared domain + JSON:API models
│ │ ├── 📂 mappers/ # Shared mappers
│ │ ├── 📂 enums/
│ │ ├── 📂 guards/
│ │ └── 📂 helpers/
│ │
│ ├── app.component.ts
│ ├── app.config.ts
│ ├── app.config.server.ts # SSR app config
│ ├── app.routes.ts
│ └── app.routes.server.ts # SSR routes
├── 📂 core/ # Base module for global services, components, and state
│ ├── 📂 services/ # Global services (API, Auth, LocalStorage)
│ ├── 📂 components/ # Global components (Header, Footer, Sidebar)
│ ├── 📂 store/ # Core state management (Auth, Settings, Router)
│ ├── core.module.ts # Optional, but must have a provider for core.
├── 📂 shared/ # Shared module for common components, directives, pipes, and services
│ ├── 📂 ui/ # Shared UI components (Button, Input, Modal), or wrappers for 3rd party
│ ├── 📂 directives/ # Shared Directives (ClickOutside, Draggable)
│ ├── 📂 pipes/ # Shared Pipes (Filter, Sort, Format)
│ ├── 📂 services/ # Services, Facades for shared logic (Http, LocalStorage)
│ ├── 📂 store/ # Shared State management (Settings, Theme, Language)
├── app.routes.ts # General Entry point for routing
├── main.ts # Providers Setup and Bootstrap
├── package.json # Dependencies and Scripts
├── 📂 assets/
├── 📂 environments/
├── 📂 styles/
├── 📂 testing/ # Test helpers, mocks, builders (@testing/*)
├── main.ts # Browser bootstrap
├── main.server.ts # SSR bootstrap
├── server.ts # Express / SSR server entry
└── index.html
```

---

## 📋 Models and mappers

- Types live in `*.model.ts` files (interfaces/types, not classes).
- Shared catalog: `shared/models/<domain>/` with optional `*-json-api.model.ts` twins.
- Feature-local types: `features/<feature>/models/`.
- Mappers convert JSON:API ↔ domain at the service boundary.

See [Models Conventions](./models.md).

---

## 🗃️ State

- Shared domains: `shared/stores/<domain>/`
- Feature domains: `features/<feature>/store/`
- Core domains: `core/store/`

See [NGXS State Management](./ngxs.md).

---
```

## 🚀 Dynamic File Generation (Schematics)

Use Angular CLI to generate new feature components, services, and modules.
Use Angular CLI for scaffolding:

```sh
ng generate component feature-name/components/new-component
```

### 📌 Other Schematics:

| **Entity** | **Command** |
|--------------|----------------------------------------------|
| 📌 **Service** | `ng g s feature-name/services/new-service` |
| 📦 **Module** | `ng g m feature-name` |
| 🔐 **Guard** | `ng g g feature-name/guards/auth-guard` |
| 🔄 **Pipe** | `ng g p shared/pipes/currency-format` |
| ✨ **Directive** | `ng g d shared/directives/highlight` |


```
| **Entity** | **Command** |
| ---------------- | ------------------------------------------ |
| 📌 **Service** | `ng g s feature-name/services/new-service` |
| 🔐 **Guard** | `ng g g feature-name/guards/auth-guard` |
| 🔄 **Pipe** | `ng g p shared/pipes/currency-format` |
| ✨ **Directive** | `ng g d shared/directives/highlight` |
94 changes: 94 additions & 0 deletions docs/models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Models Conventions

## Purpose

Models are TypeScript interfaces and types that describe data shapes. They are not classes. Mapping from API wire format to app domain happens in mappers.

## Layers

```
JSON:API (*-json-api.model.ts) → mapper → domain model → store (*StateModel) → UI
```

| Layer | Role | Naming |
| -------- | ------------------------------------------------------ | ----------------------------------------------------- |
| JSON:API | Wire/DTO shapes from the backend (`snake_case` fields) | `*JsonApi`, `*DataJsonApi`, `*ResponseJsonApi` |
| Domain | App-facing shapes (`camelCase` fields) | Prefer descriptive names; `*Model` suffix is optional |
| State | NGXS slice shape | Always `*StateModel` |
| Form | Reactive form value shapes | `*Form`, `*FormGroup` |

Keep JSON:API types in services and mappers. Prefer domain models in stores, selectors, and components.

## Locations

| Location | Use for |
| ---------------------------------------- | ------------------------------------------------------------------------------------------- |
| `src/app/shared/models/<domain>/` | Cross-feature domain + JSON:API types |
| `src/app/shared/models/common/json-api/` | Shared JSON:API primitives (`JsonApiResource`, `ItemResponse`, `ListResponse`, links, meta) |
| `src/app/features/<feature>/models/` | Feature-local UI, form, and API types |
| `src/app/**/store*/**/*.model.ts` | Colocated NGXS state models |
| `src/app/core/models/` | App-wide config and core types |

## File naming

- Domain / general: `kebab-case.model.ts`
- JSON:API twin: `kebab-case-json-api.model.ts` (dash, not dot)
- Forms: `kebab-case-form.model.ts`
- One concern per file when practical; group related exports in the same domain folder

Examples:

- `user.model.ts` + `user-json-api.model.ts`
- `configured-addon.model.ts` + `configured-addon-json-api.model.ts`

## Symbol naming

- Domain: `UserModel`, `InstitutionUser`, `RegistrationCard` (suffix `Model` is encouraged for primary entities, not required for every interface)
- JSON:API: always end with `JsonApi` (e.g. `UserDataJsonApi`, `UserResponseJsonApi`)
- State: always `*StateModel` (e.g. `AddonsStateModel`, `SubjectsStateModel`)
- Do not put snake_case field names on domain models; map them in the mapper

## Mappers

- Live in `shared/mappers/` or `features/<feature>/mappers/`
- Convert `*JsonApi` → domain models (and domain → request payloads when needed)
- Prefer static mapper classes (`UserMapper.fromUserGetResponse`) or focused `mapX` functions; keep one style within a feature

## Imports

Prefer the `@osf/` alias for app code:

```ts
import { UserModel } from '@osf/shared/models/user/user.model';
import { UserDataJsonApi } from '@osf/shared/models/user/user-json-api.model';
```

Feature barrels are optional. When a feature has `models/index.ts`, import from the barrel:

```ts
import { ModeratorModel } from '@osf/features/moderation/models';
```

Otherwise import the file path directly. Do not mix `@osf/shared/models/...` and `@shared/models/...` in new code — use `@osf/...`.

## State models

Colocate with the store. Compose domain types inside `AsyncStateModel` / `AsyncStateWithTotalCount`:

```ts
export interface FilesStateModel {
files: AsyncStateModel<FileModel[]>;
}
```

See [NGXS docs](./ngxs.md) for store layout and async state shape.

## Checklist for new models

1. Put shared types under `shared/models/<domain>/`, feature-only types under `features/<feature>/models/`
2. Add a `*-json-api.model.ts` twin only when typing an HTTP payload/response
3. Use interfaces/types, not classes
4. Keep domain fields camelCase; leave snake_case in JSON:API types
5. Map at the service/mapper boundary before storing or rendering
6. Name store interfaces `*StateModel`
7. Import via `@osf/...`
54 changes: 31 additions & 23 deletions docs/ngxs.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The goal of using NGXS is to centralize and streamline the handling of applicati

### Diagram

[![OSF NGRX Diagram](./assets/osf-ngxs-diagram.png)](./assets/osf-ngxs-diagram.png)
[![OSF NGXS Diagram](./assets/osf-ngxs-diagram.png)](./assets/osf-ngxs-diagram.png)

---

Expand All @@ -45,44 +45,51 @@ Typical NGXS-related files are organized as follows:
```
src/app/shared/stores/
└── addons/
├── addons.actions.ts # All action definitions
├── addons.model.ts # Interfaces & data model
├── addons.state.ts # State implementation
├── addons.selectors.ts # Reusable selectors
├── addons.actions.ts # Action definitions
├── addons.model.ts # State interface (*StateModel) and defaults
├── addons.state.ts # State implementation
├── addons.selectors.ts # Selectors
```

```
src/app/shared/services/
└── addons/
├── addons.service.ts # External API calls
├── addons.service.ts # External API calls (map JSON:API → domain)
```

Feature stores follow the same file set under `features/<feature>/store/`. Core stores live under `core/store/`.

---

## State Models

The OSF Angular project follows a consistent NGXS state model structure to ensure clarity, predictability, and alignment across all features. The recommended shape for each domain-specific state is as follows:
State interfaces are named `*StateModel` and live in the colocated `*.model.ts` file. They are TypeScript interfaces (not classes). Domain entity types come from `shared/models` or feature `models/` — see [Models Conventions](./models.md).

1. Domain state pattern:
Use `AsyncStateModel<T>` (and `AsyncStateWithTotalCount` when a total count is needed) from `shared/models/store/`:

```ts
domain: {
data: [], // Array of typed model data (e.g., Project[], User[])
isLoading: false, // Indicates if data retrieval (GET) is in progress
isSubmitting: false, // Indicates if data submission (POST/PUT/DELETE) is in progress
error: null, // Captures error messages from failed HTTP requests
export interface AsyncStateModel<T> {
data: T;
isLoading: boolean;
isSubmitting?: boolean;
error: string | null;
}
```

2. `data` holds the strongly typed collection of entities defined by the feature's interface or model class.

3. `isLoading` is a signal used to inform the component and template layer that a read or fetch operation is currently pending.
Example store shape:

4. `isSubmitting` signals that a write operation (form submission, update, delete, etc.) is currently in progress.
```ts
export interface FilesStateModel {
files: AsyncStateModel<FileModel[]>;
}
```

5. `error` stores error state information (commonly strings or structured error objects) that result from failed service interactions. This can be displayed in UI or logged for debugging.
1. `data` holds strongly typed domain data (not raw JSON:API payloads when a domain model exists).
2. `isLoading` indicates a read/fetch is in progress.
3. `isSubmitting` indicates a write (create/update/delete) is in progress.
4. `error` stores a failed request message for UI or logging.

Each domain state should be minimal, normalized, and scoped to its specific feature, mirroring the structure and shape of the corresponding OSF backend API response.
Each domain state should be minimal and scoped to its feature.

---

Expand All @@ -96,12 +103,13 @@ Each domain state should be minimal, normalized, and scoped to its specific feat

## Testing

- [Testing Strategy](docs/testing.md)
- [NGXS State Testing Strategy](docs/testing.md#ngxs-state-testing-strategy)
- [Testing Strategy](./testing.md)
- [NGXS State Testing Strategy](./testing.md#15-testing-ngxs-state)

---

## Documentation

Refer to the official NGXS documentation for full API details and advanced usage:
[https://www.ngxs.io/docs](https://www.ngxs.io/docs)
- [Models Conventions](./models.md)
- [Folder Structure](./arch.md)
- Official NGXS docs: [https://www.ngxs.io/docs](https://www.ngxs.io/docs)
2 changes: 1 addition & 1 deletion src/app/core/constants/nav-items.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ export const MENU_ITEMS: CustomMenuItem[] = [
routerLink: '/settings/profile',
label: 'navigation.profileSettings',
visible: true,
routerLinkActiveOptions: { exact: true },
routerLinkActiveOptions: { exact: false },
},
{
id: 'settings-account',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { FilterChipsComponent } from '@osf/shared/components/filter-chips/filter-chips.component';
import { SearchFiltersComponent } from '@osf/shared/components/search-filters/search-filters.component';
import { DiscoverableFilter } from '@osf/shared/models/search/discoverable-filter.model';
import { FilterOptionRemoved } from '@osf/shared/models/search/filter-option-removed';
import { FilterOptionRemoved } from '@osf/shared/models/search/filter-option-removed.model';
import { FilterOptionSelected } from '@osf/shared/models/search/filter-option-selected.model';
import { FilterOptionsSearchText } from '@osf/shared/models/search/filter-options-search-text.model';
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import {
InstitutionDepartment,
InstitutionDepartmentDataJsonApi,
InstitutionDepartmentsJsonApi,
} from '@osf/features/admin-institutions/models';
import { InstitutionDepartment, InstitutionDepartmentDataJsonApi, InstitutionDepartmentsJsonApi } from '../models';

export function mapInstitutionDepartments(jsonApiData: InstitutionDepartmentsJsonApi): InstitutionDepartment[] {
return jsonApiData.data.map((department: InstitutionDepartmentDataJsonApi) => ({
id: department.id,
name: department.attributes.name,
numberOfUsers: department.attributes.number_of_users,
selfLink: department.links.self,
}));
}

Expand All @@ -18,6 +13,5 @@ export function mapInstitutionDepartment(department: InstitutionDepartmentDataJs
id: department.id,
name: department.attributes.name,
numberOfUsers: department.attributes.number_of_users,
selfLink: department.links.self,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import {
InstitutionIndexValueSearchIncludedJsonApi,
InstitutionSearchFilter,
InstitutionSearchResultCountJsonApi,
} from '@osf/features/admin-institutions/models';
} from '../models';

export function mapIndexCardResults(
included: InstitutionIndexValueSearchIncludedJsonApi[] | undefined
): InstitutionSearchFilter[] {
if (!included) {
return [];
}

export function mapIndexCardResults(included: InstitutionIndexValueSearchIncludedJsonApi[]): InstitutionSearchFilter[] {
const indexCardMap = included.reduce(
(acc, item) => {
if (item.type === 'index-card') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InstitutionUser, InstitutionUserDataJsonApi, InstitutionUsersJsonApi }
export function mapInstitutionUsers(jsonApiData: InstitutionUsersJsonApi): InstitutionUser[] {
return jsonApiData.data.map((user: InstitutionUserDataJsonApi) => ({
id: user.id,
userId: user.relationships.user.data.id,
userId: user.relationships?.user?.data?.id ?? '',
userName: user.attributes.user_name,
department: user.attributes.department,
orcidId: user.attributes.orcid_id,
Expand Down
Loading
Loading