diff --git a/docs/arch.md b/docs/arch.md index c926abcef..e6f34845c 100644 --- a/docs/arch.md +++ b/docs/arch.md @@ -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//` with optional `*-json-api.model.ts` twins. +- Feature-local types: `features//models/`. +- Mappers convert JSON:API ↔ domain at the service boundary. + +See [Models Conventions](./models.md). + +--- + +## πŸ—ƒοΈ State + +- Shared domains: `shared/stores//` +- Feature domains: `features//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` | diff --git a/docs/models.md b/docs/models.md new file mode 100644 index 000000000..ac08f49e8 --- /dev/null +++ b/docs/models.md @@ -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//` | 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//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//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; +} +``` + +See [NGXS docs](./ngxs.md) for store layout and async state shape. + +## Checklist for new models + +1. Put shared types under `shared/models//`, feature-only types under `features//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/...` diff --git a/docs/ngxs.md b/docs/ngxs.md index 583394c53..cb3df1551 100644 --- a/docs/ngxs.md +++ b/docs/ngxs.md @@ -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) --- @@ -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//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` (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 { + 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; +} +``` -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. --- @@ -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) diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index 6ec9ee652..1a4f3b833 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -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', diff --git a/src/app/features/admin-institutions/components/filters-section/filters-section.component.ts b/src/app/features/admin-institutions/components/filters-section/filters-section.component.ts index 5ec51efde..836ee5c46 100644 --- a/src/app/features/admin-institutions/components/filters-section/filters-section.component.ts +++ b/src/app/features/admin-institutions/components/filters-section/filters-section.component.ts @@ -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 { diff --git a/src/app/features/admin-institutions/mappers/institution-departments.mapper.ts b/src/app/features/admin-institutions/mappers/institution-departments.mapper.ts index 22b0f07df..aced15a9c 100644 --- a/src/app/features/admin-institutions/mappers/institution-departments.mapper.ts +++ b/src/app/features/admin-institutions/mappers/institution-departments.mapper.ts @@ -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, })); } @@ -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, }; } diff --git a/src/app/features/admin-institutions/mappers/institution-summary-index.mapper.ts b/src/app/features/admin-institutions/mappers/institution-summary-index.mapper.ts index 14c55de85..6f40631a6 100644 --- a/src/app/features/admin-institutions/mappers/institution-summary-index.mapper.ts +++ b/src/app/features/admin-institutions/mappers/institution-summary-index.mapper.ts @@ -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') { diff --git a/src/app/features/admin-institutions/mappers/institution-users.mapper.ts b/src/app/features/admin-institutions/mappers/institution-users.mapper.ts index 7f9785b2e..d79f8f198 100644 --- a/src/app/features/admin-institutions/mappers/institution-users.mapper.ts +++ b/src/app/features/admin-institutions/mappers/institution-users.mapper.ts @@ -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, diff --git a/src/app/features/admin-institutions/models/institution-department.model.ts b/src/app/features/admin-institutions/models/institution-department.model.ts index f0341bd1d..9119aa3e6 100644 --- a/src/app/features/admin-institutions/models/institution-department.model.ts +++ b/src/app/features/admin-institutions/models/institution-department.model.ts @@ -2,5 +2,4 @@ export interface InstitutionDepartment { id: string; name: string; numberOfUsers: number; - selfLink: string; } diff --git a/src/app/features/admin-institutions/models/institution-departments-json-api.model.ts b/src/app/features/admin-institutions/models/institution-departments-json-api.model.ts index b44db76b7..cf15fb815 100644 --- a/src/app/features/admin-institutions/models/institution-departments-json-api.model.ts +++ b/src/app/features/admin-institutions/models/institution-departments-json-api.model.ts @@ -1,19 +1,14 @@ -export interface InstitutionDepartmentAttributesJsonApi { - name: string; - number_of_users: number; -} +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; -export interface InstitutionDepartmentLinksJsonApi { - self: string; -} +export type InstitutionDepartmentsJsonApi = ListResponse; -export interface InstitutionDepartmentDataJsonApi { - id: string; - type: 'institution-departments'; - attributes: InstitutionDepartmentAttributesJsonApi; - links: InstitutionDepartmentLinksJsonApi; -} +export type InstitutionDepartmentDataJsonApi = JsonApiResource< + 'institution-departments', + InstitutionDepartmentAttributesJsonApi +>; -export interface InstitutionDepartmentsJsonApi { - data: InstitutionDepartmentDataJsonApi[]; +interface InstitutionDepartmentAttributesJsonApi { + name: string; + number_of_users: number; } diff --git a/src/app/features/admin-institutions/models/institution-index-value-search-json-api.model.ts b/src/app/features/admin-institutions/models/institution-index-value-search-json-api.model.ts index b76c8e348..1d0767823 100644 --- a/src/app/features/admin-institutions/models/institution-index-value-search-json-api.model.ts +++ b/src/app/features/admin-institutions/models/institution-index-value-search-json-api.model.ts @@ -1,42 +1,37 @@ -import { JsonApiResponse } from '@shared/models/common/json-api.model'; +import { ToOneRelData } from '@osf/shared/models/common/json-api/relationships.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { JsonApiResponse } from '@osf/shared/models/common/json-api/responses.model'; -export interface InstitutionSearchResultCountJsonApi { - attributes: { - cardSearchResultCount: number; - }; - id: string; - type: string; - relationships: { - indexCard: { - data: { - id: string; - type: string; - }; - }; - }; +export type InstitutionIndexValueSearchIncludedJsonApi = + | InstitutionSearchResultCountJsonApi + | InstitutionIndexCardFilterJsonApi; + +export type InstitutionIndexValueSearchJsonApi = JsonApiResponse; +export type InstitutionIndexCardFilterJsonApi = JsonApiResource<'index-card', InstitutionIndexCardAttributesJsonApi>; + +interface InstitutionIndexCardResourceMetadataJsonApi { + '@id': string; + displayLabel?: { '@value': string }[]; + name: { '@value': string }[]; } -export interface InstitutionIndexCardFilterJsonApi { - attributes: { - resourceIdentifier: string[]; - resourceMetadata: { - displayLabel: { '@value': string }[]; - '@id': string; - name: { '@value': string }[]; - }; - }; - id: string; - type: string; +interface InstitutionIndexCardAttributesJsonApi { + resourceIdentifier: string[]; + resourceMetadata: InstitutionIndexCardResourceMetadataJsonApi; } -export type InstitutionIndexValueSearchIncludedJsonApi = - | InstitutionSearchResultCountJsonApi - | InstitutionIndexCardFilterJsonApi; +interface InstitutionSearchResultAttributesJsonApi { + cardSearchResultCount: number; +} + +interface InstitutionSearchResultRelationshipsJsonApi { + indexCard: ToOneRelData; +} -export interface InstitutionIndexValueSearchJsonApi extends JsonApiResponse< - null, - InstitutionIndexValueSearchIncludedJsonApi[] +export interface InstitutionSearchResultCountJsonApi extends Omit< + JsonApiResource, + 'attributes' > { - data: null; - included: InstitutionIndexValueSearchIncludedJsonApi[]; + attributes?: InstitutionSearchResultAttributesJsonApi; + relationships: InstitutionSearchResultRelationshipsJsonApi; } diff --git a/src/app/features/admin-institutions/models/institution-summary-metrics-json-api.model.ts b/src/app/features/admin-institutions/models/institution-summary-metrics-json-api.model.ts index c418e98ac..77a62bad9 100644 --- a/src/app/features/admin-institutions/models/institution-summary-metrics-json-api.model.ts +++ b/src/app/features/admin-institutions/models/institution-summary-metrics-json-api.model.ts @@ -1,3 +1,13 @@ +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse } from '@osf/shared/models/common/json-api/responses.model'; + +export type InstitutionSummaryMetricsJsonApi = ItemResponse; + +export type InstitutionSummaryMetricsDataJsonApi = JsonApiResource< + 'institution-summary-metrics', + InstitutionSummaryMetricsAttributesJsonApi +>; + export interface InstitutionSummaryMetricsAttributesJsonApi { report_yearmonth: string; user_count: number; @@ -11,36 +21,3 @@ export interface InstitutionSummaryMetricsAttributesJsonApi { monthly_logged_in_user_count: number; monthly_active_user_count: number; } - -export interface InstitutionSummaryMetricsRelationshipsJsonApi { - user: { - data: null; - }; - institution: { - links: { - related: { - href: string; - meta: Record; - }; - }; - data: { - id: string; - type: 'institutions'; - }; - }; -} - -export interface InstitutionSummaryMetricsDataJsonApi { - id: string; - type: 'institution-summary-metrics'; - attributes: InstitutionSummaryMetricsAttributesJsonApi; - relationships: InstitutionSummaryMetricsRelationshipsJsonApi; - links: Record; -} - -export interface InstitutionSummaryMetricsJsonApi { - data: InstitutionSummaryMetricsDataJsonApi; - meta: { - version: string; - }; -} diff --git a/src/app/features/admin-institutions/models/institution-users-json-api.model.ts b/src/app/features/admin-institutions/models/institution-users-json-api.model.ts index dd07ec1eb..b35559cee 100644 --- a/src/app/features/admin-institutions/models/institution-users-json-api.model.ts +++ b/src/app/features/admin-institutions/models/institution-users-json-api.model.ts @@ -1,6 +1,17 @@ -import { MetaJsonApi } from '@osf/shared/models/common/json-api.model'; +import { ToOneRel } from '@osf/shared/models/common/json-api/relationships.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; -export interface InstitutionUserAttributesJsonApi { +export type InstitutionUsersJsonApi = ListResponse; + +export interface InstitutionUserDataJsonApi extends JsonApiResource< + 'institution-users', + InstitutionUserAttributesJsonApi +> { + relationships: InstitutionUserRelationshipsJsonApi; +} + +interface InstitutionUserAttributesJsonApi { user_name: string; department: string | null; orcid_id: string | null; @@ -18,29 +29,7 @@ export interface InstitutionUserAttributesJsonApi { storage_byte_count: number; } -export interface InstitutionUserRelationshipDataJsonApi { - id: string; - type: string; -} - -export interface InstitutionUserRelationshipJsonApi { - data: InstitutionUserRelationshipDataJsonApi; -} - -export interface InstitutionUserRelationshipsJsonApi { - user: InstitutionUserRelationshipJsonApi; - institution: InstitutionUserRelationshipJsonApi; -} - -export interface InstitutionUserDataJsonApi { - id: string; - type: 'institution-users'; - attributes: InstitutionUserAttributesJsonApi; - relationships: InstitutionUserRelationshipsJsonApi; - links: Record; -} - -export interface InstitutionUsersJsonApi { - data: InstitutionUserDataJsonApi[]; - meta: MetaJsonApi; +interface InstitutionUserRelationshipsJsonApi { + user: ToOneRel<'user'>; + institution: ToOneRel<'institution'>; } diff --git a/src/app/features/admin-institutions/models/send-message-json-api.model.ts b/src/app/features/admin-institutions/models/send-message-json-api.model.ts index e561865da..70d03b304 100644 --- a/src/app/features/admin-institutions/models/send-message-json-api.model.ts +++ b/src/app/features/admin-institutions/models/send-message-json-api.model.ts @@ -1,12 +1,13 @@ -export interface SendMessageResponseJsonApi { - data: { - id: string; - type: string; - attributes: { - message_text: string; - message_type: string; - bcc_sender: boolean; - reply_to: boolean; - }; - }; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse } from '@osf/shared/models/common/json-api/responses.model'; + +export type SendMessageResponseJsonApi = ItemResponse; + +export type SendMessageDataJsonApi = JsonApiResource<'send-message', SendMessageAttributesJsonApi>; + +interface SendMessageAttributesJsonApi { + message_text: string; + message_type: string; + bcc_sender: boolean; + reply_to: boolean; } diff --git a/src/app/features/admin-institutions/store/institutions-admin.model.ts b/src/app/features/admin-institutions/store/institutions-admin.model.ts index 239078b99..e1f994297 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.model.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.model.ts @@ -4,7 +4,7 @@ import { AsyncStateWithTotalCount } from '@osf/shared/models/store/async-state-w import { InstitutionDepartment, InstitutionSearchFilter, InstitutionSummaryMetrics, InstitutionUser } from '../models'; -export interface InstitutionsAdminModel { +export interface InstitutionsAdminStateModel { departments: AsyncStateModel; summaryMetrics: AsyncStateModel; hasOsfAddonSearch: AsyncStateModel; @@ -14,7 +14,7 @@ export interface InstitutionsAdminModel { institution: AsyncStateModel; } -export const INSTITUTIONS_ADMIN_STATE_DEFAULTS: InstitutionsAdminModel = { +export const INSTITUTIONS_ADMIN_STATE_DEFAULTS: InstitutionsAdminStateModel = { departments: { data: [], isLoading: false, error: null }, summaryMetrics: { data: {} as InstitutionSummaryMetrics, isLoading: false, error: null }, hasOsfAddonSearch: { data: [], isLoading: false, error: null }, diff --git a/src/app/features/admin-institutions/store/institutions-admin.selectors.ts b/src/app/features/admin-institutions/store/institutions-admin.selectors.ts index 721901e4d..a92c98d40 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.selectors.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.selectors.ts @@ -4,82 +4,82 @@ import { Institution } from '@osf/shared/models/institutions/institutions.model' import { InstitutionDepartment, InstitutionSearchFilter, InstitutionSummaryMetrics, InstitutionUser } from '../models'; -import { InstitutionsAdminModel } from './institutions-admin.model'; +import { InstitutionsAdminStateModel } from './institutions-admin.model'; import { InstitutionsAdminState } from './institutions-admin.state'; export class InstitutionsAdminSelectors { @Selector([InstitutionsAdminState]) - static getDepartments(state: InstitutionsAdminModel): InstitutionDepartment[] { + static getDepartments(state: InstitutionsAdminStateModel): InstitutionDepartment[] { return state.departments.data; } @Selector([InstitutionsAdminState]) - static getDepartmentsLoading(state: InstitutionsAdminModel): boolean { + static getDepartmentsLoading(state: InstitutionsAdminStateModel): boolean { return state.departments.isLoading; } @Selector([InstitutionsAdminState]) - static getSummaryMetrics(state: InstitutionsAdminModel): InstitutionSummaryMetrics { + static getSummaryMetrics(state: InstitutionsAdminStateModel): InstitutionSummaryMetrics { return state.summaryMetrics.data; } @Selector([InstitutionsAdminState]) - static getSummaryMetricsLoading(state: InstitutionsAdminModel): boolean { + static getSummaryMetricsLoading(state: InstitutionsAdminStateModel): boolean { return state.summaryMetrics.isLoading; } @Selector([InstitutionsAdminState]) - static getHasOsfAddonSearch(state: InstitutionsAdminModel): InstitutionSearchFilter[] { + static getHasOsfAddonSearch(state: InstitutionsAdminStateModel): InstitutionSearchFilter[] { return state.hasOsfAddonSearch.data; } @Selector([InstitutionsAdminState]) - static getHasOsfAddonSearchLoading(state: InstitutionsAdminModel): boolean { + static getHasOsfAddonSearchLoading(state: InstitutionsAdminStateModel): boolean { return state.hasOsfAddonSearch.isLoading; } @Selector([InstitutionsAdminState]) - static getStorageRegionSearch(state: InstitutionsAdminModel): InstitutionSearchFilter[] { + static getStorageRegionSearch(state: InstitutionsAdminStateModel): InstitutionSearchFilter[] { return state.storageRegionSearch.data; } @Selector([InstitutionsAdminState]) - static getStorageRegionSearchLoading(state: InstitutionsAdminModel): boolean { + static getStorageRegionSearchLoading(state: InstitutionsAdminStateModel): boolean { return state.storageRegionSearch.isLoading; } @Selector([InstitutionsAdminState]) - static getSearchResults(state: InstitutionsAdminModel): InstitutionSearchFilter[] { + static getSearchResults(state: InstitutionsAdminStateModel): InstitutionSearchFilter[] { return state.searchResults.data; } @Selector([InstitutionsAdminState]) - static getSearchResultsLoading(state: InstitutionsAdminModel): boolean { + static getSearchResultsLoading(state: InstitutionsAdminStateModel): boolean { return state.searchResults.isLoading; } @Selector([InstitutionsAdminState]) - static getUsers(state: InstitutionsAdminModel): InstitutionUser[] { + static getUsers(state: InstitutionsAdminStateModel): InstitutionUser[] { return state.users.data; } @Selector([InstitutionsAdminState]) - static getUsersLoading(state: InstitutionsAdminModel): boolean { + static getUsersLoading(state: InstitutionsAdminStateModel): boolean { return state.users.isLoading; } @Selector([InstitutionsAdminState]) - static getUsersTotalCount(state: InstitutionsAdminModel): number { + static getUsersTotalCount(state: InstitutionsAdminStateModel): number { return state.users.totalCount; } @Selector([InstitutionsAdminState]) - static getInstitution(state: InstitutionsAdminModel): Institution { + static getInstitution(state: InstitutionsAdminStateModel): Institution { return state.institution.data; } @Selector([InstitutionsAdminState]) - static getInstitutionLoading(state: InstitutionsAdminModel): boolean { + static getInstitutionLoading(state: InstitutionsAdminStateModel): boolean { return state.institution.isLoading; } } diff --git a/src/app/features/admin-institutions/store/institutions-admin.state.ts b/src/app/features/admin-institutions/store/institutions-admin.state.ts index 8285669a1..db763a6be 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.state.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.state.ts @@ -22,9 +22,9 @@ import { RequestProjectAccess, SendUserMessage, } from './institutions-admin.actions'; -import { INSTITUTIONS_ADMIN_STATE_DEFAULTS, InstitutionsAdminModel } from './institutions-admin.model'; +import { INSTITUTIONS_ADMIN_STATE_DEFAULTS, InstitutionsAdminStateModel } from './institutions-admin.model'; -@State({ +@State({ name: 'institutionsAdmin', defaults: INSTITUTIONS_ADMIN_STATE_DEFAULTS, }) @@ -34,7 +34,7 @@ export class InstitutionsAdminState { private readonly institutionsAdminService = inject(InstitutionsAdminService); @Action(FetchInstitutionById) - fetchInstitutionById(ctx: StateContext, action: FetchInstitutionById) { + fetchInstitutionById(ctx: StateContext, action: FetchInstitutionById) { ctx.patchState({ institution: { data: {} as Institution, isLoading: true, error: null } }); return this.institutionsService.getInstitutionById(action.institutionId).pipe( @@ -50,7 +50,7 @@ export class InstitutionsAdminState { } @Action(FetchInstitutionDepartments) - fetchDepartments(ctx: StateContext) { + fetchDepartments(ctx: StateContext) { const state = ctx.getState(); ctx.patchState({ departments: { ...state.departments, isLoading: true, error: null }, @@ -69,7 +69,7 @@ export class InstitutionsAdminState { } @Action(FetchInstitutionSummaryMetrics) - fetchSummaryMetrics(ctx: StateContext) { + fetchSummaryMetrics(ctx: StateContext) { const state = ctx.getState(); ctx.patchState({ summaryMetrics: { ...state.summaryMetrics, isLoading: true, error: null }, @@ -87,7 +87,7 @@ export class InstitutionsAdminState { } @Action(FetchInstitutionSearchResults) - fetchSearchResults(ctx: StateContext, action: FetchInstitutionSearchResults) { + fetchSearchResults(ctx: StateContext, action: FetchInstitutionSearchResults) { const state = ctx.getState(); ctx.patchState({ searchResults: { ...state.searchResults, isLoading: true, error: null }, @@ -107,7 +107,7 @@ export class InstitutionsAdminState { } @Action(FetchHasOsfAddonSearch) - fetchHasOsfAddonSearch(ctx: StateContext) { + fetchHasOsfAddonSearch(ctx: StateContext) { const state = ctx.getState(); ctx.patchState({ hasOsfAddonSearch: { ...state.hasOsfAddonSearch, isLoading: true, error: null }, @@ -125,7 +125,7 @@ export class InstitutionsAdminState { } @Action(FetchStorageRegionSearch) - fetchStorageRegionSearch(ctx: StateContext) { + fetchStorageRegionSearch(ctx: StateContext) { const state = ctx.getState(); ctx.patchState({ storageRegionSearch: { ...state.storageRegionSearch, isLoading: true, error: null }, @@ -143,7 +143,7 @@ export class InstitutionsAdminState { } @Action(FetchInstitutionUsers) - fetchUsers(ctx: StateContext, action: FetchInstitutionUsers) { + fetchUsers(ctx: StateContext, action: FetchInstitutionUsers) { const state = ctx.getState(); ctx.patchState({ users: { ...state.users, isLoading: true, error: null }, @@ -162,7 +162,7 @@ export class InstitutionsAdminState { } @Action(SendUserMessage) - sendUserMessage(_: StateContext, action: SendUserMessage) { + sendUserMessage(_: StateContext, action: SendUserMessage) { return this.institutionsAdminService .sendMessage({ userId: action.userId, @@ -175,7 +175,7 @@ export class InstitutionsAdminState { } @Action(RequestProjectAccess) - requestProjectAccess(_: StateContext, action: RequestProjectAccess) { + requestProjectAccess(_: StateContext, action: RequestProjectAccess) { return this.institutionsAdminService .requestProjectAccess(action.payload) .pipe(catchError((error) => throwError(() => error))); diff --git a/src/app/features/analytics/mappers/related-counts.mapper.ts b/src/app/features/analytics/mappers/related-counts.mapper.ts index 89ba90f4f..caef745eb 100644 --- a/src/app/features/analytics/mappers/related-counts.mapper.ts +++ b/src/app/features/analytics/mappers/related-counts.mapper.ts @@ -1,13 +1,13 @@ -import { RelatedCountsGetResponse, RelatedCountsModel } from '../models'; +import { RelatedCountsModel, RelatedCountsResponseJsonApi } from '../models'; export class RelatedCountsMapper { - static fromResponse(response: RelatedCountsGetResponse): RelatedCountsModel { + static fromResponse(response: RelatedCountsResponseJsonApi): RelatedCountsModel { return { id: response.data.id, isPublic: response.data.attributes.public, - forksCount: response.data.relationships.forks?.links.related.meta.count || 0, - linksToCount: response.data.relationships.linked_by_nodes?.links.related.meta.count || 0, - templateCount: response.meta.templated_by_count || 0, + forksCount: response.data.relationships.forks?.links?.related?.meta?.count ?? 0, + linksToCount: response.data.relationships.linked_by_nodes?.links?.related?.meta?.count ?? 0, + templateCount: response.meta?.templated_by_count ?? 0, }; } } diff --git a/src/app/features/analytics/models/node-analytics-json-api.model.ts b/src/app/features/analytics/models/node-analytics-json-api.model.ts index e06297531..6fdf9f7be 100644 --- a/src/app/features/analytics/models/node-analytics-json-api.model.ts +++ b/src/app/features/analytics/models/node-analytics-json-api.model.ts @@ -1,33 +1,30 @@ -import { ResponseDataJsonApi } from '@osf/shared/models/common/json-api.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse } from '@osf/shared/models/common/json-api/responses.model'; -export type NodeAnalyticsResponseJsonApi = ResponseDataJsonApi; +export type NodeAnalyticsResponseJsonApi = ItemResponse; -export interface NodeAnalyticsDataJsonApi { - id: string; - type: 'node-analytics'; - attributes: NodeAnalyticsAttributesJsonApi; -} +export type NodeAnalyticsDataJsonApi = JsonApiResource<'node-analytics', NodeAnalyticsAttributesJsonApi>; -export interface NodeAnalyticsAttributesJsonApi { +interface NodeAnalyticsAttributesJsonApi { popular_pages: PopularPageJsonApi[]; unique_visits: UniqueVisitJsonApi[]; time_of_day: TimeOfDayJsonApi[]; referer_domain: RefererDomainJsonApi[]; } -export interface PopularPageJsonApi { +interface PopularPageJsonApi { path: string; route: string; title: string; count: number; } -export interface UniqueVisitJsonApi { +interface UniqueVisitJsonApi { date: string; count: number; } -export interface TimeOfDayJsonApi { +interface TimeOfDayJsonApi { hour: number; count: number; } diff --git a/src/app/features/analytics/models/related-counts-json-api.model.ts b/src/app/features/analytics/models/related-counts-json-api.model.ts index f51b9acf4..2914a1545 100644 --- a/src/app/features/analytics/models/related-counts-json-api.model.ts +++ b/src/app/features/analytics/models/related-counts-json-api.model.ts @@ -1,39 +1,21 @@ -export interface RelatedCountsGetResponse { - data: RelatedCountsDataResponse; - meta: MetaGetResponse; -} - -interface MetaGetResponse { - templated_by_count: number; -} - -interface RelatedCountsDataResponse { - id: string; - attributes: RelatedCountsAttributes; - relationships: RelationshipsResponse; -} +import { RelatedCountRel } from '@osf/shared/models/common/json-api/relationships.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse } from '@osf/shared/models/common/json-api/responses.model'; +import { BaseNodeAttributesJsonApi } from '@osf/shared/models/nodes/base-node-attributes-json-api.model'; -interface RelatedCountsAttributes { - public: boolean; -} - -interface RelationshipsResponse { - forks: Relationship; - linked_by_nodes: Relationship; -} +export type RelatedCountsResponseJsonApi = ItemResponse & { + meta: RelatedCountsResponseMetaJsonApi; +}; -interface Relationship { - links: Links; +interface RelatedCountsDataJsonApi extends JsonApiResource<'related-counts', BaseNodeAttributesJsonApi> { + relationships: RelatedCountsRelationshipsJsonApi; } -interface Links { - related: RelatedLink; +interface RelatedCountsRelationshipsJsonApi { + forks?: RelatedCountRel; + linked_by_nodes?: RelatedCountRel; } -interface RelatedLink { - meta: MetaCount; -} - -interface MetaCount { - count: number; +interface RelatedCountsResponseMetaJsonApi { + templated_by_count: number; } diff --git a/src/app/features/analytics/services/resource-analytics.service.ts b/src/app/features/analytics/services/resource-analytics.service.ts index 96a7e1bf6..019de928c 100644 --- a/src/app/features/analytics/services/resource-analytics.service.ts +++ b/src/app/features/analytics/services/resource-analytics.service.ts @@ -7,7 +7,7 @@ import { AnalyticsMetricsMapper, RelatedCountsMapper } from '@osf/features/analy import { NodeAnalyticsModel, NodeAnalyticsResponseJsonApi, - RelatedCountsGetResponse, + RelatedCountsResponseJsonApi, } from '@osf/features/analytics/models'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { JsonApiService } from '@osf/shared/services/json-api.service'; @@ -41,7 +41,7 @@ export class ResourceAnalyticsService { const url = `${this.apiDomainUrl}/v2/${resourcePath}/${resourceId}/?related_counts=true`; return this.jsonApiService - .get(url) + .get(url) .pipe(map((response) => RelatedCountsMapper.fromResponse(response))); } } diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.spec.ts b/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.spec.ts index 8ca0ac448..976162229 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.spec.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.spec.ts @@ -11,7 +11,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CreateCollectionSubmission } from '@osf/features/collections/store/add-to-collection/add-to-collection.actions'; import { CedarMetadataAttributes, - CedarMetadataRecordData, + CedarMetadataRecordModel, CedarRecordDataBinding, } from '@osf/features/metadata/models'; import { CreateCedarMetadataRecord, UpdateCedarMetadataRecord } from '@osf/features/metadata/store'; @@ -33,13 +33,13 @@ const MOCK_CEDAR_DATA: CedarRecordDataBinding = { isPublished: true, }; -const MOCK_EXISTING_CEDAR_RECORD: CedarMetadataRecordData = { +const MOCK_EXISTING_CEDAR_RECORD: CedarMetadataRecordModel = { id: 'cedar-record-1', - attributes: { metadata: {} as CedarMetadataAttributes, is_published: true }, - relationships: { - template: { data: { type: 'cedar-metadata-templates', id: 'template-1' } }, - target: { data: { type: 'nodes', id: 'project-1' } }, - }, + metadata: {} as CedarMetadataAttributes, + isPublished: true, + templateId: 'template-1', + targetId: 'project-1', + schemaName: 'Test Schema', }; describe('AddToCollectionConfirmationDialogComponent', () => { @@ -53,7 +53,7 @@ describe('AddToCollectionConfirmationDialogComponent', () => { payload?: CollectionSubmissionPayload; project?: { id: string; isPublic: boolean }; cedarData?: CedarRecordDataBinding | null; - existingCedarRecord?: CedarMetadataRecordData | null; + existingCedarRecord?: CedarMetadataRecordModel | null; }; }; diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.ts b/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.ts index 52aef57c0..b7be98cef 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.ts @@ -11,7 +11,7 @@ import { ChangeDetectionStrategy, Component, DestroyRef, inject, signal } from ' import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CreateCollectionSubmission } from '@osf/features/collections/store/add-to-collection/add-to-collection.actions'; -import { CedarMetadataRecordData, CedarRecordDataBinding } from '@osf/features/metadata/models'; +import { CedarMetadataRecordModel, CedarRecordDataBinding } from '@osf/features/metadata/models'; import { CreateCedarMetadataRecord, UpdateCedarMetadataRecord } from '@osf/features/metadata/store'; import { UpdateProjectPublicStatus } from '@osf/features/project/overview/store'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; @@ -26,12 +26,15 @@ import { ToastService } from '@osf/shared/services/toast.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AddToCollectionConfirmationDialogComponent { - private toastService = inject(ToastService); - dialogRef = inject(DynamicDialogRef); - config = inject(DynamicDialogConfig); - destroyRef = inject(DestroyRef); - isSubmitting = signal(false); - actions = createDispatchMap({ + readonly dialogRef = inject(DynamicDialogRef); + + private readonly toastService = inject(ToastService); + private readonly config = inject(DynamicDialogConfig); + private readonly destroyRef = inject(DestroyRef); + + readonly isSubmitting = signal(false); + + private readonly actions = createDispatchMap({ createCollectionSubmission: CreateCollectionSubmission, updateProjectPublicStatus: UpdateProjectPublicStatus, createCedarRecord: CreateCedarMetadataRecord, @@ -42,7 +45,7 @@ export class AddToCollectionConfirmationDialogComponent { const payload = this.config.data.payload; const project = this.config.data.project as ProjectModel | null | undefined; const cedarData = this.config.data.cedarData as CedarRecordDataBinding | null | undefined; - const existingCedarRecord = this.config.data.existingCedarRecord as CedarMetadataRecordData | null | undefined; + const existingCedarRecord = this.config.data.existingCedarRecord as CedarMetadataRecordModel | null | undefined; if (!payload || !project) return; diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts b/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts index 7c78df5cb..2d9b08390 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts @@ -23,7 +23,7 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { UserSelectors } from '@core/store/user'; -import { CedarMetadataRecordData, CedarRecordDataBinding } from '@osf/features/metadata/models'; +import { CedarMetadataRecordModel, CedarRecordDataBinding } from '@osf/features/metadata/models'; import { CreateCedarMetadataRecord, GetCedarMetadataRecords, @@ -117,11 +117,11 @@ export class AddToCollectionComponent implements CanDeactivateComponent { isCollectionMetadataDisabled = computed( () => !this.selectedProject() || !this.projectMetadataSaved() || !this.projectContributorsSaved() ); - existingCedarRecord = computed(() => { + existingCedarRecord = computed(() => { const records = this.cedarRecords(); const templateId = this.requiredMetadataTemplate()?.id; if (!records?.length || !templateId) return null; - return records.find((r) => r.relationships?.template?.data?.id === templateId) ?? null; + return records.find((r) => r.templateId === templateId) ?? null; }); readonly actions = createDispatchMap({ diff --git a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.html b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.html index 0b345920a..0a07bac19 100644 --- a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.html +++ b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.html @@ -17,7 +17,7 @@

{{ 'collections.addToCollection.collectionMetadata' | translate }}

} @@ -44,7 +44,7 @@

{{ 'collections.addToCollection.collectionMetadata' | translate }}

{ let component: CollectionMetadataStepComponent; let fixture: ComponentFixture; + const mockCedarTemplate = MOCK_CEDAR_METADATA_TEMPLATE; + function setup( options: { - cedarTemplate?: CedarMetadataDataTemplateJsonApi | null; - existingCedarRecord?: CedarMetadataRecordData | null; + cedarTemplate?: CedarMetadataTemplateModel | null; + existingCedarRecord?: CedarMetadataRecordModel | null; stepperActiveValue?: number; targetStepValue?: number; isDisabled?: boolean; @@ -88,13 +89,13 @@ describe('CollectionMetadataStepComponent', () => { }); it('should accept cedar template', () => { - setup({ cedarTemplate: MOCK_CEDAR_TEMPLATE }); + setup({ cedarTemplate: mockCedarTemplate }); - expect(component.cedarTemplate()).toEqual(MOCK_CEDAR_TEMPLATE); + expect(component.cedarTemplate()).toEqual(mockCedarTemplate); }); it('should handle discard changes without existing record', () => { - setup({ cedarTemplate: MOCK_CEDAR_TEMPLATE }); + setup({ cedarTemplate: mockCedarTemplate }); component.cedarFormData.set({ field: 'value' }); component.collectionMetadataSaved.set(true); @@ -107,7 +108,7 @@ describe('CollectionMetadataStepComponent', () => { it('should discard cedar changes to existing record metadata', () => { setup({ - cedarTemplate: MOCK_CEDAR_TEMPLATE, + cedarTemplate: mockCedarTemplate, existingCedarRecord: MOCK_CEDAR_RECORD, }); @@ -122,7 +123,7 @@ describe('CollectionMetadataStepComponent', () => { it('should populate cedarFormData from existingCedarRecord', () => { setup({ - cedarTemplate: MOCK_CEDAR_TEMPLATE, + cedarTemplate: mockCedarTemplate, existingCedarRecord: MOCK_CEDAR_RECORD, }); @@ -131,7 +132,7 @@ describe('CollectionMetadataStepComponent', () => { it('should not overwrite cedarFormData from API when metadata is already saved locally', () => { setup({ - cedarTemplate: MOCK_CEDAR_TEMPLATE, + cedarTemplate: mockCedarTemplate, existingCedarRecord: MOCK_CEDAR_RECORD, }); @@ -140,10 +141,7 @@ describe('CollectionMetadataStepComponent', () => { fixture.componentRef.setInput('existingCedarRecord', { ...MOCK_CEDAR_RECORD, - attributes: { - ...MOCK_CEDAR_RECORD.attributes, - metadata: { field: 'api' } as unknown as CedarMetadataRecordData['attributes']['metadata'], - }, + metadata: { field: 'api' } as unknown as CedarMetadataRecordModel['metadata'], }); fixture.detectChanges(); @@ -151,7 +149,7 @@ describe('CollectionMetadataStepComponent', () => { }); it('should not emit cedarDataSaved when handleSaveCedarMetadata is called without editor', () => { - setup({ cedarTemplate: MOCK_CEDAR_TEMPLATE }); + setup({ cedarTemplate: mockCedarTemplate }); const cedarDataSavedSpy = vi.spyOn(component.cedarDataSaved, 'emit'); const stepChangeSpy = vi.spyOn(component.stepChange, 'emit'); @@ -163,7 +161,7 @@ describe('CollectionMetadataStepComponent', () => { }); it('should handle onCedarChange event', () => { - setup({ cedarTemplate: MOCK_CEDAR_TEMPLATE }); + setup({ cedarTemplate: mockCedarTemplate }); const mockMetadata = { field: 'changed' }; const mockEditor = { currentMetadata: mockMetadata }; @@ -177,7 +175,7 @@ describe('CollectionMetadataStepComponent', () => { }); it('should not update cedarFormData when onCedarChange has no currentMetadata', () => { - setup({ cedarTemplate: MOCK_CEDAR_TEMPLATE }); + setup({ cedarTemplate: mockCedarTemplate }); const mockEvent = new CustomEvent('change', {}); const mockEditor = {}; @@ -192,7 +190,7 @@ describe('CollectionMetadataStepComponent', () => { }); it('should not emit cedarDataSaved without template', () => { - setup({ cedarTemplate: MOCK_CEDAR_TEMPLATE }); + setup({ cedarTemplate: mockCedarTemplate }); fixture.componentRef.setInput('cedarTemplate', null); fixture.detectChanges(); diff --git a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts index 0a1934b02..d383908ec 100644 --- a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts +++ b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts @@ -23,8 +23,8 @@ import { AddToCollectionSteps } from '@osf/features/collections/enums'; import { CEDAR_CONFIG, CEDAR_VIEWER_CONFIG } from '@osf/features/metadata/constants'; import { CedarEditorElement, - CedarMetadataDataTemplateJsonApi, - CedarMetadataRecordData, + CedarMetadataRecordModel, + CedarMetadataTemplateModel, CedarRecordDataBinding, } from '@osf/features/metadata/models'; @@ -41,8 +41,8 @@ export class CollectionMetadataStepComponent { stepperActiveValue = input.required(); targetStepValue = input.required(); isDisabled = input.required(); - cedarTemplate = input(null); - existingCedarRecord = input(null); + cedarTemplate = input(null); + existingCedarRecord = input(null); stepChange = output(); cedarDataSaved = output(); @@ -68,7 +68,7 @@ export class CollectionMetadataStepComponent { handleDiscardChanges() { const record = this.existingCedarRecord(); - this.cedarFormData.set(record?.attributes?.metadata ? (record.attributes.metadata as Record) : {}); + this.cedarFormData.set(record?.metadata ? (record.metadata as Record) : {}); this.syncCedarInstance(this.cedarEditor()?.nativeElement); this.collectionMetadataSaved.set(false); } @@ -111,8 +111,8 @@ export class CollectionMetadataStepComponent { if (this.collectionMetadataSaved()) return; const record = this.existingCedarRecord(); - if (record?.attributes?.metadata) { - this.cedarFormData.set(record.attributes.metadata as Record); + if (record?.metadata) { + this.cedarFormData.set(record.metadata as Record); } }); @@ -122,7 +122,7 @@ export class CollectionMetadataStepComponent { const record = this.existingCedarRecord(); const saved = this.collectionMetadataSaved(); - if (!record?.attributes?.metadata && !saved) return; + if (!record?.metadata && !saved) return; this.syncCedarInstance(this.cedarEditor()?.nativeElement); }); diff --git a/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts b/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts index 42e4f6c3c..bc5426251 100644 --- a/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts +++ b/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts @@ -38,38 +38,37 @@ const MOCK_COLLECTION_PROVIDER_WITH_TEMPLATE = { ...MOCK_COLLECTION_PROVIDER, requiredMetadataTemplate: { id: 'template-1', - type: 'cedar-metadata-templates' as const, - attributes: { - schema_name: 'Test', - cedar_id: 'cedar-1', - template: { - '@id': 'https://repo.metadatacenter.org/templates/test', - '@type': 'https://schema.metadatacenter.org/core/Template', - type: 'object', - title: 'Test', - description: '', - $schema: 'http://json-schema.org/draft-04/schema', - '@context': {} as never, - required: [], - properties: { - '@context': { - properties: { - field1: { enum: ['https://schema.metadatacenter.org/properties/test-field-uuid'] }, - }, - }, - field1: { - '@type': 'https://schema.metadatacenter.org/core/TemplateField', - _valueConstraints: { - literals: [{ label: 'Option A' }, { label: 'Option B' }], - }, + schemaName: 'Test', + isForCollections: false, + active: true, + cedarId: 'cedar-1', + template: { + '@id': 'https://repo.metadatacenter.org/templates/test', + '@type': 'https://schema.metadatacenter.org/core/Template', + type: 'object', + title: 'Test', + description: '', + $schema: 'http://json-schema.org/draft-04/schema', + '@context': {} as never, + required: [], + properties: { + '@context': { + properties: { + field1: { enum: ['https://schema.metadatacenter.org/properties/test-field-uuid'] }, }, }, - _ui: { - order: ['field1'], - propertyLabels: { field1: 'Field One' }, - propertyDescriptions: {}, + field1: { + '@type': 'https://schema.metadatacenter.org/core/TemplateField', + _valueConstraints: { + literals: [{ label: 'Option A' }, { label: 'Option B' }], + }, }, }, + _ui: { + order: ['field1'], + propertyLabels: { field1: 'Field One' }, + propertyDescriptions: {}, + }, }, }, }; diff --git a/src/app/features/collections/components/collections-discover/collections-discover.component.ts b/src/app/features/collections/components/collections-discover/collections-discover.component.ts index 39fc8245d..73098fc71 100644 --- a/src/app/features/collections/components/collections-discover/collections-discover.component.ts +++ b/src/app/features/collections/components/collections-discover/collections-discover.component.ts @@ -113,10 +113,8 @@ export class CollectionsDiscoverComponent { this.actions.setDefaultFilterValue('isPartOfCollection', collectionIri); - if (provider.requiredMetadataTemplate?.attributes?.template) { - const extraFilters = CedarTemplateFilterMapper.fromTemplate( - provider.requiredMetadataTemplate.attributes.template - ); + if (provider.requiredMetadataTemplate?.template) { + const extraFilters = CedarTemplateFilterMapper.fromTemplate(provider.requiredMetadataTemplate.template); this.actions.setExtraFilters(extraFilters); } diff --git a/src/app/features/collections/models/collection-license-json-api.model.ts b/src/app/features/collections/models/collection-license-json-api.model.ts index 10f98c57e..13072e4ed 100644 --- a/src/app/features/collections/models/collection-license-json-api.model.ts +++ b/src/app/features/collections/models/collection-license-json-api.model.ts @@ -1,22 +1,24 @@ +import { ToOneRelData } from '@osf/shared/models/common/json-api/relationships.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { DataResponse } from '@osf/shared/models/common/json-api/responses.model'; import { LicenseRecordJsonApi } from '@osf/shared/models/license/licenses-json-api.model'; -export interface CollectionSubmissionMetadataPayloadJsonApi { - data: { - type: 'nodes'; - id: string; - relationships: { - license: { - data: { - id: string; - type: string; - }; - }; - }; - attributes: { - node_license?: LicenseRecordJsonApi; - title?: string; - description?: string; - tags?: string[]; - }; - }; +export type CollectionSubmissionMetadataPayloadJsonApi = DataResponse; + +interface CollectionSubmissionMetadataDataJsonApi extends JsonApiResource< + 'nodes', + CollectionSubmissionMetadataAttributesJsonApi +> { + relationships: CollectionSubmissionMetadataRelationshipsJsonApi; +} + +interface CollectionSubmissionMetadataAttributesJsonApi { + node_license?: LicenseRecordJsonApi; + title?: string; + description?: string; + tags?: string[]; +} + +interface CollectionSubmissionMetadataRelationshipsJsonApi { + license: ToOneRelData<'licenses'>; } diff --git a/src/app/features/collections/models/project-metadata-form.model.ts b/src/app/features/collections/models/project-metadata-form.model.ts index 464ca263a..c21bdf81f 100644 --- a/src/app/features/collections/models/project-metadata-form.model.ts +++ b/src/app/features/collections/models/project-metadata-form.model.ts @@ -1,8 +1,9 @@ import { FormControl } from '@angular/forms'; -import { ProjectMetadataFormControls } from '@osf/features/collections/enums'; import { LicenseModel } from '@osf/shared/models/license/license.model'; +import { ProjectMetadataFormControls } from '../enums'; + export interface ProjectMetadataForm { [ProjectMetadataFormControls.Title]: FormControl; [ProjectMetadataFormControls.Description]: FormControl; diff --git a/src/app/features/collections/store/add-to-collection/add-to-collection.model.ts b/src/app/features/collections/store/add-to-collection/add-to-collection.model.ts index ba47d319d..bbdc0823a 100644 --- a/src/app/features/collections/store/add-to-collection/add-to-collection.model.ts +++ b/src/app/features/collections/store/add-to-collection/add-to-collection.model.ts @@ -1,4 +1,4 @@ -import { CollectionProjectSubmission } from '@osf/shared/models/collections/collections.model'; +import { CollectionProjectSubmission } from '@osf/shared/models/collections/collection-submissions.model'; import { LicenseModel } from '@shared/models/license/license.model'; import { AsyncStateModel } from '@shared/models/store/async-state.model'; diff --git a/src/app/features/files/mappers/file-custom-metadata.mapper.ts b/src/app/features/files/mappers/file-custom-metadata.mapper.ts index 1292bd004..233dd0a2c 100644 --- a/src/app/features/files/mappers/file-custom-metadata.mapper.ts +++ b/src/app/features/files/mappers/file-custom-metadata.mapper.ts @@ -1,10 +1,9 @@ -import { ApiData } from '@osf/shared/models/common/json-api.model'; import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { OsfFileCustomMetadata } from '../models/file-custom-metadata.model'; -import { FileCustomMetadata } from '../models/get-file-metadata-response.model'; +import { FileCustomMetadataDataJsonApi } from '../models/file-metadata-response.model'; -export function MapFileCustomMetadata(data: ApiData): OsfFileCustomMetadata { +export function MapFileCustomMetadata(data: FileCustomMetadataDataJsonApi): OsfFileCustomMetadata { return { id: data.id, description: replaceBadEncodedChars(data.attributes.description), diff --git a/src/app/features/files/mappers/file-revision.mapper.ts b/src/app/features/files/mappers/file-revision.mapper.ts index 34c6b08a4..83268af3a 100644 --- a/src/app/features/files/mappers/file-revision.mapper.ts +++ b/src/app/features/files/mappers/file-revision.mapper.ts @@ -1,14 +1,11 @@ -import { ApiData } from '@osf/shared/models/common/json-api.model'; - import { OsfFileRevision } from '../models/file-revisions.model'; -import { FileRevisionJsonApi } from '../models/get-file-revisions-response.model'; +import { FileRevisionDataJsonApi } from '../models/file-revisions-response.model'; -export function MapFileRevision(data: ApiData[]): OsfFileRevision[] { - const revision = data.map((revision) => ({ +export function MapFileRevision(data: FileRevisionDataJsonApi[]): OsfFileRevision[] { + return data.map((revision) => ({ downloads: revision.attributes.extra.downloads ?? 0, hashes: { md5: revision.attributes.extra.hashes?.md5, sha256: revision.attributes.extra.hashes?.sha256 }, dateTime: new Date(revision.attributes.modified_utc), version: revision.attributes.version, })); - return revision; } diff --git a/src/app/features/files/mappers/resource-metadata.mapper.ts b/src/app/features/files/mappers/resource-metadata.mapper.ts index 5fc0e6fc0..827c3e92b 100644 --- a/src/app/features/files/mappers/resource-metadata.mapper.ts +++ b/src/app/features/files/mappers/resource-metadata.mapper.ts @@ -2,12 +2,12 @@ import { IdentifiersMapper } from '@osf/shared/mappers/identifiers.mapper'; import { ResourceMetadata } from '@osf/shared/models/resource-metadata.model'; import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; -import { GetResourceCustomMetadataResponse } from '../models/get-resource-custom-metadata-response.model'; -import { GetResourceShortInfoResponse } from '../models/get-resource-short-info-response.model'; +import { NodeShortInfoResponse } from '../models/node-short-info-response.model'; +import { ResourceCustomMetadataResponse } from '../models/resource-custom-metadata-response.model'; export function MapResourceMetadata( - shortInfo: GetResourceShortInfoResponse, - customMetadata: GetResourceCustomMetadataResponse + shortInfo: NodeShortInfoResponse, + customMetadata: ResourceCustomMetadataResponse ): ResourceMetadata { return { title: replaceBadEncodedChars(shortInfo.data.attributes.title), diff --git a/src/app/features/files/models/file-metadata-response.model.ts b/src/app/features/files/models/file-metadata-response.model.ts new file mode 100644 index 000000000..925861218 --- /dev/null +++ b/src/app/features/files/models/file-metadata-response.model.ts @@ -0,0 +1,16 @@ +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse } from '@osf/shared/models/common/json-api/responses.model'; + +export type FileMetadataResponse = ItemResponse; + +export type FileCustomMetadataDataJsonApi = JsonApiResource< + 'custom_file_metadata_records', + FileCustomMetadataAttributesJsonApi +>; + +interface FileCustomMetadataAttributesJsonApi { + description: string; + language: string; + resource_type_general: string; + title: string; +} diff --git a/src/app/features/files/models/file-revisions-response.model.ts b/src/app/features/files/models/file-revisions-response.model.ts new file mode 100644 index 000000000..0ee38dddd --- /dev/null +++ b/src/app/features/files/models/file-revisions-response.model.ts @@ -0,0 +1,23 @@ +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { DataResponse } from '@osf/shared/models/common/json-api/responses.model'; + +export type FileRevisionsResponse = DataResponse; + +export type FileRevisionDataJsonApi = JsonApiResource; + +interface FileRevisionAttributesJsonApi { + extra: FileRevisionExtraJsonApi; + modified: string; + modified_utc: string; + version: string; +} + +interface FileRevisionExtraJsonApi { + downloads: number; + hashes: FileRevisionHashesJsonApi; +} + +interface FileRevisionHashesJsonApi { + md5: string; + sha256: string; +} diff --git a/src/app/features/files/models/get-custom-metadata-response.model.ts b/src/app/features/files/models/get-custom-metadata-response.model.ts deleted file mode 100644 index 4420eff6e..000000000 --- a/src/app/features/files/models/get-custom-metadata-response.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiData, JsonApiResponse } from '@osf/shared/models/common/json-api.model'; - -export type GetCustomMetadataResponse = JsonApiResponse, null>; - -export interface MetadataEmbedResponse { - custom_metadata: JsonApiResponse< - ApiData< - { - language: string; - resource_type_general: string; - funders: { - funder_name: string; - funder_identifier: string; - funder_identifier_type: string; - award_number: string; - award_uri: string; - award_title: string; - }[]; - }, - null, - null, - null - >, - null - >; -} diff --git a/src/app/features/files/models/get-file-metadata-response.model.ts b/src/app/features/files/models/get-file-metadata-response.model.ts deleted file mode 100644 index 7f0662855..000000000 --- a/src/app/features/files/models/get-file-metadata-response.model.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ApiData, JsonApiResponse } from '@osf/shared/models/common/json-api.model'; - -export type GetFileMetadataResponse = JsonApiResponse, null>; - -export interface FileCustomMetadata { - language: string; - resource_type_general: string; - title: string; - description: string; -} diff --git a/src/app/features/files/models/get-file-revisions-response.model.ts b/src/app/features/files/models/get-file-revisions-response.model.ts deleted file mode 100644 index 04de4bc59..000000000 --- a/src/app/features/files/models/get-file-revisions-response.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiData, JsonApiResponse } from '@osf/shared/models/common/json-api.model'; - -export interface FileRevisionJsonApi { - extra: { - downloads: number; - hashes: { - md5: string; - sha256: string; - }; - }; - version: string; - modified: string; - modified_utc: string; -} - -export type GetFileRevisionsResponse = JsonApiResponse[], null>; diff --git a/src/app/features/files/models/get-resource-custom-metadata-response.model.ts b/src/app/features/files/models/get-resource-custom-metadata-response.model.ts deleted file mode 100644 index 10e121cfa..000000000 --- a/src/app/features/files/models/get-resource-custom-metadata-response.model.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ApiData, JsonApiResponse } from '@osf/shared/models/common/json-api.model'; - -export type GetResourceCustomMetadataResponse = JsonApiResponse< - ApiData, - null ->; - -export interface ResourceMetadataEmbedResponse { - custom_metadata: JsonApiResponse< - ApiData< - { - language: string; - resource_type_general: string; - funders: { - funder_name: string; - funder_identifier: string; - funder_identifier_type: string; - award_number: string; - award_uri: string; - award_title: string; - }[]; - }, - null, - null, - null - >, - null - >; -} diff --git a/src/app/features/files/models/get-resource-short-info-response.model.ts b/src/app/features/files/models/get-resource-short-info-response.model.ts deleted file mode 100644 index 831a33f76..000000000 --- a/src/app/features/files/models/get-resource-short-info-response.model.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiData, JsonApiResponse } from '@osf/shared/models/common/json-api.model'; -import { IdentifiersResponseJsonApi } from '@osf/shared/models/identifiers/identifier-json-api.model'; - -export type GetResourceShortInfoResponse = JsonApiResponse< - ApiData< - { - title: string; - description: string; - date_created: string; - date_modified: string; - }, - { identifiers: IdentifiersResponseJsonApi }, - null, - null - >, - null ->; diff --git a/src/app/features/files/models/get-short-info-response.model.ts b/src/app/features/files/models/get-short-info-response.model.ts deleted file mode 100644 index 2fd10f273..000000000 --- a/src/app/features/files/models/get-short-info-response.model.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiData, JsonApiResponse } from '@osf/shared/models/common/json-api.model'; -import { IdentifiersResponseJsonApi } from '@osf/shared/models/identifiers/identifier-json-api.model'; - -export type GetShortInfoResponse = JsonApiResponse< - ApiData< - { - title: string; - description: string; - date_created: string; - date_modified: string; - }, - { identifiers: IdentifiersResponseJsonApi }, - null, - null - >, - null ->; diff --git a/src/app/features/files/models/node-short-info-response.model.ts b/src/app/features/files/models/node-short-info-response.model.ts new file mode 100644 index 000000000..13a248d8e --- /dev/null +++ b/src/app/features/files/models/node-short-info-response.model.ts @@ -0,0 +1,20 @@ +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse } from '@osf/shared/models/common/json-api/responses.model'; +import { IdentifiersResponseJsonApi } from '@osf/shared/models/identifiers/identifier-json-api.model'; + +export type NodeShortInfoResponse = ItemResponse; + +export interface NodeShortInfoDataJsonApi extends JsonApiResource { + embeds?: NodeShortInfoEmbedsJsonApi; +} + +interface NodeShortInfoAttributesJsonApi { + date_created: string; + date_modified: string; + description: string; + title: string; +} + +interface NodeShortInfoEmbedsJsonApi { + identifiers: IdentifiersResponseJsonApi; +} diff --git a/src/app/features/files/models/resource-custom-metadata-response.model.ts b/src/app/features/files/models/resource-custom-metadata-response.model.ts new file mode 100644 index 000000000..debe5a66b --- /dev/null +++ b/src/app/features/files/models/resource-custom-metadata-response.model.ts @@ -0,0 +1,30 @@ +import { Embed } from '@osf/shared/models/common/json-api/embeds.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse } from '@osf/shared/models/common/json-api/responses.model'; + +export type ResourceCustomMetadataResponse = ItemResponse; + +export interface ResourceCustomMetadataGuidDataJsonApi extends JsonApiResource<'guids', Record> { + embeds: ResourceCustomMetadataEmbedsJsonApi; +} + +interface ResourceCustomMetadataEmbedsJsonApi { + custom_metadata: Embed; +} + +type CustomMetadataDataJsonApi = JsonApiResource<'custom-metadata-records', CustomMetadataAttributesJsonApi>; + +interface CustomMetadataAttributesJsonApi { + funders: CustomMetadataFunderJsonApi[]; + language: string; + resource_type_general: string; +} + +interface CustomMetadataFunderJsonApi { + award_number: string; + award_title: string; + award_uri: string; + funder_identifier: string; + funder_identifier_type: string; + funder_name: string; +} diff --git a/src/app/features/files/pages/file-detail/file-detail.component.spec.ts b/src/app/features/files/pages/file-detail/file-detail.component.spec.ts index 6cb497816..275f16279 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.spec.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.spec.ts @@ -8,10 +8,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { - CedarMetadataDataTemplateJsonApi, - CedarMetadataRecordData, + CedarMetadataRecordModel, + CedarMetadataTemplateModel, CedarRecordDataBinding, - CedarTemplate, } from '@osf/features/metadata/models'; import { GetCedarMetadataRecords, MetadataSelectors, UpdateCedarMetadataRecord } from '@osf/features/metadata/store'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; @@ -349,26 +348,22 @@ describe('FileDetailComponent', () => { }); it('should select cedar record when metadata tab matches cedar record id', () => { - const cedarRecord = { + const cedarRecord: CedarMetadataRecordModel = { id: 'rec-1', - attributes: { - metadata: {}, - is_published: false, - }, - relationships: { - template: { data: { type: 'cedar-metadata-templates', id: 'tpl-1' } }, - target: { data: { type: 'files', id: 'file-1' } }, - }, - } as CedarMetadataRecordData; - const cedarTemplate = { + metadata: {} as CedarMetadataRecordModel['metadata'], + isPublished: false, + templateId: 'tpl-1', + targetId: 'file-1', + schemaName: 'Schema A', + }; + const cedarTemplate: CedarMetadataTemplateModel = { id: 'tpl-1', - type: 'cedar-metadata-templates', - attributes: { - schema_name: 'Schema A', - cedar_id: 'cedar', - template: {} as CedarTemplate, - }, - } as CedarMetadataDataTemplateJsonApi; + schemaName: 'Schema A', + isForCollections: false, + active: true, + cedarId: 'cedar', + template: {} as CedarMetadataTemplateModel['template'], + }; setup({ selectorOverrides: [ { selector: MetadataSelectors.getCedarRecords, value: [cedarRecord] }, @@ -383,26 +378,22 @@ describe('FileDetailComponent', () => { }); it('should dispatch cedar update and refresh records on cedar form submit', async () => { - const cedarRecord = { + const cedarRecord: CedarMetadataRecordModel = { id: 'rec-1', - attributes: { - metadata: {}, - is_published: false, - }, - relationships: { - template: { data: { type: 'cedar-metadata-templates', id: 'tpl-1' } }, - target: { data: { type: 'files', id: 'file-1' } }, - }, - } as CedarMetadataRecordData; - const cedarTemplate = { + metadata: {} as CedarMetadataRecordModel['metadata'], + isPublished: false, + templateId: 'tpl-1', + targetId: 'file-1', + schemaName: 'Schema A', + }; + const cedarTemplate: CedarMetadataTemplateModel = { id: 'tpl-1', - type: 'cedar-metadata-templates', - attributes: { - schema_name: 'Schema A', - cedar_id: 'cedar', - template: {} as CedarTemplate, - }, - } as CedarMetadataDataTemplateJsonApi; + schemaName: 'Schema A', + isForCollections: false, + active: true, + cedarId: 'cedar', + template: {} as CedarMetadataTemplateModel['template'], + }; setup({ selectorOverrides: [ { selector: MetadataSelectors.getCedarRecords, value: [cedarRecord] }, diff --git a/src/app/features/files/pages/file-detail/file-detail.component.ts b/src/app/features/files/pages/file-detail/file-detail.component.ts index 4e1591d22..e9d72d80d 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.ts @@ -24,8 +24,8 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { - CedarMetadataDataTemplateJsonApi, - CedarMetadataRecordData, + CedarMetadataRecordModel, + CedarMetadataTemplateModel, CedarRecordDataBinding, } from '@osf/features/metadata/models'; import { @@ -182,8 +182,8 @@ export class FileDetailComponent implements OnDestroy { selectedMetadataTab = signal('osf'); - selectedCedarRecord = signal(null); - selectedCedarTemplate = signal(null); + selectedCedarRecord = signal(null); + selectedCedarTemplate = signal(null); cedarFormReadonly = signal(true); canManageFileActions = computed(() => !this.isAnonymous() && !this.hasViewOnly() && this.hasWriteAccess()); @@ -394,7 +394,7 @@ export class FileDetailComponent implements OnDestroy { this.selectedCedarRecord.set(record); this.cedarFormReadonly.set(true); - const templateId = record.relationships?.template?.data?.id; + const templateId = record.templateId; if (templateId && templates?.data) { const template = templates.data.find((t) => t.id === templateId); @@ -419,7 +419,7 @@ export class FileDetailComponent implements OnDestroy { const cedarTabs = records?.map((record) => ({ id: record.id || '', - label: record.embeds?.template?.data?.attributes?.schema_name || `Record ${record.id}`, + label: record.schemaName || `Record ${record.id}`, type: MetadataResourceEnum.CEDAR, })) || []; diff --git a/src/app/features/files/store/files.state.ts b/src/app/features/files/store/files.state.ts index 9b84843c5..5813dfce0 100644 --- a/src/app/features/files/store/files.state.ts +++ b/src/app/features/files/store/files.state.ts @@ -159,11 +159,11 @@ export class FilesState { ctx.patchState({ tags: { ...state.tags, isLoading: true, error: null } }); return this.filesService.getFileTarget(action.fileGuid).pipe( - tap(({ file, meta }) => { + tap(({ file, isAnonymous }) => { ctx.patchState({ openedFile: { data: file, isLoading: false, error: null }, tags: { data: file.tags, isLoading: false, error: null }, - isAnonymous: meta?.anonymous ?? false, + isAnonymous, }); }), catchError((error) => handleSectionError(ctx, 'openedFile', error)) @@ -267,11 +267,11 @@ export class FilesState { tap((response) => ctx.patchState({ rootFolders: { - data: response.files, + data: response.data, isLoading: false, error: null, }, - isAnonymous: response.meta?.anonymous ?? false, + isAnonymous: response.isAnonymous, }) ), catchError((error) => handleSectionError(ctx, 'rootFolders', error)) @@ -287,11 +287,11 @@ export class FilesState { tap((response) => ctx.patchState({ moveDialogRootFolders: { - data: response.files, + data: response.data, isLoading: false, error: null, }, - isAnonymous: response.meta?.anonymous ?? false, + isAnonymous: response.isAnonymous, }) ), catchError((error) => handleSectionError(ctx, 'moveDialogRootFolders', error)) diff --git a/src/app/features/home/pages/dashboard/dashboard.component.ts b/src/app/features/home/pages/dashboard/dashboard.component.ts index 63ec0f106..886c670fc 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.ts +++ b/src/app/features/home/pages/dashboard/dashboard.component.ts @@ -23,7 +23,7 @@ import { SearchInputComponent } from '@osf/shared/components/search-input/search import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants/default-table-params.constants'; import { SortOrder } from '@osf/shared/enums/sort-order.enum'; -import { MyResourcesItem } from '@osf/shared/models/my-resources/my-resources.model'; +import { MyResourcesItem } from '@osf/shared/models/my-resources/my-resources-item.model'; import { MyResourcesSearchFilters } from '@osf/shared/models/my-resources/my-resources-search-filters.model'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ProjectRedirectDialogService } from '@osf/shared/services/project-redirect-dialog.service'; diff --git a/src/app/features/meetings/mappers/meetings.mapper.ts b/src/app/features/meetings/mappers/meetings.mapper.ts index f967aa4bb..bd34d187f 100644 --- a/src/app/features/meetings/mappers/meetings.mapper.ts +++ b/src/app/features/meetings/mappers/meetings.mapper.ts @@ -1,15 +1,15 @@ -import { ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { - MeetingGetResponseJsonApi, - MeetingSubmissionGetResponseJsonApi, + MeetingDataJsonApi, + MeetingsListResponseJsonApi, + MeetingSubmissionsListResponseJsonApi, MeetingSubmissionsWithPaging, MeetingsWithPaging, } from '../models'; export class MeetingsMapper { - static fromMeetingsGetResponse(response: ResponseJsonApi): MeetingsWithPaging { + static fromMeetingsGetResponse(response: MeetingsListResponseJsonApi): MeetingsWithPaging { return { data: response.data.map((item) => ({ id: item.id, @@ -24,7 +24,7 @@ export class MeetingsMapper { } static fromMeetingSubmissionGetResponse( - response: ResponseJsonApi + response: MeetingSubmissionsListResponseJsonApi ): MeetingSubmissionsWithPaging { return { data: response.data.map((item) => ({ @@ -40,7 +40,7 @@ export class MeetingsMapper { }; } - static fromMeetingGetResponse(response: MeetingGetResponseJsonApi) { + static fromMeetingGetResponse(response: MeetingDataJsonApi) { return { id: response.id, name: response.attributes.name, diff --git a/src/app/features/meetings/models/meetings-json-api.model.ts b/src/app/features/meetings/models/meetings-json-api.model.ts index b2d77d7e1..c66e38c64 100644 --- a/src/app/features/meetings/models/meetings-json-api.model.ts +++ b/src/app/features/meetings/models/meetings-json-api.model.ts @@ -1,28 +1,36 @@ import { NumberOrNull, StringOrNull } from '@osf/shared/helpers/types.helper'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse, ListResponse } from '@osf/shared/models/common/json-api/responses.model'; -export interface MeetingGetResponseJsonApi { - id: string; - type: 'meetings'; - attributes: { - name: string; - location: string; - start_date: Date; - end_date: Date; - submissions_count: number; - }; +export type MeetingsListResponseJsonApi = ListResponse; +export type MeetingSubmissionsListResponseJsonApi = ListResponse; +export type MeetingResponseJsonApi = ItemResponse; + +export type MeetingDataJsonApi = JsonApiResource<'meetings', MeetingAttributesJsonApi>; + +export interface MeetingSubmissionDataJsonApi extends JsonApiResource< + 'meeting-submissions', + MeetingSubmissionAttributesJsonApi +> { + links: MeetingSubmissionLinksJsonApi; +} + +interface MeetingAttributesJsonApi { + end_date: Date; + location: string; + name: string; + start_date: Date; + submissions_count: number; +} + +interface MeetingSubmissionAttributesJsonApi { + author_name: string; + date_created: Date; + download_count: NumberOrNull; + meeting_category: string; + title: string; } -export interface MeetingSubmissionGetResponseJsonApi { - id: string; - type: 'meeting-submissions'; - attributes: { - title: string; - date_created: Date; - author_name: string; - download_count: NumberOrNull; - meeting_category: string; - }; - links: { - download: StringOrNull; - }; +interface MeetingSubmissionLinksJsonApi { + download: StringOrNull; } diff --git a/src/app/features/meetings/services/meetings.service.ts b/src/app/features/meetings/services/meetings.service.ts index d3dc25201..d0f95e029 100644 --- a/src/app/features/meetings/services/meetings.service.ts +++ b/src/app/features/meetings/services/meetings.service.ts @@ -4,15 +4,15 @@ import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { searchPreferencesToJsonApiQueryParams } from '@osf/shared/helpers/search-pref-to-json-api-query-params.helper'; -import { JsonApiResponse, ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; import { SearchFilters } from '@osf/shared/models/search-filters.model'; import { JsonApiService } from '@osf/shared/services/json-api.service'; import { meetingSortFieldMap, meetingSubmissionSortFieldMap } from '../constants'; import { MeetingsMapper } from '../mappers'; import { - MeetingGetResponseJsonApi, - MeetingSubmissionGetResponseJsonApi, + MeetingResponseJsonApi, + MeetingsListResponseJsonApi, + MeetingSubmissionsListResponseJsonApi, MeetingSubmissionsWithPaging, MeetingsWithPaging, } from '../models'; @@ -38,7 +38,7 @@ export class MeetingsService { ); return this.jsonApiService - .get>(this.apiUrl, params) + .get(this.apiUrl, params) .pipe(map((response) => MeetingsMapper.fromMeetingsGetResponse(response))); } @@ -57,13 +57,13 @@ export class MeetingsService { ); return this.jsonApiService - .get>(`${this.apiUrl}${meetingId}/submissions/`, params) + .get(`${this.apiUrl}${meetingId}/submissions/`, params) .pipe(map((response) => MeetingsMapper.fromMeetingSubmissionGetResponse(response))); } getMeetingById(meetingId: string) { return this.jsonApiService - .get>(this.apiUrl + meetingId) + .get(this.apiUrl + meetingId) .pipe(map((response) => MeetingsMapper.fromMeetingGetResponse(response.data))); } } diff --git a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.html b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.html index 9fbe071e8..634051a81 100644 --- a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.html +++ b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.html @@ -2,7 +2,7 @@
@if (readonly()) {
- @if (existingRecord()?.attributes?.is_published) { + @if (existingRecord()?.isPublished) {

{{ 'project.metadata.addMetadata.publishedText' | translate }}

} @else {

{{ 'project.metadata.addMetadata.notPublishedText' | translate }}

@@ -58,14 +58,14 @@

{{ 'project.metadata.addMetadata.notPublishedTe } @else { { let component: CedarTemplateFormComponent; let fixture: ComponentFixture; - const mockTemplate = CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK; + const mockTemplate = MOCK_CEDAR_METADATA_TEMPLATE; + const mockRecord = MOCK_CEDAR_METADATA_RECORD; beforeEach(() => { TestBed.configureTestingModule({ @@ -43,10 +44,10 @@ describe('CedarTemplateFormComponent', () => { }); it('should set existingRecord input', () => { - fixture.componentRef.setInput('existingRecord', mockTemplate); + fixture.componentRef.setInput('existingRecord', mockRecord); fixture.detectChanges(); - expect(component.existingRecord()).toEqual(mockTemplate); + expect(component.existingRecord()).toEqual(mockRecord); }); it('should set readonly input', () => { diff --git a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts index 76bbf33ed..3072ae81d 100644 --- a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts +++ b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts @@ -30,8 +30,8 @@ import { CEDAR_CONFIG, CEDAR_VIEWER_CONFIG } from '../../constants'; import { CedarMetadataHelper } from '../../helpers'; import { CedarEditorElement, - CedarMetadataDataTemplateJsonApi, - CedarMetadataRecordData, + CedarMetadataRecordModel, + CedarMetadataTemplateModel, CedarRecordDataBinding, } from '../../models'; @@ -49,8 +49,8 @@ export class CedarTemplateFormComponent { changeTemplate = output(); toggleEditMode = output(); - template = input.required(); - existingRecord = input(null); + template = input.required(); + existingRecord = input(null); readonly = input(false); showEditButton = input(false); @@ -91,14 +91,14 @@ export class CedarTemplateFormComponent { constructor() { effect(() => { const tpl = this.template(); - if (tpl?.attributes?.template) { + if (tpl?.template) { this.initializeCedar(); } }); effect(() => { const record = this.existingRecord(); - this.schemaName.set(record?.embeds?.template.data.attributes.schema_name || ''); + this.schemaName.set(record?.schemaName || ''); if (record) { this.initializeCedar(); } @@ -106,7 +106,7 @@ export class CedarTemplateFormComponent { } private initializeCedar(): void { - const metadata = this.existingRecord()?.attributes?.metadata; + const metadata = this.existingRecord()?.metadata; const editor = this.cedarEditor()?.nativeElement; const viewer = this.cedarViewer()?.nativeElement; @@ -126,9 +126,9 @@ export class CedarTemplateFormComponent { } private initializeFormData(): void { - const template = this.template()?.attributes?.template; + const template = this.template()?.template; if (!template) return; - const metadata = this.existingRecord()?.attributes?.metadata; + const metadata = this.existingRecord()?.metadata; if (this.existingRecord()) { const structuredMetadata = CedarMetadataHelper.buildStructuredMetadata(metadata); this.formData.set(structuredMetadata); diff --git a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.html b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.html index 67e98e8f0..8332fca90 100644 --- a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.html +++ b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.html @@ -26,7 +26,7 @@
diff --git a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts index 7f0606a4a..8ecd342d8 100644 --- a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts +++ b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; -import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; +import { CollectionSubmission } from '@osf/shared/models/collections/collection-submissions.model'; import { MOCK_CEDAR_RECORD, diff --git a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.ts b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.ts index d1d5d0967..7ddc37f58 100644 --- a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.ts +++ b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.ts @@ -13,12 +13,13 @@ import { } from '@angular/core'; import { RouterLink } from '@angular/router'; -import { CEDAR_VIEWER_CONFIG } from '@osf/features/metadata/constants'; -import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData } from '@osf/features/metadata/models'; import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; -import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; +import { CollectionSubmission } from '@osf/shared/models/collections/collection-submissions.model'; import { CollectionStatusSeverityPipe } from '@osf/shared/pipes/collection-status-severity.pipe'; +import { CEDAR_VIEWER_CONFIG } from '../../constants'; +import { CedarMetadataRecordModel, CedarMetadataTemplateModel } from '../../models'; + @Component({ selector: 'osf-metadata-collection-item', imports: [TranslatePipe, Tag, Button, RouterLink, CollectionStatusSeverityPipe], @@ -32,8 +33,8 @@ export class MetadataCollectionItemComponent { readonly CollectionSubmissionReviewState = CollectionSubmissionReviewState; submission = input.required(); - cedarRecord = input(null); - cedarTemplate = input(null); + cedarRecord = input(null); + cedarTemplate = input(null); cedarViewerConfig = CEDAR_VIEWER_CONFIG; @@ -48,12 +49,12 @@ export class MetadataCollectionItemComponent { showCedarViewer = computed( () => !!this.cedarRecord() && - !!this.cedarTemplate()?.attributes?.template && + !!this.cedarTemplate()?.template && this.submission().reviewsState !== CollectionSubmissionReviewState.Removed ); cedarMetadata = computed(() => { const record = this.cedarRecord(); - return record?.attributes?.metadata ? (record.attributes.metadata as Record) : {}; + return record?.metadata ? (record.metadata as Record) : {}; }); } diff --git a/src/app/features/metadata/components/metadata-collections/metadata-collections.component.ts b/src/app/features/metadata/components/metadata-collections/metadata-collections.component.ts index eb46f0dda..8102cef73 100644 --- a/src/app/features/metadata/components/metadata-collections/metadata-collections.component.ts +++ b/src/app/features/metadata/components/metadata-collections/metadata-collections.component.ts @@ -5,9 +5,9 @@ import { Skeleton } from 'primeng/skeleton'; import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData } from '@osf/features/metadata/models'; -import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; +import { CollectionSubmission } from '@osf/shared/models/collections/collection-submissions.model'; +import { CedarMetadataRecordModel, CedarMetadataTemplateModel } from '../../models'; import { MetadataCollectionItemComponent } from '../metadata-collection-item/metadata-collection-item.component'; @Component({ @@ -20,14 +20,14 @@ import { MetadataCollectionItemComponent } from '../metadata-collection-item/met export class MetadataCollectionsComponent { projectSubmissions = input(null); isProjectSubmissionsLoading = input(false); - cedarRecords = input(null); - cedarTemplates = input(null); + cedarRecords = input(null); + cedarTemplates = input(null); cedarRecordByTemplateId = computed(() => { const records = this.cedarRecords(); return new Map( records?.flatMap((record) => { - const templateId = record.relationships?.template?.data?.id; + const templateId = record.templateId; return templateId ? [[templateId, record] as const] : []; }) ?? [] ); diff --git a/src/app/features/metadata/mappers/cedar-metadata.mapper.ts b/src/app/features/metadata/mappers/cedar-metadata.mapper.ts new file mode 100644 index 000000000..62541da8b --- /dev/null +++ b/src/app/features/metadata/mappers/cedar-metadata.mapper.ts @@ -0,0 +1,42 @@ +import { CedarMetadataRecordModel } from '../models/cedar-metadata-record.model'; +import { CedarMetadataRecordDataJsonApi } from '../models/cedar-metadata-record-json-api.model'; +import { CedarMetadataTemplateModel, PaginatedCedarTemplatesModel } from '../models/cedar-metadata-template.model'; +import { + CedarMetadataDataTemplateJsonApi, + CedarMetadataTemplateJsonApi, +} from '../models/cedar-metadata-template-json-api.model'; + +export class CedarMetadataMapper { + static fromTemplate(data: CedarMetadataDataTemplateJsonApi): CedarMetadataTemplateModel { + return { + id: data.id, + schemaName: data.attributes.schema_name, + isForCollections: data.attributes.is_for_collections, + active: data.attributes.active, + cedarId: data.attributes.cedar_id, + template: data.attributes.template, + }; + } + + static fromTemplatesResponse(response: CedarMetadataTemplateJsonApi): PaginatedCedarTemplatesModel { + return { + data: response.data.map((item) => this.fromTemplate(item)), + links: { + first: response.links?.first, + next: response.links?.next, + last: response.links?.last, + }, + }; + } + + static fromRecord(data: CedarMetadataRecordDataJsonApi): CedarMetadataRecordModel { + return { + id: data.id ?? '', + metadata: data.attributes.metadata, + isPublished: data.attributes.is_published, + templateId: data.relationships.template.data.id, + targetId: data.relationships.target.data.id, + schemaName: data.embeds?.template?.data?.attributes?.schema_name ?? '', + }; + } +} diff --git a/src/app/features/metadata/mappers/cedar-records.mapper.ts b/src/app/features/metadata/mappers/cedar-records.mapper.ts index afe093d10..164b4e7dd 100644 --- a/src/app/features/metadata/mappers/cedar-records.mapper.ts +++ b/src/app/features/metadata/mappers/cedar-records.mapper.ts @@ -1,11 +1,11 @@ -import { CedarMetadataRecord, CedarRecordDataBinding } from '../models'; +import { CedarMetadataRecordResponseJsonApi, CedarRecordDataBinding } from '../models'; export class CedarRecordsMapper { static toCedarRecordsPayload( data: CedarRecordDataBinding, resourceId: string, resourceType: string - ): CedarMetadataRecord { + ): CedarMetadataRecordResponseJsonApi { return { data: { type: 'cedar_metadata_records', diff --git a/src/app/features/metadata/mappers/index.ts b/src/app/features/metadata/mappers/index.ts index 5c0905874..7e6b2ee61 100644 --- a/src/app/features/metadata/mappers/index.ts +++ b/src/app/features/metadata/mappers/index.ts @@ -1,3 +1,4 @@ +export * from './cedar-metadata.mapper'; export * from './cedar-records.mapper'; export * from './metadata.mapper'; export * from './ror.mapper'; diff --git a/src/app/features/metadata/mappers/metadata.mapper.ts b/src/app/features/metadata/mappers/metadata.mapper.ts index eb6044cb3..8aeeb95d0 100644 --- a/src/app/features/metadata/mappers/metadata.mapper.ts +++ b/src/app/features/metadata/mappers/metadata.mapper.ts @@ -2,10 +2,10 @@ import { IdentifiersMapper } from '@osf/shared/mappers/identifiers.mapper'; import { LicensesMapper } from '@osf/shared/mappers/licenses.mapper'; import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; -import { CustomItemMetadataRecord, CustomMetadataJsonApi, MetadataJsonApi, MetadataModel } from '../models'; +import { CustomItemMetadataRecord, CustomMetadataDataJsonApi, MetadataDataJsonApi, MetadataModel } from '../models'; export class MetadataMapper { - static fromMetadataApiResponse(response: MetadataJsonApi): MetadataModel { + static fromMetadataApiResponse(response: MetadataDataJsonApi): MetadataModel { return { id: response.id, title: replaceBadEncodedChars(response.attributes.title), @@ -14,7 +14,9 @@ export class MetadataMapper { dateCreated: response.attributes.date_created, dateModified: response.attributes.date_modified, publicationDoi: response.attributes.article_doi, - license: LicensesMapper.fromLicenseDataJsonApi(response.embeds?.license?.data), + license: response.embeds?.license?.data + ? LicensesMapper.fromLicenseDataJsonApi(response.embeds?.license?.data) + : null, nodeLicense: response.attributes.node_license ? { copyrightHolders: response.attributes.node_license.copyright_holders || [], @@ -22,14 +24,14 @@ export class MetadataMapper { } : undefined, identifiers: IdentifiersMapper.fromJsonApi(response.embeds?.identifiers), - provider: response.embeds?.provider?.data.id, + provider: response.relationships?.provider?.data?.id, public: response.attributes.public, currentUserPermissions: response.attributes.current_user_permissions, registrationSupplement: response.attributes.registration_supplement, }; } - static fromCustomMetadataApiResponse(response: CustomMetadataJsonApi): Partial { + static fromCustomMetadataApiResponse(response: CustomMetadataDataJsonApi): Partial { return { language: response.attributes.language, resourceTypeGeneral: response.attributes.resource_type_general, diff --git a/src/app/features/metadata/metadata.component.ts b/src/app/features/metadata/metadata.component.ts index f82c3fec4..f5f77f5db 100644 --- a/src/app/features/metadata/metadata.component.ts +++ b/src/app/features/metadata/metadata.component.ts @@ -74,8 +74,8 @@ import { PublicationDoiDialogComponent } from './dialogs/publication-doi-dialog/ import { ResourceInformationDialogComponent } from './dialogs/resource-information-dialog/resource-information-dialog.component'; import { ResourceInfoTooltipComponent } from './dialogs/resource-tooltip-info/resource-tooltip-info.component'; import { - CedarMetadataDataTemplateJsonApi, - CedarMetadataRecordData, + CedarMetadataRecordModel, + CedarMetadataTemplateModel, CedarRecordDataBinding, DialogValueModel, } from './models'; @@ -133,8 +133,8 @@ export class MetadataComponent implements OnInit, OnDestroy { tabs = signal([]); selectedTab = signal('osf'); - selectedCedarRecord = signal(null); - selectedCedarTemplate = signal(null); + selectedCedarRecord = signal(null); + selectedCedarTemplate = signal(null); cedarFormReadonly = signal(true); resourceType = signal(this.activeRoute.parent?.snapshot.data['resourceType'] || ResourceType.Project); @@ -219,7 +219,7 @@ export class MetadataComponent implements OnInit, OnDestroy { const cedarTabs = records?.map((record) => ({ id: record.id || '', - label: record.embeds?.template?.data?.attributes?.schema_name || `Record ${record.id}`, + label: record.schemaName || `Record ${record.id}`, type: MetadataResourceEnum.CEDAR, })) || []; @@ -232,7 +232,7 @@ export class MetadataComponent implements OnInit, OnDestroy { const selectedRecord = this.selectedCedarRecord(); if (selectedRecord && templates?.data && !this.selectedCedarTemplate()) { - const templateId = selectedRecord.relationships?.template?.data?.id; + const templateId = selectedRecord.templateId; if (templateId) { const template = templates.data.find((t) => t.id === templateId); if (template) { @@ -574,7 +574,7 @@ export class MetadataComponent implements OnInit, OnDestroy { this.selectedCedarRecord.set(record); this.cedarFormReadonly.set(true); - const templateId = record.relationships?.template?.data?.id; + const templateId = record.templateId; if (templateId && templates?.data) { const template = templates.data.find((t) => t.id === templateId); diff --git a/src/app/features/metadata/models/cedar-metadata-attributes.model.ts b/src/app/features/metadata/models/cedar-metadata-attributes.model.ts new file mode 100644 index 000000000..6d2398d0b --- /dev/null +++ b/src/app/features/metadata/models/cedar-metadata-attributes.model.ts @@ -0,0 +1,4 @@ +export interface CedarMetadataAttributes { + '@context': Record; + [field: string]: unknown; +} diff --git a/src/app/features/metadata/models/cedar-metadata-record-json-api.model.ts b/src/app/features/metadata/models/cedar-metadata-record-json-api.model.ts new file mode 100644 index 000000000..785f0fc26 --- /dev/null +++ b/src/app/features/metadata/models/cedar-metadata-record-json-api.model.ts @@ -0,0 +1,27 @@ +import { Embed } from '@osf/shared/models/common/json-api/embeds.model'; +import { ToOneRelData } from '@osf/shared/models/common/json-api/relationships.model'; +import { DataResponse, ListResponse } from '@osf/shared/models/common/json-api/responses.model'; + +import { CedarMetadataAttributes } from './cedar-metadata-attributes.model'; +import { CedarMetadataDataTemplateJsonApi } from './cedar-metadata-template-json-api.model'; + +export type CedarMetadataRecordJsonApi = ListResponse; +export type CedarMetadataRecordResponseJsonApi = DataResponse; + +export interface CedarMetadataRecordDataJsonApi { + id?: string; + type?: string; + attributes: CedarMetadataRecordAttributesJsonApi; + embeds?: { + template: Embed; + }; + relationships: { + template: ToOneRelData; + target: ToOneRelData; + }; +} + +interface CedarMetadataRecordAttributesJsonApi { + metadata: CedarMetadataAttributes; + is_published: boolean; +} diff --git a/src/app/features/metadata/models/cedar-metadata-record.model.ts b/src/app/features/metadata/models/cedar-metadata-record.model.ts new file mode 100644 index 000000000..c3cf894de --- /dev/null +++ b/src/app/features/metadata/models/cedar-metadata-record.model.ts @@ -0,0 +1,16 @@ +import { CedarMetadataAttributes } from './cedar-metadata-attributes.model'; + +export interface CedarMetadataRecordModel { + id: string; + metadata: CedarMetadataAttributes; + isPublished: boolean; + templateId: string; + targetId: string; + schemaName: string; +} + +export interface CedarRecordDataBinding { + id: string; + data: CedarMetadataAttributes; + isPublished: boolean; +} diff --git a/src/app/features/metadata/models/cedar-metadata-template-json-api.model.ts b/src/app/features/metadata/models/cedar-metadata-template-json-api.model.ts new file mode 100644 index 000000000..b14b5f5b6 --- /dev/null +++ b/src/app/features/metadata/models/cedar-metadata-template-json-api.model.ts @@ -0,0 +1,19 @@ +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; + +import { CedarTemplate } from './cedar-template.model'; + +export type CedarMetadataTemplateJsonApi = ListResponse; + +export type CedarMetadataDataTemplateJsonApi = JsonApiResource< + 'cedar-metadata-templates', + CedarMetadataTemplateAttributesJsonApi +>; + +interface CedarMetadataTemplateAttributesJsonApi { + active: boolean; + cedar_id: string; + schema_name: string; + template: CedarTemplate; + is_for_collections: boolean; +} diff --git a/src/app/features/metadata/models/cedar-metadata-template.model.ts b/src/app/features/metadata/models/cedar-metadata-template.model.ts index 7b904d7ea..f385f479e 100644 --- a/src/app/features/metadata/models/cedar-metadata-template.model.ts +++ b/src/app/features/metadata/models/cedar-metadata-template.model.ts @@ -1,254 +1,17 @@ -import { MetaJsonApi, PaginationLinksJsonApi } from '@osf/shared/models/common/json-api.model'; +import { PaginationLinks } from '@osf/shared/models/common/json-api/links.model'; -export interface CedarMetadataDataTemplateJsonApi { - id: string; - type: 'cedar-metadata-templates'; - attributes: { - schema_name: string; - cedar_id: string; - template: CedarTemplate; - is_for_collections: boolean; - }; -} - -export const CEDAR_TEMPLATE_FIELD_TYPE = 'https://schema.metadatacenter.org/core/TemplateField'; -export const CEDAR_PROPERTIES_BASE_IRI = 'https://schema.metadatacenter.org/properties/'; - -export interface CedarTemplateField { - '@type': string; - _valueConstraints?: { - literals?: { label: string }[]; - multipleChoice?: boolean; - requiredValue?: boolean; - }; -} - -export interface CedarTemplateContextSchema { - properties: Record; -} - -export interface CedarTemplate { - '@id': string; - '@type': string; - type: string; - title: string; - description: string; - $schema: string; - '@context': CedarTemplateContext; - required: string[]; - properties: Record; - _ui: { - order: string[]; - propertyLabels: Record; - propertyDescriptions: Record; - }; -} - -export interface CedarTemplateContext { - pav: string; - xsd: string; - bibo: string; - oslc: string; - schema: string; - 'schema:name': { - '@type': string; - }; - 'pav:createdBy': { - '@type': string; - }; - 'pav:createdOn': { - '@type': string; - }; - 'oslc:modifiedBy': { - '@type': string; - }; - 'pav:lastUpdatedOn': { - '@type': string; - }; - 'schema:description': { - '@type': string; - }; -} - -export interface CedarMetadataTemplate { - id: string; - type: 'cedar-metadata-templates'; - attributes: { - schema_name: string; - cedar_id: string; - template: CedarTemplate; - is_for_collections: boolean; - }; -} - -export interface CedarTemplate { - '@id': string; - '@type': string; - type: string; - title: string; - $schema: string; - '@context': CedarTemplateContext; - required: string[]; - properties: Record; - _ui: { - order: string[]; - propertyLabels: Record; - propertyDescriptions: Record; - }; -} - -export interface CedarTemplateContext { - pav: string; - xsd: string; - bibo: string; - oslc: string; - schema: string; - 'schema:name': { - '@type': string; - }; - 'pav:createdBy': { - '@type': string; - }; - 'pav:createdOn': { - '@type': string; - }; - 'oslc:modifiedBy': { - '@type': string; - }; - 'pav:lastUpdatedOn': { - '@type': string; - }; - 'schema:description': { - '@type': string; - }; -} - -export interface CedarMetadataTemplateJsonApi { - data: CedarMetadataDataTemplateJsonApi[]; - links: PaginationLinksJsonApi; -} - -export interface FieldSchema { - type?: string; - format?: string; - title?: string; - description?: string; - maxLength?: number; - items?: FieldSchema; - properties?: Record; - required?: string[]; - _ui?: { - inputType?: string; - order?: string[]; - propertyLabels?: Record; - propertyDescriptions?: Record; - }; - 'schema:name'?: string; - 'schema:description'?: string; - '@id': string; -} - -export interface CedarFieldItem extends Record { - '@id'?: string; - '@type'?: string; - 'rdfs:label'?: string | null; - '@value'?: string; -} - -export interface CedarMetadataAttributes { - '@context': Record; - Constructs: CedarFieldItem[]; - Assessments: CedarFieldItem[]; - Organization: { - '@id': string; - '@context': { - OrganizationID: string; - OrganizationName: string; - }; - OrganizationID: Record; - OrganizationName: { - '@value': string; - }; - }[]; - 'Project Name': { - '@value': string; - }; - LDbaseWebsite: Record; - 'Project Methods': CedarFieldItem[]; - 'Participant Types': CedarFieldItem[]; - 'Special Populations': CedarFieldItem[]; - 'Developmental Design': Record; - LDbaseProjectEndDate: { - '@type': string; - '@value': string; - }; - 'Educational Curricula': CedarFieldItem[]; - LDbaseInvestigatorORCID: CedarFieldItem[]; - LDbaseProjectStartDates: { - '@type': string; - '@value': string; - }; - 'Educational Environments': Record; - LDbaseProjectDescription: { - '@value': string; - }; - - [key: string]: unknown; +import { CedarTemplate } from './cedar-template.model'; - LDbaseProjectContributors: { - '@value': string; - }[]; -} - -export interface CedarMetadataRecord { - data: CedarMetadataRecordData; -} - -export interface CedarRecordDataBinding { - data: CedarMetadataAttributes; +export interface CedarMetadataTemplateModel { id: string; - isPublished: boolean; -} - -export interface CedarMetadataRecordJsonApi { - data: CedarMetadataRecordData[]; - links: PaginationLinksJsonApi; - meta: MetaJsonApi; -} - -export interface CedarMetadataRecordData { - id?: string; - attributes: CedarMetadataRecordAttributes; - embeds?: { - template: { - data: { - attributes: { - active: boolean; - cedar_id: string; - schema_name: string; - }; - id: string; - }; - }; - }; - relationships: { - template: { - data: { - type: string; - id: string; - }; - }; - target: { - data: { - type: string; - id: string; - }; - }; - }; - type?: string; + schemaName: string; + isForCollections: boolean; + active: boolean; + cedarId: string; + template: CedarTemplate; } -export interface CedarMetadataRecordAttributes { - metadata: CedarMetadataAttributes; - is_published: boolean; +export interface PaginatedCedarTemplatesModel { + data: CedarMetadataTemplateModel[]; + links: PaginationLinks; } diff --git a/src/app/features/metadata/models/cedar-template.model.ts b/src/app/features/metadata/models/cedar-template.model.ts new file mode 100644 index 000000000..af6ad9619 --- /dev/null +++ b/src/app/features/metadata/models/cedar-template.model.ts @@ -0,0 +1,78 @@ +export const CEDAR_TEMPLATE_FIELD_TYPE = 'https://schema.metadatacenter.org/core/TemplateField'; +export const CEDAR_PROPERTIES_BASE_IRI = 'https://schema.metadatacenter.org/properties/'; + +export interface CedarTemplateField { + '@type': string; + _valueConstraints?: { + literals?: { label: string }[]; + multipleChoice?: boolean; + requiredValue?: boolean; + }; +} + +export interface CedarTemplateContextSchema { + properties: Record; +} + +export interface CedarTemplate { + '@id': string; + '@type': string; + type: string; + title: string; + description: string; + $schema: string; + '@context': CedarTemplateContext; + required: string[]; + properties: Record; + _ui: { + order: string[]; + propertyLabels: Record; + propertyDescriptions: Record; + }; +} + +interface CedarTemplateContext { + pav: string; + xsd: string; + bibo: string; + oslc: string; + schema: string; + 'schema:name': { + '@type': string; + }; + 'pav:createdBy': { + '@type': string; + }; + 'pav:createdOn': { + '@type': string; + }; + 'oslc:modifiedBy': { + '@type': string; + }; + 'pav:lastUpdatedOn': { + '@type': string; + }; + 'schema:description': { + '@type': string; + }; +} + +export interface FieldSchema { + type?: string; + format?: string; + title?: string; + description?: string; + maxLength?: number; + items?: FieldSchema; + properties?: Record; + required?: string[]; + _ui?: { + inputType?: string; + order?: string[]; + propertyLabels?: Record; + propertyDescriptions?: Record; + }; + 'schema:name'?: string; + 'schema:description'?: string; + '@id': string; +} diff --git a/src/app/features/metadata/models/index.ts b/src/app/features/metadata/models/index.ts index 30e978c95..f7cc29849 100644 --- a/src/app/features/metadata/models/index.ts +++ b/src/app/features/metadata/models/index.ts @@ -1,5 +1,10 @@ export * from './cedar-editor-element.model'; +export * from './cedar-metadata-attributes.model'; +export * from './cedar-metadata-record.model'; +export * from './cedar-metadata-record-json-api.model'; export * from './cedar-metadata-template.model'; +export * from './cedar-metadata-template-json-api.model'; +export * from './cedar-template.model'; export * from './dialog-value.model'; export * from './funding-dialog.model'; export * from './metadata.model'; diff --git a/src/app/features/metadata/models/metadata-json-api.model.ts b/src/app/features/metadata/models/metadata-json-api.model.ts index 802777461..ce91803fb 100644 --- a/src/app/features/metadata/models/metadata-json-api.model.ts +++ b/src/app/features/metadata/models/metadata-json-api.model.ts @@ -1,58 +1,59 @@ +import { ToOneRel } from '@osf/shared/models/common/json-api/relationships.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; import { UserPermissions } from '@shared/enums/user-permissions.enum'; -import { ApiData } from '@shared/models/common/json-api.model'; +import { Embed } from '@shared/models/common/json-api/embeds.model'; +import { ItemResponse } from '@shared/models/common/json-api/responses.model'; import { IdentifiersResponseJsonApi } from '@shared/models/identifiers/identifier-json-api.model'; -import { InstitutionsJsonApiResponse } from '@shared/models/institutions/institution-json-api.model'; import { LicenseDataJsonApi, LicenseRecordJsonApi } from '@shared/models/license/licenses-json-api.model'; -export interface MetadataJsonApiResponse { - data: MetadataJsonApi; +export type MetadataResponseJsonApi = ItemResponse; +export type CustomMetadataResponseJsonApi = ItemResponse; + +export interface MetadataDataJsonApi extends JsonApiResource<'metadata', MetadataAttributesJsonApi> { + relationships: MetadataRelationshipsJsonApi; + embeds?: MetadataEmbedsJsonApi; } -export type MetadataJsonApi = ApiData; +export type CustomMetadataDataJsonApi = JsonApiResource< + 'custom-item-metadata-records', + CustomMetadataAttributesJsonApi +>; export interface MetadataAttributesJsonApi { - title: string; - description: string; - tags: string[]; + article_doi?: string; + category?: string; + current_user_permissions: UserPermissions[]; date_created: string; date_modified: string; - article_doi?: string; + description: string; doi?: boolean; - category?: string; node_license?: LicenseRecordJsonApi; public?: boolean; registration_supplement?: string; - current_user_permissions: UserPermissions[]; -} - -interface MetadataEmbedsJsonApi { - affiliated_institutions: InstitutionsJsonApiResponse; - identifiers: IdentifiersResponseJsonApi; - license: { - data: LicenseDataJsonApi; - }; - provider?: { - data: { id: string; type: string; attributes: { name: string } }; - }; -} - -export interface CustomMetadataJsonApiResponse { - data: CustomMetadataJsonApi; + tags: string[]; + title: string; } -export type CustomMetadataJsonApi = ApiData; - -export interface CustomMetadataAttributesJsonApi { +interface CustomMetadataAttributesJsonApi { + funders?: FunderJsonApi[]; language?: string; resource_type_general?: string; - funders?: FunderJsonApi[]; } -export interface FunderJsonApi { - funder_name: string; - funder_identifier: string; - funder_identifier_type: string; +interface FunderJsonApi { award_number: string; - award_uri: string; award_title: string; + award_uri: string; + funder_identifier: string; + funder_identifier_type: string; + funder_name: string; +} + +interface MetadataEmbedsJsonApi { + identifiers: IdentifiersResponseJsonApi; + license: Embed; +} + +interface MetadataRelationshipsJsonApi { + provider: ToOneRel; } diff --git a/src/app/features/metadata/models/ror.model.ts b/src/app/features/metadata/models/ror.model.ts index 5775df7e4..a4ddede3a 100644 --- a/src/app/features/metadata/models/ror.model.ts +++ b/src/app/features/metadata/models/ror.model.ts @@ -1,4 +1,40 @@ -export interface RorAdmin { +export interface RorSearchResponse { + items: RorOrganization[]; + meta: RorMeta; + number_of_results: number; + time_taken: number; +} + +export interface RorFunderOption { + id: string; + name: string; +} + +export interface RorOrganization { + id: string; + admin: RorAdmin; + domains: string[]; + established: number | null; + external_ids: RorExternalId[]; + links: RorLink[]; + locations: RorLocation[]; + names: RorName[]; + relationships: RorRelationship[]; + status: 'active' | 'inactive' | 'withdrawn'; + types: ( + | 'education' + | 'healthcare' + | 'company' + | 'archive' + | 'nonprofit' + | 'government' + | 'facility' + | 'other' + | 'funder' + )[]; +} + +interface RorAdmin { created: { date: string; schema_version: string; @@ -9,18 +45,18 @@ export interface RorAdmin { }; } -export interface RorExternalId { +interface RorExternalId { all: string[]; preferred: string | null; type: 'grid' | 'fundref' | 'isni' | 'wikidata'; } -export interface RorLink { +interface RorLink { type: 'website' | 'wikipedia'; value: string; } -export interface RorGeonamesDetails { +interface RorGeonamesDetails { continent_code: string; continent_name: string; country_code: string; @@ -32,80 +68,32 @@ export interface RorGeonamesDetails { name: string; } -export interface RorLocation { +interface RorLocation { geonames_details: RorGeonamesDetails; geonames_id: number; } -export interface RorName { +interface RorName { lang: string | null; types: ('ror_display' | 'label' | 'alias' | 'acronym')[]; value: string; } -export interface RorRelationship { +interface RorRelationship { type: string; id: string; label: string; } -export interface RorOrganization { - id: string; - admin: RorAdmin; - domains: string[]; - established: number | null; - external_ids: RorExternalId[]; - links: RorLink[]; - locations: RorLocation[]; - names: RorName[]; - relationships: RorRelationship[]; - status: 'active' | 'inactive' | 'withdrawn'; - types: ( - | 'education' - | 'healthcare' - | 'company' - | 'archive' - | 'nonprofit' - | 'government' - | 'facility' - | 'other' - | 'funder' - )[]; -} - -export interface RorMetaCount { +interface RorMetaCount { id: string; title: string; count: number; } -export interface RorMeta { +interface RorMeta { types: RorMetaCount[]; countries: RorMetaCount[]; continents: RorMetaCount[]; statuses: RorMetaCount[]; } - -export interface RorSearchResponse { - items: RorOrganization[]; - meta: RorMeta; - number_of_results: number; - time_taken: number; -} - -export interface RorFunderOption { - id: string; - name: string; -} - -export interface RorDisplayData { - id: string; - displayName: string; - acronym?: string; - type: string; - country: string; - city: string; - established?: number; - website?: string; - status: string; -} diff --git a/src/app/features/metadata/pages/add-metadata/add-metadata.component.html b/src/app/features/metadata/pages/add-metadata/add-metadata.component.html index 21d47d7c1..ffffe2a3c 100644 --- a/src/app/features/metadata/pages/add-metadata/add-metadata.component.html +++ b/src/app/features/metadata/pages/add-metadata/add-metadata.component.html @@ -20,15 +20,16 @@

{{ 'project.metadata.addMetadata.selectTemplate' | translate }}

[class.metadata-item]="isMedium()" >
-

{{ meta.attributes.template.title }}

+

{{ meta.template.title }}

-

{{ meta.attributes.template.description }}

+

{{ meta.template.description }}

@if (hasExistingRecord(meta.id)) { {{ meta.attributes.template.title }}

> } @else { { let activatedRoute: Partial; let toastService: ReturnType; - const mockTemplate = CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK; - const mockRecord = MOCK_CEDAR_METADATA_RECORD_DATA; + const mockTemplate = MOCK_CEDAR_METADATA_TEMPLATE; + const mockRecord = MOCK_CEDAR_METADATA_RECORD; const mockCedarTemplates = { data: [mockTemplate], @@ -81,7 +80,7 @@ describe('AddMetadataComponent', () => { { selector: MetadataSelectors.getCedarTemplatesExcludingCollections, value: mockCedarTemplates }, { selector: MetadataSelectors.getCedarRecords, value: mockCedarRecords }, { selector: MetadataSelectors.getCedarTemplatesLoading, value: false }, - { selector: MetadataSelectors.getCedarRecord, value: { data: mockRecord } }, + { selector: MetadataSelectors.getCedarRecord, value: mockRecord }, ], }), ], diff --git a/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts b/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts index 20b1d67ef..ab9a7818d 100644 --- a/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts +++ b/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts @@ -25,7 +25,7 @@ import { IS_MEDIUM } from '@osf/shared/helpers/breakpoints.tokens'; import { ToastService } from '@osf/shared/services/toast.service'; import { CedarTemplateFormComponent } from '../../components/cedar-template-form/cedar-template-form.component'; -import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData, CedarRecordDataBinding } from '../../models'; +import { CedarMetadataRecordModel, CedarMetadataTemplateModel, CedarRecordDataBinding } from '../../models'; import { CreateCedarMetadataRecord, GetCedarMetadataRecords, @@ -53,8 +53,8 @@ export class AddMetadataComponent implements OnInit { private resourceId = ''; isEditMode = true; - selectedTemplate: CedarMetadataDataTemplateJsonApi | null = null; - existingRecord: CedarMetadataRecordData | null = null; + selectedTemplate: CedarMetadataTemplateModel | null = null; + existingRecord: CedarMetadataRecordModel | null = null; readonly cedarTemplates = select(MetadataSelectors.getCedarTemplatesExcludingCollections); readonly cedarRecords = select(MetadataSelectors.getCedarRecords); @@ -84,7 +84,7 @@ export class AddMetadataComponent implements OnInit { return record.id === recordId; }); if (existingRecord) { - const templateId = existingRecord.relationships.template.data.id; + const templateId = existingRecord.templateId; const matchingTemplate = cedarTemplatesData.find((template) => template.id === templateId); if (matchingTemplate) { @@ -111,7 +111,7 @@ export class AddMetadataComponent implements OnInit { this.actions.getCedarTemplates(); } - onSelect(template: CedarMetadataDataTemplateJsonApi): void { + onSelect(template: CedarMetadataTemplateModel): void { if (this.hasExistingRecord(template.id)) { return; } @@ -157,7 +157,7 @@ export class AddMetadataComponent implements OnInit { const records = this.cedarRecords(); if (!records) return false; - return records.some((record) => record.relationships.template.data.id === templateId); + return records.some((record) => record.templateId === templateId); } createRecordMetadata(data: CedarRecordDataBinding): void { @@ -192,7 +192,7 @@ export class AddMetadataComponent implements OnInit { } private navigateToRecord(resourceId: string, resourceType: ResourceType): void { - const recordId = this.cedarRecord()?.data.id; + const recordId = this.cedarRecord()?.id; if (resourceType === ResourceType.File) { this.router.navigate([resourceId]); } else { diff --git a/src/app/features/metadata/store/metadata.actions.ts b/src/app/features/metadata/store/metadata.actions.ts index cc3301bca..7755da809 100644 --- a/src/app/features/metadata/store/metadata.actions.ts +++ b/src/app/features/metadata/store/metadata.actions.ts @@ -2,7 +2,7 @@ import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { LicenseOptions } from '@shared/models/license/license.model'; import { - CedarMetadataRecordData, + CedarMetadataRecordModel, CedarRecordDataBinding, CustomItemMetadataRecord, MetadataAttributesJsonApi, @@ -98,5 +98,5 @@ export class UpdateCedarMetadataRecord { export class AddCedarMetadataRecordToState { static readonly type = '[Metadata] Add Cedar Metadata Record To State'; - constructor(public record: CedarMetadataRecordData) {} + constructor(public record: CedarMetadataRecordModel) {} } diff --git a/src/app/features/metadata/store/metadata.model.ts b/src/app/features/metadata/store/metadata.model.ts index a5e05dc5b..b7f614030 100644 --- a/src/app/features/metadata/store/metadata.model.ts +++ b/src/app/features/metadata/store/metadata.model.ts @@ -1,21 +1,19 @@ import { - CedarMetadataRecord, - CedarMetadataRecordData, - CedarMetadataTemplateJsonApi, + CedarMetadataRecordModel, CustomItemMetadataRecord, + MetadataModel, + PaginatedCedarTemplatesModel, + RorFunderOption, } from '@osf/features/metadata/models'; import { AsyncStateModel } from '@osf/shared/models/store/async-state.model'; -import { MetadataModel } from '../models'; -import { RorFunderOption } from '../models/ror.model'; - export interface MetadataStateModel { metadata: AsyncStateModel; customMetadata: AsyncStateModel; fundersList: AsyncStateModel; - cedarTemplates: AsyncStateModel; - cedarRecord: AsyncStateModel; - cedarRecords: AsyncStateModel; + cedarTemplates: AsyncStateModel; + cedarRecord: AsyncStateModel; + cedarRecords: AsyncStateModel; } export const METADATA_STATE_DEFAULTS: MetadataStateModel = { diff --git a/src/app/features/metadata/store/metadata.selectors.ts b/src/app/features/metadata/store/metadata.selectors.ts index ba86d6cf3..d21ce781b 100644 --- a/src/app/features/metadata/store/metadata.selectors.ts +++ b/src/app/features/metadata/store/metadata.selectors.ts @@ -52,7 +52,7 @@ export class MetadataSelectors { if (!templates) return null; return { ...templates, - data: templates.data.filter((t) => !t.attributes.is_for_collections), + data: templates.data.filter((t) => !t.isForCollections), }; } diff --git a/src/app/features/metadata/store/metadata.state.ts b/src/app/features/metadata/store/metadata.state.ts index 88ac11b71..d0079627c 100644 --- a/src/app/features/metadata/store/metadata.state.ts +++ b/src/app/features/metadata/store/metadata.state.ts @@ -7,7 +7,7 @@ import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@osf/shared/helpers/state-error.handler'; import { MetadataService } from '@osf/shared/services/metadata.service'; -import { CedarMetadataRecord, CedarMetadataRecordJsonApi, MetadataModel } from '../models'; +import { CedarMetadataRecordModel, MetadataModel } from '../models'; import { AddCedarMetadataRecordToState, @@ -179,10 +179,10 @@ export class MetadataState { }, }); return this.metadataService.getMetadataCedarRecords(action.resourceId, action.resourceType, action.url).pipe( - tap((response: CedarMetadataRecordJsonApi) => { + tap((records: CedarMetadataRecordModel[]) => { ctx.patchState({ cedarRecords: { - data: response.data, + data: records, error: null, isLoading: false, }, @@ -194,15 +194,15 @@ export class MetadataState { @Action(CreateCedarMetadataRecord) createCedarMetadataRecord(ctx: StateContext, action: CreateCedarMetadataRecord) { return this.metadataService.createMetadataCedarRecord(action.record, action.resourceId, action.resourceType).pipe( - tap((response: CedarMetadataRecord) => { + tap((record: CedarMetadataRecordModel) => { ctx.patchState({ cedarRecord: { - data: response, + data: record, error: null, isLoading: false, }, }); - ctx.dispatch(new AddCedarMetadataRecordToState(response.data)); + ctx.dispatch(new AddCedarMetadataRecordToState(record)); }) ); } @@ -212,10 +212,10 @@ export class MetadataState { return this.metadataService .updateMetadataCedarRecord(action.record, action.recordId, action.resourceId, action.resourceType) .pipe( - tap((response: CedarMetadataRecord) => { + tap((updatedRecord: CedarMetadataRecordModel) => { const state = ctx.getState(); const updatedRecords = state.cedarRecords.data.map((record) => - record.id === action.recordId ? response.data : record + record.id === action.recordId ? updatedRecord : record ); ctx.patchState({ cedarRecords: { diff --git a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts index 9d6f23010..1c9574854 100644 --- a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts +++ b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts @@ -4,7 +4,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; -import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/collections.model'; import { CollectionsSelectors } from '@osf/shared/stores/collections'; import { DateAgoPipe } from '@shared/pipes/date-ago.pipe'; @@ -24,7 +23,7 @@ describe('CollectionSubmissionItemComponent', () => { let mockRouter: ReturnType; let mockActivatedRoute: ReturnType; - const mockSubmission: CollectionSubmissionWithGuid = MOCK_COLLECTION_SUBMISSION_WITH_GUID; + const mockSubmission = MOCK_COLLECTION_SUBMISSION_WITH_GUID; const mockCollectionProvider = { id: 'provider-123', diff --git a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts index 013686142..6d57c01a2 100644 --- a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts +++ b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts @@ -11,7 +11,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { TruncatedTextComponent } from '@osf/shared/components/truncated-text/truncated-text.component'; -import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/collections.model'; +import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/collection-submissions.model'; import { DateAgoPipe } from '@osf/shared/pipes/date-ago.pipe'; import { CollectionsSelectors } from '@osf/shared/stores/collections'; diff --git a/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.ts b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.ts index b98c947df..4d45defc7 100644 --- a/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.ts +++ b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.ts @@ -8,7 +8,7 @@ import { GetCollectionSubmissionContributors, LoadMoreCollectionSubmissionContributors, } from '@osf/features/moderation/store/collections-moderation'; -import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/collections.model'; +import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/collection-submissions.model'; import { CollectionsModerationSelectors } from '../../store/collections-moderation'; import { CollectionSubmissionItemComponent } from '../collection-submission-item/collection-submission-item.component'; diff --git a/src/app/features/moderation/mappers/moderation.mapper.ts b/src/app/features/moderation/mappers/moderation.mapper.ts index 18af21c2e..160ae8844 100644 --- a/src/app/features/moderation/mappers/moderation.mapper.ts +++ b/src/app/features/moderation/mappers/moderation.mapper.ts @@ -1,7 +1,3 @@ -import { ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; -import { PaginatedData } from '@osf/shared/models/paginated-data.model'; -import { UserDataJsonApi } from '@osf/shared/models/user/user-json-api.model'; - import { AddModeratorType, ModeratorPermission } from '../enums'; import { ModeratorAddModel, ModeratorAddRequestModel, ModeratorDataJsonApi, ModeratorModel } from '../models'; @@ -25,23 +21,6 @@ export class ModerationMapper { }; } - static fromUsersWithPaginationGetResponse( - response: ResponseJsonApi - ): PaginatedData { - return { - data: response.data.map( - (user) => - ({ - id: user.id, - fullName: user.attributes.full_name, - permission: ModeratorPermission.Moderator, - }) as ModeratorAddModel - ), - totalCount: response.meta.total, - pageSize: response.meta.per_page, - }; - } - static toModeratorAddRequest(model: ModeratorAddModel, type = AddModeratorType.Search): ModeratorAddRequestModel { if (type === AddModeratorType.Search) { return { diff --git a/src/app/features/moderation/mappers/preprint-moderation.mapper.ts b/src/app/features/moderation/mappers/preprint-moderation.mapper.ts index 621d929c1..455b11257 100644 --- a/src/app/features/moderation/mappers/preprint-moderation.mapper.ts +++ b/src/app/features/moderation/mappers/preprint-moderation.mapper.ts @@ -1,11 +1,12 @@ +import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants/default-table-params.constants'; import { UserMapper } from '@osf/shared/mappers/user'; -import { ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; import { PaginatedData } from '@osf/shared/models/paginated-data.model'; import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { PreprintProviderModerationInfo, PreprintRelatedCountJsonApi, + PreprintReviewActionListResponseJsonApi, PreprintSubmissionResponseJsonApi, PreprintSubmissionWithdrawalResponseJsonApi, PreprintWithdrawalPaginatedData, @@ -39,12 +40,12 @@ export class PreprintModerationMapper { } static fromResponseWithPagination( - response: ResponseJsonApi + response: PreprintReviewActionListResponseJsonApi ): PaginatedData { return { data: response.data.map((x) => this.fromResponse(x)), totalCount: response.meta.total, - pageSize: response.meta.per_page, + pageSize: response.meta.per_page ?? DEFAULT_TABLE_PARAMS.rows, }; } @@ -73,7 +74,7 @@ export class PreprintModerationMapper { totalContributors: 0, })), totalCount: response.meta.total, - pageSize: response.meta.per_page, + pageSize: response.meta.per_page ?? DEFAULT_TABLE_PARAMS.rows, pendingCount: response.meta?.reviews_state_counts?.pending || 0, acceptedCount: response.meta?.reviews_state_counts?.accepted || 0, rejectedCount: response.meta?.reviews_state_counts?.rejected || 0, @@ -87,14 +88,14 @@ export class PreprintModerationMapper { return { data: response.data.map((x) => ({ id: x.id, - title: replaceBadEncodedChars(x.embeds.target.data.attributes.title), - preprintId: x.embeds.target.data.id, + title: replaceBadEncodedChars(x.embeds.target?.data?.attributes?.title), + preprintId: x.embeds.target?.data?.id, actions: [], contributors: [], totalContributors: 0, })), totalCount: response.meta.total, - pageSize: response.meta.per_page, + pageSize: response.meta.per_page ?? DEFAULT_TABLE_PARAMS.rows, pendingCount: response.meta.requests_state_counts.pending, acceptedCount: response.meta.requests_state_counts.accepted, rejectedCount: response.meta.requests_state_counts.rejected, diff --git a/src/app/features/moderation/mappers/registry-moderation.mapper.ts b/src/app/features/moderation/mappers/registry-moderation.mapper.ts index 70ab5fdb4..bab234290 100644 --- a/src/app/features/moderation/mappers/registry-moderation.mapper.ts +++ b/src/app/features/moderation/mappers/registry-moderation.mapper.ts @@ -1,3 +1,4 @@ +import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants/default-table-params.constants'; import { UserMapper } from '@osf/shared/mappers/user'; import { PaginatedData } from '@osf/shared/models/paginated-data.model'; import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; @@ -7,7 +8,7 @@ import { RegistryModeration, RegistryResponseJsonApi, ReviewAction, - ReviewActionsDataJsonApi, + ReviewActionDataJsonApi, } from '../models'; export class RegistryModerationMapper { @@ -29,11 +30,11 @@ export class RegistryModerationMapper { return { data: response.data.map((x) => this.fromResponse(x)), totalCount: response.meta.total, - pageSize: response.meta.per_page, + pageSize: response.meta.per_page ?? DEFAULT_TABLE_PARAMS.rows, }; } - static fromActionResponse(response: ReviewActionsDataJsonApi): ReviewAction { + static fromActionResponse(response: ReviewActionDataJsonApi): ReviewAction { const creator = UserMapper.getUserInfo(response.embeds?.creator); return { diff --git a/src/app/features/moderation/models/collection-submission-review-action-json-api.model.ts b/src/app/features/moderation/models/collection-submission-review-action-json-api.model.ts index cca102865..9374bcf29 100644 --- a/src/app/features/moderation/models/collection-submission-review-action-json-api.model.ts +++ b/src/app/features/moderation/models/collection-submission-review-action-json-api.model.ts @@ -1,25 +1,32 @@ +import { ToOneRelData } from '@osf/shared/models/common/json-api/relationships.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; import { UserDataErrorResponseJsonApi } from '@osf/shared/models/user/user-json-api.model'; -export interface CollectionSubmissionReviewActionJsonApi { - id: string; - type: 'collection-submission-actions'; - attributes: { - trigger: string; - comment: string; - from_state: string; - to_state: string; - date_created: string; - date_modified: string; - }; - embeds: { - creator: UserDataErrorResponseJsonApi; - }; - relationships: { - target: { - data: { - id: string; - type: 'collection-submission'; - }; - }; - }; +export type CollectionSubmissionReviewActionsListResponseJsonApi = + ListResponse; + +export interface CollectionSubmissionReviewActionJsonApi extends JsonApiResource< + 'collection-submission-actions', + CollectionSubmissionReviewActionAttributesJsonApi +> { + embeds: CollectionSubmissionReviewActionEmbedsJsonApi; + relationships: CollectionSubmissionReviewActionRelationshipsJsonApi; +} + +interface CollectionSubmissionReviewActionAttributesJsonApi { + comment: string; + date_created: string; + date_modified: string; + from_state: string; + to_state: string; + trigger: string; +} + +interface CollectionSubmissionReviewActionEmbedsJsonApi { + creator: UserDataErrorResponseJsonApi; +} + +interface CollectionSubmissionReviewActionRelationshipsJsonApi { + target: ToOneRelData<'collection-submission'>; } diff --git a/src/app/features/moderation/models/moderator-json-api.model.ts b/src/app/features/moderation/models/moderator-json-api.model.ts index 24449f115..fe2637360 100644 --- a/src/app/features/moderation/models/moderator-json-api.model.ts +++ b/src/app/features/moderation/models/moderator-json-api.model.ts @@ -1,31 +1,32 @@ -import { ApiData, MetaJsonApi, PaginationLinksJsonApi } from '@osf/shared/models/common/json-api.model'; +import { Embed } from '@osf/shared/models/common/json-api/embeds.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse, ListResponse } from '@osf/shared/models/common/json-api/responses.model'; import { UserDataJsonApi } from '@osf/shared/models/user/user-json-api.model'; -export interface ModeratorResponseJsonApi { - data: ModeratorDataJsonApi[]; - meta: MetaJsonApi; - links: PaginationLinksJsonApi; +export type ModeratorResponseJsonApi = ListResponse; +export type ModeratorItemResponseJsonApi = ItemResponse; + +export interface ModeratorDataJsonApi extends JsonApiResource<'moderators', ModeratorAttributesJsonApi> { + embeds: ModeratorEmbedsJsonApi; +} + +export interface ModeratorAddRequestModel { + attributes: ModeratorAddAttributesJsonApi; + type: 'moderators'; } -export type ModeratorDataJsonApi = ApiData; +interface ModeratorAddAttributesJsonApi { + email?: string; + full_name?: string; + id?: string; + permission_group: string; +} interface ModeratorAttributesJsonApi { full_name: string; - permission_group: 'moderator' | 'admin'; + permission_group: 'admin' | 'moderator'; } interface ModeratorEmbedsJsonApi { - user: { - data: UserDataJsonApi; - }; -} - -export interface ModeratorAddRequestModel { - type: 'moderators'; - attributes: { - id?: string; - permission_group: string; - full_name?: string; - email?: string; - }; + user: Embed; } diff --git a/src/app/features/moderation/models/preprint-related-count-json-api.model.ts b/src/app/features/moderation/models/preprint-related-count-json-api.model.ts index 7b78a3871..6acc16e83 100644 --- a/src/app/features/moderation/models/preprint-related-count-json-api.model.ts +++ b/src/app/features/moderation/models/preprint-related-count-json-api.model.ts @@ -1,21 +1,31 @@ +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse, ListResponse } from '@osf/shared/models/common/json-api/responses.model'; import { PreprintProviderAttributesJsonApi } from '@osf/shared/models/provider/preprints-provider-json-api.model'; -export interface PreprintRelatedCountJsonApi { - id: string; - attributes: PreprintProviderAttributesJsonApi; - relationships: { - preprints: { - links: { - related: { - meta: { - accepted: number; - initial: number; - pending: number; - rejected: number; - withdrawn: number; - }; - }; +export type PreprintRelatedCountListResponseJsonApi = ListResponse; +export type PreprintRelatedCountItemResponseJsonApi = ItemResponse; + +export interface PreprintRelatedCountJsonApi extends JsonApiResource< + 'preprint-providers', + PreprintProviderAttributesJsonApi +> { + relationships: PreprintRelatedCountRelationshipsJsonApi; +} + +interface PreprintRelatedCountRelationshipsJsonApi { + preprints: { + links: { + related: { + meta: PreprintRelationshipMetaJsonApi; }; }; }; } + +interface PreprintRelationshipMetaJsonApi { + accepted: number; + initial: number; + pending: number; + rejected: number; + withdrawn: number; +} diff --git a/src/app/features/moderation/models/preprint-review-action-json-api.model.ts b/src/app/features/moderation/models/preprint-review-action-json-api.model.ts index ef2ce5fbf..552394353 100644 --- a/src/app/features/moderation/models/preprint-review-action-json-api.model.ts +++ b/src/app/features/moderation/models/preprint-review-action-json-api.model.ts @@ -1,49 +1,28 @@ -import { UserAttributesJsonApi, UserDataErrorResponseJsonApi } from '@osf/shared/models/user/user-json-api.model'; +import { Embed } from '@osf/shared/models/common/json-api/embeds.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; +import { PreprintDataJsonApi } from '@osf/shared/models/preprints/preprint-json-api.model'; +import { PreprintProviderAttributesJsonApi } from '@osf/shared/models/provider/preprints-provider-json-api.model'; +import { UserDataErrorResponseJsonApi } from '@osf/shared/models/user/user-json-api.model'; -export interface ReviewActionJsonApi { - id: string; - type: 'review-actions'; - attributes: ReviewActionAttributesJsonApi; - embeds: { - creator: UserDataErrorResponseJsonApi; - target: { - data: PreprintModelJsonApi; - }; - provider: { - data: ProviderModelJsonApi; - }; - }; +export type PreprintReviewActionListResponseJsonApi = ListResponse; + +export interface ReviewActionJsonApi extends JsonApiResource<'review-actions', ReviewActionAttributesJsonApi> { + embeds: ReviewActionEmbedsJsonApi; } -export interface ReviewActionAttributesJsonApi { - trigger: string; +interface ReviewActionAttributesJsonApi { + auto: boolean; comment: string; - from_state: string; - to_state: string; date_created: string; date_modified: string; - auto: boolean; -} - -export interface UserModelJsonApi { - id: string; - type: 'users'; - attributes: UserAttributesJsonApi; -} - -export interface PreprintModelJsonApi { - id: string; - type: 'preprints'; - attributes: PreprintAttributesJsonApi; -} - -export interface PreprintAttributesJsonApi { - title: string; + from_state: string; + to_state: string; + trigger: string; } -export interface ProviderModelJsonApi { - id: string; - attributes: { - name: string; - }; +interface ReviewActionEmbedsJsonApi { + creator: UserDataErrorResponseJsonApi; + provider: Embed>; + target: Embed; } diff --git a/src/app/features/moderation/models/preprint-submission-json-api.model.ts b/src/app/features/moderation/models/preprint-submission-json-api.model.ts index d18b87089..51509db33 100644 --- a/src/app/features/moderation/models/preprint-submission-json-api.model.ts +++ b/src/app/features/moderation/models/preprint-submission-json-api.model.ts @@ -1,30 +1,27 @@ -import { JsonApiResponseWithMeta, MetaJsonApi } from '@osf/shared/models/common/json-api.model'; +import { ListMetaJsonApi } from '@osf/shared/models/common/json-api/meta.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; -export type PreprintSubmissionResponseJsonApi = JsonApiResponseWithMeta< - PreprintSubmissionDataJsonApi[], - PreprintSubmissionMetaJsonApi, - null ->; +export type PreprintSubmissionResponseJsonApi = ListResponse & { + meta: PreprintSubmissionMetaJsonApi; +}; -export interface PreprintSubmissionDataJsonApi { - id: string; - attributes: PreprintSubmissionAttributesJsonApi; -} +export type PreprintSubmissionDataJsonApi = JsonApiResource<'preprints', PreprintSubmissionAttributesJsonApi>; -interface PreprintSubmissionMetaJsonApi extends MetaJsonApi { +interface PreprintSubmissionMetaJsonApi extends ListMetaJsonApi { reviews_state_counts: { - pending: number; accepted: number; + pending: number; rejected: number; withdrawn: number; }; } interface PreprintSubmissionAttributesJsonApi { + embargo_end_date: string; + embargoed: boolean; id: string; - title: string; - reviews_state: string; public: boolean; - embargoed: boolean; - embargo_end_date: string; + reviews_state: string; + title: string; } diff --git a/src/app/features/moderation/models/preprint-withdrawal-submission-json-api.model.ts b/src/app/features/moderation/models/preprint-withdrawal-submission-json-api.model.ts index c68a0c336..5430734b3 100644 --- a/src/app/features/moderation/models/preprint-withdrawal-submission-json-api.model.ts +++ b/src/app/features/moderation/models/preprint-withdrawal-submission-json-api.model.ts @@ -1,40 +1,36 @@ -import { JsonApiResponseWithMeta, MetaJsonApi } from '@osf/shared/models/common/json-api.model'; +import { Embed } from '@osf/shared/models/common/json-api/embeds.model'; +import { ListMetaJsonApi } from '@osf/shared/models/common/json-api/meta.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; +import { PreprintAttributesJsonApi } from '@osf/shared/models/preprints/preprint-json-api.model'; import { UserDataErrorResponseJsonApi } from '@osf/shared/models/user/user-json-api.model'; -export type PreprintSubmissionWithdrawalResponseJsonApi = JsonApiResponseWithMeta< - PreprintWithdrawalSubmissionDataJsonApi[], - PreprintWithdrawalSubmissionMetaJsonApi, - null ->; +export type PreprintSubmissionWithdrawalResponseJsonApi = ListResponse & { + meta: PreprintWithdrawalSubmissionMetaJsonApi; +}; -export interface PreprintWithdrawalSubmissionDataJsonApi { - id: string; - attributes: PreprintWithdrawalSubmissionAttributesJsonApi; +export interface PreprintWithdrawalSubmissionDataJsonApi extends JsonApiResource< + 'preprint-withdrawal-submissions', + PreprintWithdrawalSubmissionAttributesJsonApi +> { embeds: PreprintWithdrawalSubmissionEmbedsJsonApi; } -interface PreprintWithdrawalSubmissionMetaJsonApi extends MetaJsonApi { +interface PreprintWithdrawalSubmissionMetaJsonApi extends ListMetaJsonApi { requests_state_counts: { - pending: number; accepted: number; + pending: number; rejected: number; }; } interface PreprintWithdrawalSubmissionAttributesJsonApi { comment: string; - machine_state: string; date_last_transitioned: string; + machine_state: string; } interface PreprintWithdrawalSubmissionEmbedsJsonApi { - target: { - data: { - id: string; - attributes: { - title: string; - }; - }; - }; creator: UserDataErrorResponseJsonApi; + target: Embed>; } diff --git a/src/app/features/moderation/models/registry-json-api.model.ts b/src/app/features/moderation/models/registry-json-api.model.ts index e8dd6c41e..1fbf11e9d 100644 --- a/src/app/features/moderation/models/registry-json-api.model.ts +++ b/src/app/features/moderation/models/registry-json-api.model.ts @@ -1,16 +1,14 @@ -import { ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; +import { ToManyRel } from '@osf/shared/models/common/json-api/relationships.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; import { RegistrationNodeAttributesJsonApi } from '@osf/shared/models/registration/registration-node-json-api.model'; -export type RegistryResponseJsonApi = ResponseJsonApi; +export type RegistryResponseJsonApi = ListResponse; -export interface RegistryDataJsonApi { - id: string; - attributes: RegistrationNodeAttributesJsonApi; +export interface RegistryDataJsonApi extends JsonApiResource<'registrations', RegistrationNodeAttributesJsonApi> { embeds: RegistryEmbedsJsonApi; } -export interface RegistryEmbedsJsonApi { - schema_responses: { - data: { id: string }[]; - }; +interface RegistryEmbedsJsonApi { + schema_responses: ToManyRel<'schema-responses'>; } diff --git a/src/app/features/moderation/models/review-action-json-api.model.ts b/src/app/features/moderation/models/review-action-json-api.model.ts index a8d5be563..0384b3046 100644 --- a/src/app/features/moderation/models/review-action-json-api.model.ts +++ b/src/app/features/moderation/models/review-action-json-api.model.ts @@ -1,11 +1,10 @@ -import { JsonApiResponse } from '@osf/shared/models/common/json-api.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; import { UserDataErrorResponseJsonApi } from '@osf/shared/models/user/user-json-api.model'; -export type ReviewActionsResponseJsonApi = JsonApiResponse; +export type ReviewActionsResponseJsonApi = ListResponse; -export interface ReviewActionsDataJsonApi { - id: string; - attributes: ReviewActionAttributesJsonApi; +export interface ReviewActionDataJsonApi extends JsonApiResource<'review-actions', ReviewActionAttributesJsonApi> { embeds: ReviewActionEmbedsJsonApi; } diff --git a/src/app/features/moderation/services/moderators.service.ts b/src/app/features/moderation/services/moderators.service.ts index 3a184913f..3e201bdb6 100644 --- a/src/app/features/moderation/services/moderators.service.ts +++ b/src/app/features/moderation/services/moderators.service.ts @@ -3,10 +3,10 @@ import { forkJoin, map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants/default-table-params.constants'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { parseSearchTotalCount } from '@osf/shared/helpers/search-total-count.helper'; import { MapResources } from '@osf/shared/mappers/search'; -import { JsonApiResponse } from '@osf/shared/models/common/json-api.model'; import { PaginatedData } from '@osf/shared/models/paginated-data.model'; import { IndexCardSearchResponseJsonApi } from '@osf/shared/models/search/index-card-search-json-api.model'; import { SearchUserDataModel } from '@osf/shared/models/user/search-user-data.model'; @@ -15,7 +15,13 @@ import { StringOrNull } from '@shared/helpers/types.helper'; import { AddModeratorType, ModeratorPermission } from '../enums'; import { ModerationMapper } from '../mappers'; -import { ModeratorAddModel, ModeratorDataJsonApi, ModeratorModel, ModeratorResponseJsonApi } from '../models'; +import { + ModeratorAddModel, + ModeratorDataJsonApi, + ModeratorItemResponseJsonApi, + ModeratorModel, + ModeratorResponseJsonApi, +} from '../models'; @Injectable({ providedIn: 'root', @@ -64,7 +70,7 @@ export class ModeratorsService { map((response) => ({ data: ModerationMapper.getModerators(response.data), totalCount: response.meta.total, - pageSize: response.meta.per_page, + pageSize: response.meta.per_page ?? DEFAULT_TABLE_PARAMS.rows, })) ); } @@ -76,7 +82,7 @@ export class ModeratorsService { const moderatorData = { data: ModerationMapper.toModeratorAddRequest(data, type) }; return this.jsonApiService - .post>(baseUrl, moderatorData) + .post(baseUrl, moderatorData) .pipe(map((moderator) => ModerationMapper.fromModeratorResponse(moderator.data))); } diff --git a/src/app/features/moderation/services/preprint-moderation.service.ts b/src/app/features/moderation/services/preprint-moderation.service.ts index a41c9906f..61672912a 100644 --- a/src/app/features/moderation/services/preprint-moderation.service.ts +++ b/src/app/features/moderation/services/preprint-moderation.service.ts @@ -3,7 +3,6 @@ import { catchError, forkJoin, map, Observable, of, switchMap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; -import { ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; import { PaginatedData } from '@osf/shared/models/paginated-data.model'; import { JsonApiService } from '@osf/shared/services/json-api.service'; @@ -11,13 +10,14 @@ import { PreprintSubmissionsSort } from '../enums'; import { PreprintModerationMapper, RegistryModerationMapper } from '../mappers'; import { PreprintProviderModerationInfo, - PreprintRelatedCountJsonApi, + PreprintRelatedCountItemResponseJsonApi, + PreprintRelatedCountListResponseJsonApi, + PreprintReviewActionListResponseJsonApi, PreprintReviewActionModel, PreprintSubmissionResponseJsonApi, PreprintSubmissionWithdrawalResponseJsonApi, PreprintWithdrawalPaginatedData, ReviewAction, - ReviewActionJsonApi, ReviewActionsResponseJsonApi, } from '../models'; import { PreprintSubmissionPaginatedData } from '../models/preprint-submission.model'; @@ -37,7 +37,7 @@ export class PreprintModerationService { const baseUrl = `${this.apiUrl}/providers/preprints/?filter[permissions]=view_actions,set_up_moderation`; return this.jsonApiService - .get>(baseUrl) + .get(baseUrl) .pipe(map((response) => response.data.map((x) => PreprintModerationMapper.fromPreprintRelatedCounts(x)))); } @@ -45,7 +45,7 @@ export class PreprintModerationService { const baseUrl = `${this.apiUrl}/providers/preprints/${id}/?related_counts=true`; return this.jsonApiService - .get>(baseUrl) + .get(baseUrl) .pipe(map((response) => PreprintModerationMapper.fromPreprintRelatedCounts(response.data))); } @@ -53,7 +53,7 @@ export class PreprintModerationService { const baseUrl = `${this.apiUrl}/actions/reviews/?embed=provider&embed=target&page=${page}`; return this.jsonApiService - .get>(baseUrl) + .get(baseUrl) .pipe(map((response) => PreprintModerationMapper.fromResponseWithPagination(response))); } diff --git a/src/app/features/moderation/services/provider-subscription.service.ts b/src/app/features/moderation/services/provider-subscription.service.ts index 21277c50b..378022180 100644 --- a/src/app/features/moderation/services/provider-subscription.service.ts +++ b/src/app/features/moderation/services/provider-subscription.service.ts @@ -6,9 +6,11 @@ import { ENVIRONMENT } from '@core/provider/environment.provider'; import { SubscriptionFrequency } from '@osf/shared/enums/subscriptions/subscription-frequency.enum'; import { SubscriptionType } from '@osf/shared/enums/subscriptions/subscription-type.enum'; import { NotificationSubscriptionMapper } from '@osf/shared/mappers/notification-subscription.mapper'; -import { JsonApiResponse } from '@osf/shared/models/common/json-api.model'; import { NotificationSubscription } from '@osf/shared/models/notifications/notification-subscription.model'; -import { NotificationSubscriptionGetResponseJsonApi } from '@osf/shared/models/notifications/notification-subscription-json-api.model'; +import { + NotificationSubscriptionDataJsonApi, + NotificationSubscriptionsListResponseJsonApi, +} from '@osf/shared/models/notifications/notification-subscription-json-api.model'; import { JsonApiService } from '@osf/shared/services/json-api.service'; @Injectable({ @@ -24,9 +26,7 @@ export class ProviderSubscriptionService { getProviderSubscriptions(providerType: string, providerId: string): Observable { return this.jsonApiService - .get< - JsonApiResponse - >(this.providerUrl(providerType, providerId)) + .get(this.providerUrl(providerType, providerId)) .pipe( map((responses) => responses.data.map((response) => NotificationSubscriptionMapper.fromGetResponse(response))) ); @@ -47,7 +47,7 @@ export class ProviderSubscriptionService { }; return this.jsonApiService - .patch( + .patch( `${this.providerUrl(providerType, providerId)}${subscriptionId}/`, request ) diff --git a/src/app/features/moderation/store/collections-moderation/collections-moderation.model.ts b/src/app/features/moderation/store/collections-moderation/collections-moderation.model.ts index daa7cb0af..e5ab11a96 100644 --- a/src/app/features/moderation/store/collections-moderation/collections-moderation.model.ts +++ b/src/app/features/moderation/store/collections-moderation/collections-moderation.model.ts @@ -1,5 +1,5 @@ import { CollectionSubmissionReviewAction } from '@osf/features/moderation/models'; -import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/collections.model'; +import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/collection-submissions.model'; import { AsyncStateModel } from '@osf/shared/models/store/async-state.model'; import { AsyncStateWithTotalCount } from '@osf/shared/models/store/async-state-with-total-count.model'; diff --git a/src/app/features/my-projects/my-projects.component.ts b/src/app/features/my-projects/my-projects.component.ts index 8fd1df271..d541a1d69 100644 --- a/src/app/features/my-projects/my-projects.component.ts +++ b/src/app/features/my-projects/my-projects.component.ts @@ -32,7 +32,7 @@ import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants/default-table-params.constants'; import { SortOrder } from '@osf/shared/enums/sort-order.enum'; import { IS_MEDIUM } from '@osf/shared/helpers/breakpoints.tokens'; -import { MyResourcesItem } from '@osf/shared/models/my-resources/my-resources.model'; +import { MyResourcesItem } from '@osf/shared/models/my-resources/my-resources-item.model'; import { MyResourcesSearchFilters } from '@osf/shared/models/my-resources/my-resources-search-filters.model'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ProjectRedirectDialogService } from '@osf/shared/services/project-redirect-dialog.service'; diff --git a/src/app/features/my-projects/services/project-download-options.service.spec.ts b/src/app/features/my-projects/services/project-download-options.service.spec.ts index 6b86d22c6..f04ceb8f4 100644 --- a/src/app/features/my-projects/services/project-download-options.service.spec.ts +++ b/src/app/features/my-projects/services/project-download-options.service.spec.ts @@ -53,7 +53,9 @@ describe('ProjectDownloadOptionsService', () => { beforeEach(() => { filesService = FilesServiceMock.simple(); - filesService.getRootFolders.mockReturnValue(of({ files: [osfFolder, googleDriveFolder] })); + filesService.getRootFolders.mockReturnValue( + of({ data: [osfFolder, googleDriveFolder], totalCount: 2, pageSize: 10 }) + ); filesService.getConfiguredStorageAddons.mockReturnValue(of([googleDriveAddon])); filesService.getFilesWithoutFiltering.mockReturnValue(of({ data: [], totalCount: 2, pageSize: 10 })); @@ -111,7 +113,9 @@ describe('ProjectDownloadOptionsService', () => { ...osfFolder, links: osfLinksWithoutFilesLink, } as FileFolderModel; - filesService.getRootFolders.mockReturnValue(of({ files: [osfFolderWithoutFilesLink, googleDriveFolder] })); + filesService.getRootFolders.mockReturnValue( + of({ data: [osfFolderWithoutFilesLink, googleDriveFolder], totalCount: 2, pageSize: 10 }) + ); await firstValueFrom(service.loadOptions('project-1')); diff --git a/src/app/features/my-projects/services/project-download-options.service.ts b/src/app/features/my-projects/services/project-download-options.service.ts index a170e4563..555bb70a3 100644 --- a/src/app/features/my-projects/services/project-download-options.service.ts +++ b/src/app/features/my-projects/services/project-download-options.service.ts @@ -61,7 +61,7 @@ export class ProjectDownloadOptionsService { configuredAddons: this.filesService.getConfiguredStorageAddons(projectId), }).pipe( switchMap(({ rootFoldersResponse, configuredAddons }) => - this.buildOptions(rootFoldersResponse.files, configuredAddons) + this.buildOptions(rootFoldersResponse.data, configuredAddons) ), map((options) => { this.optionsCache.set(projectId, options); diff --git a/src/app/features/preprints/mappers/preprint-providers.mapper.ts b/src/app/features/preprints/mappers/preprint-providers.mapper.ts index c9fa25ac2..bbb1eb94d 100644 --- a/src/app/features/preprints/mappers/preprint-providers.mapper.ts +++ b/src/app/features/preprints/mappers/preprint-providers.mapper.ts @@ -1,6 +1,6 @@ import { replaceBadEncodedChars } from '@osf/shared/helpers/format-bad-encoding.helper'; -import { BrandDataJsonApi } from '@osf/shared/models/brand/brand.json-api.model'; import { BrandModel } from '@osf/shared/models/brand/brand.model'; +import { BrandDataJsonApi } from '@osf/shared/models/brand/brand-json-api.model'; import { SubjectModel } from '@osf/shared/models/subject/subject.model'; import { SubjectDataJsonApi } from '@osf/shared/models/subject/subjects-json-api.model'; @@ -8,7 +8,7 @@ import { PreprintProviderDetails, PreprintProviderDetailsJsonApi, PreprintProvid export class PreprintProvidersMapper { static fromPreprintProviderDetailsGetResponse(response: PreprintProviderDetailsJsonApi): PreprintProviderDetails { - const brandRaw = response.embeds!.brand?.data; + const brandRaw = response.embeds?.brand?.data; return { id: response.id, name: replaceBadEncodedChars(response.attributes.name), @@ -32,7 +32,7 @@ export class PreprintProvidersMapper { }; } - static parseBrand(brandRaw: BrandDataJsonApi): BrandModel { + static parseBrand(brandRaw: BrandDataJsonApi | undefined): BrandModel { if (!brandRaw) { return { primaryColor: 'var(--osf-provider-primary-color)', diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index ed01f2be9..952817c24 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -2,18 +2,14 @@ import { StringOrNull } from '@osf/shared/helpers/types.helper'; import { ContributorsMapper } from '@osf/shared/mappers/contributors'; import { IdentifiersMapper } from '@osf/shared/mappers/identifiers.mapper'; import { LicensesMapper } from '@osf/shared/mappers/licenses.mapper'; -import { ApiData, JsonApiResponseWithMeta, ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; +import { + PreprintDataJsonApi, + PreprintResponseJsonApi, + PreprintsListResponseJsonApi, +} from '@osf/shared/models/preprints/preprint-json-api.model'; import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; -import { - PreprintAttributesJsonApi, - PreprintEmbedsJsonApi, - PreprintLinksJsonApi, - PreprintMetaJsonApi, - PreprintModel, - PreprintRelationshipsJsonApi, - PreprintShortInfoWithTotalCount, -} from '../models'; +import { PreprintModel, PreprintShortInfoWithTotalCount } from '../models'; export class PreprintsMapper { static toCreatePayload(title: string, abstract: string, providerId: string) { @@ -36,9 +32,7 @@ export class PreprintsMapper { }; } - static fromPreprintJsonApi( - response: ApiData - ): PreprintModel { + static fromPreprintJsonApi(response: PreprintDataJsonApi): PreprintModel { return { id: response.id, dateCreated: response.attributes.date_created, @@ -81,21 +75,15 @@ export class PreprintsMapper { whyNoPrereg: response.attributes.why_no_prereg ? replaceBadEncodedChars(response.attributes.why_no_prereg) : null, preregLinks: response.attributes.prereg_links, preregLinkInfo: response.attributes.prereg_link_info, - preprintDoiLink: response.links.preprint_doi, - articleDoiLink: response.links.doi, + preprintDoiLink: response.links?.preprint_doi ?? '', + articleDoiLink: response.links?.doi ?? '', embeddedLicense: null, providerId: response.relationships?.provider?.data?.id, defaultLicenseId: response.attributes.default_license_id, }; } - static fromPreprintWithEmbedsJsonApi( - response: JsonApiResponseWithMeta< - ApiData, - PreprintMetaJsonApi, - null - > - ): PreprintModel { + static fromPreprintWithEmbedsJsonApi(response: PreprintResponseJsonApi): PreprintModel { const data = response.data; const links = response.data.links; const relationships = response?.data?.relationships; @@ -142,10 +130,12 @@ export class PreprintsMapper { whyNoPrereg: data.attributes.why_no_prereg ? replaceBadEncodedChars(data.attributes.why_no_prereg) : null, preregLinks: data.attributes.prereg_links, preregLinkInfo: data.attributes.prereg_link_info, - embeddedLicense: LicensesMapper.fromLicenseDataJsonApi(data.embeds?.license?.data), + embeddedLicense: data.embeds?.license?.data + ? LicensesMapper.fromLicenseDataJsonApi(data.embeds?.license?.data) + : null, identifiers: IdentifiersMapper.fromJsonApi(data.embeds?.identifiers), - preprintDoiLink: links.preprint_doi, - articleDoiLink: links.doi, + preprintDoiLink: links?.preprint_doi ?? '', + articleDoiLink: links?.doi ?? '', providerId: relationships?.provider?.data?.id, }; } @@ -170,11 +160,7 @@ export class PreprintsMapper { }; } - static fromMyPreprintJsonApi( - response: ResponseJsonApi< - ApiData[] - > - ): PreprintShortInfoWithTotalCount { + static fromMyPreprintJsonApi(response: PreprintsListResponseJsonApi): PreprintShortInfoWithTotalCount { return { data: response.data.map((preprintData) => ({ id: preprintData.id, diff --git a/src/app/features/preprints/models/index.ts b/src/app/features/preprints/models/index.ts index 5c556d995..e831133e1 100644 --- a/src/app/features/preprints/models/index.ts +++ b/src/app/features/preprints/models/index.ts @@ -1,5 +1,4 @@ export * from './preprint.model'; -export * from './preprint-json-api.model'; export * from './preprint-licenses-json-api.model'; export * from './preprint-provider.model'; export * from './preprint-provider-json-api.model'; diff --git a/src/app/features/preprints/models/preprint-licenses-json-api.model.ts b/src/app/features/preprints/models/preprint-licenses-json-api.model.ts index b1b1ba6f8..ae507a659 100644 --- a/src/app/features/preprints/models/preprint-licenses-json-api.model.ts +++ b/src/app/features/preprints/models/preprint-licenses-json-api.model.ts @@ -1,21 +1,18 @@ +import { ToOneRelData } from '@osf/shared/models/common/json-api/relationships.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { DataResponse } from '@osf/shared/models/common/json-api/responses.model'; import { LicenseRecordJsonApi } from '@osf/shared/models/license/licenses-json-api.model'; -export interface PreprintLicenseRelationshipJsonApi { - id: string; - type: 'licenses'; +export type PreprintLicensePayloadJsonApi = DataResponse; + +interface PreprintLicenseDataJsonApi extends JsonApiResource<'preprints', PreprintLicenseAttributesJsonApi> { + relationships: PreprintLicenseRelationshipsJsonApi; +} + +interface PreprintLicenseAttributesJsonApi { + license_record?: LicenseRecordJsonApi; } -export interface PreprintLicensePayloadJsonApi { - data: { - type: 'preprints'; - id: string; - relationships: { - license: { - data: PreprintLicenseRelationshipJsonApi; - }; - }; - attributes: { - license_record?: LicenseRecordJsonApi; - }; - }; +interface PreprintLicenseRelationshipsJsonApi { + license: ToOneRelData<'licenses'>; } diff --git a/src/app/features/preprints/models/preprint-provider-json-api.model.ts b/src/app/features/preprints/models/preprint-provider-json-api.model.ts index e71cc5ee2..0ed0cb67f 100644 --- a/src/app/features/preprints/models/preprint-provider-json-api.model.ts +++ b/src/app/features/preprints/models/preprint-provider-json-api.model.ts @@ -1,41 +1,50 @@ import { ReviewPermissions } from '@osf/shared/enums/review-permissions.enum'; import { StringOrNull } from '@osf/shared/helpers/types.helper'; -import { BrandDataJsonApi } from '@osf/shared/models/brand/brand.json-api.model'; +import { BrandDataJsonApi } from '@osf/shared/models/brand/brand-json-api.model'; +import { Embed } from '@osf/shared/models/common/json-api/embeds.model'; +import { ResourceLinksJsonApi } from '@osf/shared/models/common/json-api/links.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ItemResponse, ListResponse } from '@osf/shared/models/common/json-api/responses.model'; import { ProviderReviewsWorkflow } from '../enums'; import { PreprintWord } from './preprint-provider.model'; -export interface PreprintProviderDetailsJsonApi { - id: string; - type: 'preprint-providers'; - attributes: { - name: string; - description: string; - advisory_board: StringOrNull; - example: string; - domain: string; - footer_links: string; - preprint_word: PreprintWord; - permissions: ReviewPermissions[]; - assets: { - wide_white: string; - square_color_no_transparent: string; - favicon: string; - }; - allow_submissions: boolean; - assertions_enabled: boolean; - reviews_workflow: ProviderReviewsWorkflow | null; - facebook_app_id: StringOrNull; - reviews_comments_private: StringOrNull; - reviews_comments_anonymous: StringOrNull; - }; - embeds?: { - brand: { - data: BrandDataJsonApi; - }; - }; - links: { - iri: string; - }; +export type PreprintProviderResponseJsonApi = ItemResponse; +export type PreprintProvidersListResponseJsonApi = ListResponse; + +export interface PreprintProviderDetailsJsonApi extends JsonApiResource< + 'preprint-providers', + PreprintProviderAttributesJsonApi +> { + embeds?: PreprintProviderEmbedsJsonApi; + links: ResourceLinksJsonApi; +} + +interface PreprintProviderAttributesJsonApi { + advisory_board: StringOrNull; + allow_submissions: boolean; + assertions_enabled: boolean; + assets: PreprintProviderAssetsJsonApi; + description: string; + domain: string; + example: string; + facebook_app_id: StringOrNull; + footer_links: string; + name: string; + permissions: ReviewPermissions[]; + preprint_word: PreprintWord; + reviews_comments_anonymous: StringOrNull; + reviews_comments_private: StringOrNull; + reviews_workflow: ProviderReviewsWorkflow | null; +} + +interface PreprintProviderAssetsJsonApi { + favicon: string; + square_color_no_transparent: string; + wide_white: string; +} + +interface PreprintProviderEmbedsJsonApi { + brand?: Embed; } diff --git a/src/app/features/preprints/models/preprint-request-action-json-api.model.ts b/src/app/features/preprints/models/preprint-request-action-json-api.model.ts index 4c9e53bef..777fbb899 100644 --- a/src/app/features/preprints/models/preprint-request-action-json-api.model.ts +++ b/src/app/features/preprints/models/preprint-request-action-json-api.model.ts @@ -1,24 +1,25 @@ -import { JsonApiResponse } from '@osf/shared/models/common/json-api.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; import { UserDataErrorResponseJsonApi } from '@osf/shared/models/user/user-json-api.model'; -export type PreprintRequestActionsJsonApiResponse = JsonApiResponse; +export type PreprintRequestActionsJsonApiResponse = ListResponse; -export interface PreprintRequestActionDataJsonApi { - id: string; - type: 'preprint_request_actions'; - attributes: PreprintRequestActionsAttributesJsonApi; - embeds: PreprintRequestEmbedsJsonApi; +export interface PreprintRequestActionDataJsonApi extends JsonApiResource< + 'preprint_request_actions', + PreprintRequestActionAttributesJsonApi +> { + embeds: PreprintRequestActionEmbedsJsonApi; } -interface PreprintRequestActionsAttributesJsonApi { - trigger: string; +interface PreprintRequestActionAttributesJsonApi { comment: string; - from_state: string; - to_state: string; date_created: Date; date_modified: Date; + from_state: string; + to_state: string; + trigger: string; } -interface PreprintRequestEmbedsJsonApi { +interface PreprintRequestActionEmbedsJsonApi { creator: UserDataErrorResponseJsonApi; } diff --git a/src/app/features/preprints/models/preprint-request-json-api.model.ts b/src/app/features/preprints/models/preprint-request-json-api.model.ts index fca82bcde..0d406799b 100644 --- a/src/app/features/preprints/models/preprint-request-json-api.model.ts +++ b/src/app/features/preprints/models/preprint-request-json-api.model.ts @@ -1,23 +1,25 @@ -import { PreprintRequestMachineState, PreprintRequestType } from '@osf/features/preprints/enums'; -import { JsonApiResponse } from '@osf/shared/models/common/json-api.model'; +import { JsonApiResource } from '@osf/shared/models/common/json-api/resource.model'; +import { ListResponse } from '@osf/shared/models/common/json-api/responses.model'; import { UserDataErrorResponseJsonApi } from '@osf/shared/models/user/user-json-api.model'; -export type PreprintRequestsJsonApiResponse = JsonApiResponse; +import { PreprintRequestMachineState, PreprintRequestType } from '../enums'; -export interface PreprintRequestDataJsonApi { - id: string; - type: 'preprint_requests'; - attributes: PreprintRequestAttributesJsonApi; +export type PreprintRequestsJsonApiResponse = ListResponse; + +export interface PreprintRequestDataJsonApi extends JsonApiResource< + 'preprint_requests', + PreprintRequestAttributesJsonApi +> { embeds: PreprintRequestEmbedsJsonApi; } interface PreprintRequestAttributesJsonApi { - request_type: PreprintRequestType; - machine_state: PreprintRequestMachineState; comment: string; created: Date; - modified: Date; date_last_transitioned: Date; + machine_state: PreprintRequestMachineState; + modified: Date; + request_type: PreprintRequestType; } interface PreprintRequestEmbedsJsonApi { diff --git a/src/app/features/preprints/services/preprint-files.service.ts b/src/app/features/preprints/services/preprint-files.service.ts index 0f0de7945..804f32e47 100644 --- a/src/app/features/preprints/services/preprint-files.service.ts +++ b/src/app/features/preprints/services/preprint-files.service.ts @@ -4,22 +4,17 @@ import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { PreprintsMapper } from '@osf/features/preprints/mappers'; -import { - PreprintAttributesJsonApi, - PreprintFilesLinks, - PreprintLinksJsonApi, - PreprintModel, - PreprintRelationshipsJsonApi, -} from '@osf/features/preprints/models'; -import { ApiData } from '@osf/shared/models/common/json-api.model'; import { FileFolderModel } from '@osf/shared/models/files/file-folder.model'; import { FileFolderResponseJsonApi, FileFoldersResponseJsonApi, } from '@osf/shared/models/files/file-folder-json-api.model'; +import { PreprintDataJsonApi } from '@osf/shared/models/preprints/preprint-json-api.model'; import { JsonApiService } from '@osf/shared/services/json-api.service'; import { FilesMapper } from '@shared/mappers/files/files.mapper'; +import { PreprintFilesLinks, PreprintModel } from '../models'; + @Injectable({ providedIn: 'root', }) @@ -33,24 +28,21 @@ export class PreprintFilesService { updateFileRelationship(preprintId: string, fileId: string): Observable { return this.jsonApiService - .patch>( - `${this.apiUrl}/preprints/${preprintId}/`, - { - data: { - type: 'preprints', - id: preprintId, - attributes: {}, - relationships: { - primary_file: { - data: { - type: 'files', - id: fileId, - }, + .patch(`${this.apiUrl}/preprints/${preprintId}/`, { + data: { + type: 'preprints', + id: preprintId, + attributes: {}, + relationships: { + primary_file: { + data: { + type: 'files', + id: fileId, }, }, }, - } - ) + }, + }) .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response))); } diff --git a/src/app/features/preprints/services/preprint-licenses.service.ts b/src/app/features/preprints/services/preprint-licenses.service.ts index 5d8a3a5f8..6f8de72cf 100644 --- a/src/app/features/preprints/services/preprint-licenses.service.ts +++ b/src/app/features/preprints/services/preprint-licenses.service.ts @@ -4,18 +4,14 @@ import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { PreprintsMapper } from '@osf/features/preprints/mappers'; -import { - PreprintAttributesJsonApi, - PreprintLicensePayloadJsonApi, - PreprintLinksJsonApi, - PreprintRelationshipsJsonApi, -} from '@osf/features/preprints/models'; +import { PreprintDataJsonApi } from '@osf/shared/models/preprints/preprint-json-api.model'; import { JsonApiService } from '@osf/shared/services/json-api.service'; import { LicensesMapper } from '@shared/mappers/licenses.mapper'; -import { ApiData } from '@shared/models/common/json-api.model'; import { LicenseModel, LicenseOptions } from '@shared/models/license/license.model'; import { LicensesResponseJsonApi } from '@shared/models/license/licenses-json-api.model'; +import { PreprintLicensePayloadJsonApi } from '../models'; + @Injectable({ providedIn: 'root', }) @@ -61,9 +57,7 @@ export class PreprintLicensesService { }; return this.jsonApiService - .patch< - ApiData - >(`${this.apiUrl}/preprints/${preprintId}/`, payload) + .patch(`${this.apiUrl}/preprints/${preprintId}/`, payload) .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response))); } } diff --git a/src/app/features/preprints/services/preprint-providers.service.ts b/src/app/features/preprints/services/preprint-providers.service.ts index 3463e59ed..9e2f18f61 100644 --- a/src/app/features/preprints/services/preprint-providers.service.ts +++ b/src/app/features/preprints/services/preprint-providers.service.ts @@ -6,11 +6,11 @@ import { ENVIRONMENT } from '@core/provider/environment.provider'; import { PreprintProvidersMapper } from '@osf/features/preprints/mappers'; import { PreprintProviderDetails, - PreprintProviderDetailsJsonApi, + PreprintProviderResponseJsonApi, PreprintProviderShortInfo, + PreprintProvidersListResponseJsonApi, } from '@osf/features/preprints/models'; import { JsonApiService } from '@osf/shared/services/json-api.service'; -import { JsonApiResponse } from '@shared/models/common/json-api.model'; import { SubjectModel } from '@shared/models/subject/subject.model'; import { SubjectsResponseJsonApi } from '@shared/models/subject/subjects-json-api.model'; @@ -27,15 +27,13 @@ export class PreprintProvidersService { getPreprintProviderById(id: string): Observable { return this.jsonApiService - .get>(`${this.apiUrl}${id}/?embed=brand`) + .get(`${this.apiUrl}${id}/?embed=brand`) .pipe(map((response) => PreprintProvidersMapper.fromPreprintProviderDetailsGetResponse(response.data))); } getPreprintProvidersToAdvertise(): Observable { return this.jsonApiService - .get< - JsonApiResponse - >(`${this.apiUrl}?filter[advertise_on_discover_page]=true&reload=true`) + .get(`${this.apiUrl}?filter[advertise_on_discover_page]=true&reload=true`) .pipe( map((response) => PreprintProvidersMapper.toPreprintProviderShortInfoFromGetResponse( @@ -47,7 +45,7 @@ export class PreprintProvidersService { getPreprintProvidersAllowingSubmissions(): Observable { return this.jsonApiService - .get>(`${this.apiUrl}?filter[allow_submissions]=true`) + .get(`${this.apiUrl}?filter[allow_submissions]=true`) .pipe(map((response) => PreprintProvidersMapper.toPreprintProviderShortInfoFromGetResponse(response.data))); } diff --git a/src/app/features/preprints/services/preprints-projects.service.ts b/src/app/features/preprints/services/preprints-projects.service.ts index 1f1eed57b..a228863a2 100644 --- a/src/app/features/preprints/services/preprints-projects.service.ts +++ b/src/app/features/preprints/services/preprints-projects.service.ts @@ -5,22 +5,17 @@ import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { Primitive, StringOrNull } from '@osf/shared/helpers/types.helper'; import { IdNameModel } from '@osf/shared/models/common/id-name.model'; -import { ApiData } from '@osf/shared/models/common/json-api.model'; import { CreateProjectPayloadJsoApi, NodeResponseJsonApi, NodesResponseJsonApi, } from '@osf/shared/models/nodes/nodes-json-api.model'; +import { PreprintDataJsonApi } from '@osf/shared/models/preprints/preprint-json-api.model'; import { JsonApiService } from '@osf/shared/services/json-api.service'; import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { PreprintsMapper } from '../mappers'; -import { - PreprintAttributesJsonApi, - PreprintLinksJsonApi, - PreprintModel, - PreprintRelationshipsJsonApi, -} from '../models'; +import { PreprintModel } from '../models'; @Injectable({ providedIn: 'root', @@ -70,24 +65,21 @@ export class PreprintsProjectsService { updatePreprintProjectRelationship(preprintId: string, projectId: string): Observable { return this.jsonApiService - .patch>( - `${this.apiUrl}/preprints/${preprintId}/`, - { - data: { - type: 'preprints', - id: preprintId, - attributes: {}, - relationships: { - node: { - data: { - type: 'nodes', - id: projectId, - }, + .patch(`${this.apiUrl}/preprints/${preprintId}/`, { + data: { + type: 'preprints', + id: preprintId, + attributes: {}, + relationships: { + node: { + data: { + type: 'nodes', + id: projectId, }, }, }, - } - ) + }, + }) .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response))); } diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index 5216bf12d..d065f23d0 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -5,29 +5,25 @@ import { Router } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { RegistryModerationMapper } from '@osf/features/moderation/mappers'; -import { ReviewActionsResponseJsonApi } from '@osf/features/moderation/models'; +import { ReviewActionsResponseJsonApi } from '@osf/features/moderation/models/review-action-json-api.model'; import { PreprintRequestActionsMapper } from '@osf/features/preprints/mappers/preprint-request-actions.mapper'; import { PreprintRequestAction } from '@osf/features/preprints/models/preprint-request-action.model'; import { searchPreferencesToJsonApiQueryParams } from '@osf/shared/helpers/search-pref-to-json-api-query-params.helper'; import { StringOrNull } from '@osf/shared/helpers/types.helper'; import { - ApiData, - JsonApiResponse, - JsonApiResponseWithMeta, - ResponseJsonApi, -} from '@osf/shared/models/common/json-api.model'; + PreprintAttributesJsonApi, + PreprintDataJsonApi, + PreprintMetricsResponseJsonApi, + PreprintResponseJsonApi, + PreprintsListResponseJsonApi, +} from '@osf/shared/models/preprints/preprint-json-api.model'; import { SearchFilters } from '@osf/shared/models/search-filters.model'; import { JsonApiService } from '@osf/shared/services/json-api.service'; import { preprintSortFieldMap } from '../constants'; import { PreprintRequestMapper, PreprintsMapper } from '../mappers'; import { - PreprintAttributesJsonApi, - PreprintEmbedsJsonApi, - PreprintLinksJsonApi, - PreprintMetaJsonApi, PreprintModel, - PreprintRelationshipsJsonApi, PreprintRequest, PreprintRequestActionsJsonApiResponse, PreprintRequestsJsonApiResponse, @@ -67,76 +63,52 @@ export class PreprintsService { createPreprint(title: string, abstract: string, providerId: string) { const payload = PreprintsMapper.toCreatePayload(title, abstract, providerId); return this.jsonApiService - .post< - JsonApiResponse< - ApiData, - null - > - >(`${this.apiUrl}/preprints/`, payload) + .post(`${this.apiUrl}/preprints/`, payload) .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response.data))); } getById(id: string) { return this.jsonApiService - .get< - JsonApiResponse< - ApiData, - null - > - >(`${this.apiUrl}/preprints/${id}/`) + .get(`${this.apiUrl}/preprints/${id}/`) .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response.data))); } getByIdWithEmbeds(id: string) { const params = { 'embed[]': ['license', 'identifiers'] }; - return this.jsonApiService - .get< - JsonApiResponseWithMeta< - ApiData, - PreprintMetaJsonApi, - null - > - >(`${this.apiUrl}/preprints/${id}/`, params) - .pipe( - map((response) => PreprintsMapper.fromPreprintWithEmbedsJsonApi(response)), - catchError((error) => { - if (error.error?.errors?.[0]?.meta?.flagged_content) { - this.router.navigate(['/spam-content']); - } - return throwError(() => error); - }) - ); + + return this.jsonApiService.get(`${this.apiUrl}/preprints/${id}/`, params).pipe( + map((response) => PreprintsMapper.fromPreprintWithEmbedsJsonApi(response)), + catchError((error) => { + if (error.error?.errors?.[0]?.meta?.flagged_content) { + this.router.navigate(['/spam-content']); + } + return throwError(() => error); + }) + ); } getPreprintMetrics(id: string) { const params = { 'metrics[views]': 'total', 'metrics[downloads]': 'total' }; - return this.jsonApiService - .get< - JsonApiResponseWithMeta, PreprintMetaJsonApi, null> - >(`${this.apiUrl}/preprints/${id}/`, params) - .pipe( - map((response) => ({ - downloads: response.meta.metrics.downloads, - views: response.meta.metrics.views, - })) - ); + return this.jsonApiService.get(`${this.apiUrl}/preprints/${id}/`, params).pipe( + map((response) => ({ + downloads: response.meta.metrics.downloads, + views: response.meta.metrics.views, + })) + ); } updatePreprint(id: string, payload: Partial): Observable { const apiPayload = this.mapPreprintDomainToApiPayload(payload); return this.jsonApiService - .patch>( - `${this.apiUrl}/preprints/${id}/`, - { - data: { - type: 'preprints', - id, - attributes: apiPayload, - }, - } - ) + .patch(`${this.apiUrl}/preprints/${id}/`, { + data: { + type: 'preprints', + id, + attributes: apiPayload, + }, + }) .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response))); } @@ -147,12 +119,7 @@ export class PreprintsService { createNewVersion(prevVersionPreprintId: string) { return this.jsonApiService - .post< - JsonApiResponse< - ApiData, - null - > - >(`${this.apiUrl}/preprints/${prevVersionPreprintId}/versions/`) + .post(`${this.apiUrl}/preprints/${prevVersionPreprintId}/versions/`) .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response.data))); } @@ -168,9 +135,7 @@ export class PreprintsService { getPreprintVersionIds(preprintId: string): Observable { return this.jsonApiService - .get< - ResponseJsonApi[]> - >(`${this.apiUrl}/preprints/${preprintId}/versions/`) + .get(`${this.apiUrl}/preprints/${preprintId}/versions/`) .pipe(map((response) => response.data.map((data) => data.id))); } @@ -184,9 +149,7 @@ export class PreprintsService { searchPreferencesToJsonApiQueryParams(params, pageNumber, pageSize, filters, preprintSortFieldMap); return this.jsonApiService - .get< - ResponseJsonApi[]> - >(`${this.apiUrl}/users/me/preprints/`, params) + .get(`${this.apiUrl}/users/me/preprints/`, params) .pipe(map((response) => PreprintsMapper.fromMyPreprintJsonApi(response))); } diff --git a/src/app/features/profile/components/profile-information/profile-information.component.spec.ts b/src/app/features/profile/components/profile-information/profile-information.component.spec.ts index f8ec5857e..9718289a4 100644 --- a/src/app/features/profile/components/profile-information/profile-information.component.spec.ts +++ b/src/app/features/profile/components/profile-information/profile-information.component.spec.ts @@ -92,7 +92,7 @@ describe('ProfileInformationComponent', () => { it('should expose ORCID id only when verified', () => { setup({ ...MOCK_USER, - external_identity: { + externalIdentity: { ORCID: { id: '0000-0002-1825-0097', status: ExternalIdentityStatus.VERIFIED, @@ -106,7 +106,7 @@ describe('ProfileInformationComponent', () => { it('should return undefined ORCID when status is not verified', () => { setup({ ...MOCK_USER, - external_identity: { + externalIdentity: { ORCID: { id: '0000-0002-1825-0097', status: ExternalIdentityStatus.LINK, diff --git a/src/app/features/profile/components/profile-information/profile-information.component.ts b/src/app/features/profile/components/profile-information/profile-information.component.ts index 68b9edf81..e74d23b08 100644 --- a/src/app/features/profile/components/profile-information/profile-information.component.ts +++ b/src/app/features/profile/components/profile-information/profile-information.component.ts @@ -50,7 +50,7 @@ export class ProfileInformationComponent { userSocials = computed(() => mapUserSocials(this.currentUser()?.social, SOCIAL_LINKS)); orcidId = computed(() => { - const orcid = this.currentUser()?.external_identity?.ORCID; + const orcid = this.currentUser()?.externalIdentity?.ORCID; return orcid?.status?.toUpperCase() === ExternalIdentityStatus.VERIFIED ? orcid.id : undefined; }); diff --git a/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.spec.ts b/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.spec.ts index eef31eabb..9ee681704 100644 --- a/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.spec.ts @@ -10,7 +10,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { ResourceSearchMode } from '@osf/shared/enums/resource-search-mode.enum'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; -import { MyResourcesItem } from '@osf/shared/models/my-resources/my-resources.model'; +import { MyResourcesItem } from '@osf/shared/models/my-resources/my-resources-item.model'; import { MyResourcesSelectors } from '@osf/shared/stores/my-resources'; import { NodeLinksSelectors } from '@osf/shared/stores/node-links'; diff --git a/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.ts b/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.ts index 279a2bc55..4760056f0 100644 --- a/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.ts +++ b/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.ts @@ -29,7 +29,7 @@ import { SearchInputComponent } from '@osf/shared/components/search-input/search import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants/default-table-params.constants'; import { ResourceSearchMode } from '@osf/shared/enums/resource-search-mode.enum'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; -import { MyResourcesItem } from '@osf/shared/models/my-resources/my-resources.model'; +import { MyResourcesItem } from '@osf/shared/models/my-resources/my-resources-item.model'; import { MyResourcesSearchFilters } from '@osf/shared/models/my-resources/my-resources-search-filters.model'; import { GetMyProjects, GetMyRegistrations, MyResourcesSelectors } from '@osf/shared/stores/my-resources'; import { CreateNodeLink, DeleteNodeLink, NodeLinksSelectors } from '@osf/shared/stores/node-links'; diff --git a/src/app/features/project/overview/components/overview-collections/overview-collections.component.html b/src/app/features/project/overview/components/overview-collections/overview-collections.component.html index ad246257a..cfbe47a3b 100644 --- a/src/app/features/project/overview/components/overview-collections/overview-collections.component.html +++ b/src/app/features/project/overview/components/overview-collections/overview-collections.component.html @@ -34,9 +34,7 @@

{{ 'project.overview.metadata.collection' | translate }}

@let cedarTemplate = cedarTemplateById().get(templateId) ?? null; @let attributes = - cedarRecord && cedarTemplate?.attributes?.template - ? getCedarAttributes(cedarRecord, cedarTemplate!) - : []; + cedarRecord && cedarTemplate?.template ? getCedarAttributes(cedarRecord, cedarTemplate!) : []; @if (attributes.length) {
diff --git a/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts b/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts index e0e8ec058..5a855273c 100644 --- a/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts +++ b/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts @@ -1,11 +1,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; -import { CedarMetadataDataTemplateJsonApi } from '@osf/features/metadata/models'; -import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; +import { CedarMetadataTemplateModel } from '@osf/features/metadata/models'; +import { CollectionSubmission } from '@osf/shared/models/collections/collection-submissions.model'; -import { CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK } from '@testing/mocks/cedar-metadata-data-template-json-api.mock'; -import { MOCK_CEDAR_METADATA_RECORD_DATA } from '@testing/mocks/cedar-metadata-record.mock'; +import { MOCK_CEDAR_METADATA_RECORD, MOCK_CEDAR_METADATA_TEMPLATE } from '@testing/mocks/cedar-metadata-domain.mock'; import { MOCK_COLLECTION_SUBMISSION_BASE, MOCK_COLLECTION_SUBMISSIONS, @@ -43,7 +42,7 @@ describe('OverviewCollectionsComponent', () => { ...s, collectionTitle: s.title, collectionId: `col-${s.id}`, - })) as CollectionSubmission[]; + })); fixture.componentRef.setInput('projectSubmissions', submissions); fixture.componentRef.setInput('isProjectSubmissionsLoading', true); fixture.detectChanges(); @@ -56,11 +55,10 @@ describe('OverviewCollectionsComponent', () => { ...MOCK_COLLECTION_SUBMISSION_BASE, requiredMetadataTemplateId: 'template-1', }; - const cedarTemplate: CedarMetadataDataTemplateJsonApi = - CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK as CedarMetadataDataTemplateJsonApi; + const cedarTemplate: CedarMetadataTemplateModel = MOCK_CEDAR_METADATA_TEMPLATE; fixture.componentRef.setInput('projectSubmissions', [cedarSubmission]); - fixture.componentRef.setInput('cedarRecords', [MOCK_CEDAR_METADATA_RECORD_DATA]); + fixture.componentRef.setInput('cedarRecords', [MOCK_CEDAR_METADATA_RECORD]); fixture.componentRef.setInput('cedarTemplates', [cedarTemplate]); fixture.detectChanges(); await fixture.whenStable(); @@ -88,10 +86,9 @@ describe('OverviewCollectionsComponent', () => { }); it('should extract key-value pairs from a cedar record using template field order and labels', () => { - const cedarTemplate: CedarMetadataDataTemplateJsonApi = - CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK as CedarMetadataDataTemplateJsonApi; + const cedarTemplate: CedarMetadataTemplateModel = MOCK_CEDAR_METADATA_TEMPLATE; - const result = component.getCedarAttributes(MOCK_CEDAR_METADATA_RECORD_DATA, cedarTemplate); + const result = component.getCedarAttributes(MOCK_CEDAR_METADATA_RECORD, cedarTemplate); expect(result).toContainEqual({ key: 'Project Name', label: 'Project Name', value: 'Test Project Name' }); }); diff --git a/src/app/features/project/overview/components/overview-collections/overview-collections.component.ts b/src/app/features/project/overview/components/overview-collections/overview-collections.component.ts index e07dc4016..7cd71d5c1 100644 --- a/src/app/features/project/overview/components/overview-collections/overview-collections.component.ts +++ b/src/app/features/project/overview/components/overview-collections/overview-collections.component.ts @@ -8,9 +8,9 @@ import { Tag } from 'primeng/tag'; import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { RouterLink } from '@angular/router'; -import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData } from '@osf/features/metadata/models'; +import { CedarMetadataRecordModel, CedarMetadataTemplateModel } from '@osf/features/metadata/models'; import { StopPropagationDirective } from '@osf/shared/directives/stop-propagation.directive'; -import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; +import { CollectionSubmission } from '@osf/shared/models/collections/collection-submissions.model'; import { KeyValueModel } from '@osf/shared/models/common/key-value.model'; import { CollectionStatusSeverityPipe } from '@osf/shared/pipes/collection-status-severity.pipe'; @@ -36,14 +36,14 @@ import { CollectionStatusSeverityPipe } from '@osf/shared/pipes/collection-statu export class OverviewCollectionsComponent { projectSubmissions = input(null); isProjectSubmissionsLoading = input(false); - cedarRecords = input(null); - cedarTemplates = input(null); + cedarRecords = input(null); + cedarTemplates = input(null); cedarRecordByTemplateId = computed(() => { const records = this.cedarRecords(); return new Map( records?.flatMap((record) => { - const templateId = record.relationships?.template?.data?.id; + const templateId = record.templateId; return templateId ? [[templateId, record] as const] : []; }) ?? [] ); @@ -54,9 +54,9 @@ export class OverviewCollectionsComponent { return new Map(templates?.map((t) => [t.id, t] as const) ?? []); }); - getCedarAttributes(record: CedarMetadataRecordData, template: CedarMetadataDataTemplateJsonApi): KeyValueModel[] { - const { order, propertyLabels } = template.attributes.template._ui; - const metadata = record.attributes.metadata as Record; + getCedarAttributes(record: CedarMetadataRecordModel, template: CedarMetadataTemplateModel): KeyValueModel[] { + const { order, propertyLabels } = template.template._ui; + const metadata = record.metadata as Record; const attributes: KeyValueModel[] = []; for (const key of order) { diff --git a/src/app/features/project/overview/mappers/project-overview.mapper.ts b/src/app/features/project/overview/mappers/project-overview.mapper.ts index c6fabdfe8..186b39b44 100644 --- a/src/app/features/project/overview/mappers/project-overview.mapper.ts +++ b/src/app/features/project/overview/mappers/project-overview.mapper.ts @@ -12,16 +12,13 @@ export class ProjectOverviewMapper { ...nodeAttributes, rootParentId: relationships?.root?.data?.id, parentId: relationships?.parent?.data?.id, - forksCount: relationships.forks?.links?.related?.meta - ? (relationships.forks?.links?.related?.meta['count'] as number) + forksCount: relationships?.forks?.links?.related?.meta + ? (relationships?.forks?.links?.related?.meta['count'] as number) : 0, - viewOnlyLinksCount: relationships.view_only_links?.links?.related?.meta - ? (relationships.view_only_links?.links?.related?.meta['count'] as number) + viewOnlyLinksCount: relationships?.view_only_links?.links?.related?.meta + ? (relationships?.view_only_links?.links?.related?.meta['count'] as number) : 0, - links: { - iri: data.links?.iri, - }, - licenseId: relationships.license?.data?.id, + licenseId: relationships?.license?.data?.id, }; } } diff --git a/src/app/features/project/overview/models/project-overview.model.ts b/src/app/features/project/overview/models/project-overview.model.ts index 2c155b330..f1bd65c38 100644 --- a/src/app/features/project/overview/models/project-overview.model.ts +++ b/src/app/features/project/overview/models/project-overview.model.ts @@ -1,12 +1,6 @@ -import { MetaJsonApi } from '@osf/shared/models/common/json-api.model'; import { ContributorModel } from '@osf/shared/models/contributors/contributor.model'; import { BaseNodeModel } from '@osf/shared/models/nodes/base-node.model'; -export interface ProjectOverviewWithMeta { - project: ProjectOverviewModel; - meta?: MetaJsonApi; -} - export interface ProjectOverviewModel extends BaseNodeModel { forksCount: number; viewOnlyLinksCount: number; @@ -14,9 +8,4 @@ export interface ProjectOverviewModel extends BaseNodeModel { rootParentId?: string; licenseId?: string; contributors?: ContributorModel[]; - links: ProjectOverviewLinksModel; -} - -interface ProjectOverviewLinksModel { - iri: string; } diff --git a/src/app/features/project/overview/services/project-overview.service.ts b/src/app/features/project/overview/services/project-overview.service.ts index 7133193be..5a0caba1e 100644 --- a/src/app/features/project/overview/services/project-overview.service.ts +++ b/src/app/features/project/overview/services/project-overview.service.ts @@ -12,7 +12,6 @@ import { LicensesMapper } from '@osf/shared/mappers/licenses.mapper'; import { BaseNodeMapper } from '@osf/shared/mappers/nodes'; import { NodePreprintMapper } from '@osf/shared/mappers/nodes/node-preprint.mapper'; import { NodeStorageMapper } from '@osf/shared/mappers/nodes/node-storage.mapper'; -import { JsonApiResponse } from '@osf/shared/models/common/json-api.model'; import { IdentifiersResponseJsonApi } from '@osf/shared/models/identifiers/identifier-json-api.model'; import { InstitutionsJsonApiResponse } from '@osf/shared/models/institutions/institution-json-api.model'; import { Institution } from '@osf/shared/models/institutions/institutions.model'; @@ -30,7 +29,7 @@ import { IdentifierModel } from '@shared/models/identifiers/identifier.model'; import { LicenseModel } from '@shared/models/license/license.model'; import { ProjectOverviewMapper } from '../mappers'; -import { PrivacyStatusModel, ProjectOverviewWithMeta } from '../models'; +import { PrivacyStatusModel, ProjectOverviewModel } from '../models'; @Injectable({ providedIn: 'root', @@ -43,13 +42,15 @@ export class ProjectOverviewService { return `${this.environment.apiDomainUrl}/v2`; } - getProjectById(projectId: string): Observable { + getProjectById(projectId: string): Observable> { const params: Record = { related_counts: 'forks,view_only_links' }; return this.jsonApiService.get(`${this.apiUrl}/nodes/${projectId}/`, params).pipe( map((response) => ({ - project: ProjectOverviewMapper.getProjectOverview(response.data), - meta: response.meta, + data: ProjectOverviewMapper.getProjectOverview(response.data), + totalCount: 1, + pageSize: 1, + isAnonymous: response.meta?.anonymous ?? false, })) ); } @@ -123,7 +124,7 @@ export class ProjectOverviewService { }; return this.jsonApiService - .post>(`${this.apiUrl}/nodes/`, payload) + .post(`${this.apiUrl}/nodes/`, payload) .pipe(map((response) => BaseNodeMapper.getNodeData(response.data))); } @@ -157,7 +158,7 @@ export class ProjectOverviewService { } return this.jsonApiService - .post>(`${this.apiUrl}/nodes/${projectId}/children/`, payload, params) + .post(`${this.apiUrl}/nodes/${projectId}/children/`, payload, params) .pipe( switchMap((response) => { const componentId = response.data.id; diff --git a/src/app/features/project/overview/store/project-overview.state.ts b/src/app/features/project/overview/store/project-overview.state.ts index 16a944ac1..e8799ccd1 100644 --- a/src/app/features/project/overview/store/project-overview.state.ts +++ b/src/app/features/project/overview/store/project-overview.state.ts @@ -58,11 +58,11 @@ export class ProjectOverviewState { tap((response) => { ctx.patchState({ project: { - data: response.project, + data: response.data, isLoading: false, error: null, }, - isAnonymous: response.meta?.anonymous ?? false, + isAnonymous: response.isAnonymous, }); }), catchError((error) => handleSectionError(ctx, 'project', error)) diff --git a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts index 61af24cc2..b7907d439 100644 --- a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts +++ b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts @@ -25,9 +25,9 @@ import { OperationNames } from '@osf/shared/enums/operation-names.enum'; import { ProjectAddonsStepperValue } from '@osf/shared/enums/profile-addons-stepper.enum'; import { getAddonTypeString } from '@osf/shared/helpers/addon-type.helper'; import { AddonModel } from '@osf/shared/models/addons/addon.model'; -import { AuthorizedAddonRequestJsonApi } from '@osf/shared/models/addons/addon-json-api.model'; import { AddonTerm } from '@osf/shared/models/addons/addon-utils.model'; import { AuthorizedAccountModel } from '@osf/shared/models/addons/authorized-account.model'; +import { AuthorizedAddonRequestJsonApi } from '@osf/shared/models/addons/authorized-addon-json-api.model'; import { AddonFormService } from '@osf/shared/services/addons/addon-form.service'; import { AddonOAuthService } from '@osf/shared/services/addons/addon-oauth.service'; import { AddonOperationInvocationService } from '@osf/shared/services/addons/addon-operation-invocation.service'; @@ -156,7 +156,7 @@ export class ConnectConfiguredAddonComponent { } const openURL = new URL(addon.redirectUrl); openURL.searchParams.set('nodeIri', this.resourceUri()); - openURL.searchParams.set('userIri', this.addonsUserReference()[0]?.attributes.user_uri); + openURL.searchParams.set('userIri', this.addonsUserReference()[0]?.userUri); return openURL.toString(); }); diff --git a/src/app/features/project/project-addons/project-addons.component.html b/src/app/features/project/project-addons/project-addons.component.html index 3fcf71ec0..4d4691b15 100644 --- a/src/app/features/project/project-addons/project-addons.component.html +++ b/src/app/features/project/project-addons/project-addons.component.html @@ -1,4 +1,4 @@ - +