From 8dfcb80aec9f4a2d2e05ec224f8939e0e1e661dc Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Sat, 21 Jun 2025 11:25:02 +0300 Subject: [PATCH 01/15] feat(registries): start work on registries --- src/app/app.routes.ts | 14 +++++++++++ .../new-registration.component.html | 8 ++++++ .../new-registration.component.scss | 5 ++++ .../new-registration.component.spec.ts | 22 ++++++++++++++++ .../new-registration.component.ts | 20 +++++++++++++++ src/app/features/registries/models/index.ts | 0 .../models/provider-json-api.model.ts | 14 +++++++++++ .../registries/models/provider.model.ts | 0 .../registries/registries.component.html | 3 +++ .../registries/registries.component.scss | 5 ++++ .../registries/registries.component.spec.ts | 22 ++++++++++++++++ .../registries/registries.component.ts | 11 ++++++++ src/app/features/registries/services/index.ts | 3 +++ .../registries/services/projects.service.ts | 6 +++++ .../registries/services/providers.service.ts | 6 +++++ .../registries/services/registries.service.ts | 6 +++++ src/app/features/registries/store/index.ts | 4 +++ .../registries/store/registries.actions.ts | 3 +++ .../registries/store/registries.model.ts | 11 ++++++++ .../registries/store/registries.selectors.ts | 11 ++++++++ .../registries/store/registries.state.ts | 25 +++++++++++++++++++ src/assets/i18n/en.json | 16 ++++++++++++ 22 files changed, 215 insertions(+) create mode 100644 src/app/features/registries/components/new-registration/new-registration.component.html create mode 100644 src/app/features/registries/components/new-registration/new-registration.component.scss create mode 100644 src/app/features/registries/components/new-registration/new-registration.component.spec.ts create mode 100644 src/app/features/registries/components/new-registration/new-registration.component.ts create mode 100644 src/app/features/registries/models/index.ts create mode 100644 src/app/features/registries/models/provider-json-api.model.ts create mode 100644 src/app/features/registries/models/provider.model.ts create mode 100644 src/app/features/registries/registries.component.html create mode 100644 src/app/features/registries/registries.component.scss create mode 100644 src/app/features/registries/registries.component.spec.ts create mode 100644 src/app/features/registries/registries.component.ts create mode 100644 src/app/features/registries/services/index.ts create mode 100644 src/app/features/registries/services/projects.service.ts create mode 100644 src/app/features/registries/services/providers.service.ts create mode 100644 src/app/features/registries/services/registries.service.ts create mode 100644 src/app/features/registries/store/index.ts create mode 100644 src/app/features/registries/store/registries.actions.ts create mode 100644 src/app/features/registries/store/registries.model.ts create mode 100644 src/app/features/registries/store/registries.selectors.ts create mode 100644 src/app/features/registries/store/registries.state.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 683d06f2c..f678c0695 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -169,6 +169,20 @@ export const routes: Routes = [ }, ], }, + { + path: 'registries', + loadComponent: () => + import('./features/registries/registries.component').then((mod) => mod.RegistriesComponent), + children: [ + { + path: 'new', + loadComponent: () => + import('./features/registries/components/new-registration/new-registration.component').then( + (mod) => mod.NewRegistrationComponent + ), + }, + ], + }, { path: 'settings', loadChildren: () => import('./features/settings/settings.routes').then((mod) => mod.settingsRoutes), diff --git a/src/app/features/registries/components/new-registration/new-registration.component.html b/src/app/features/registries/components/new-registration/new-registration.component.html new file mode 100644 index 000000000..f454762fd --- /dev/null +++ b/src/app/features/registries/components/new-registration/new-registration.component.html @@ -0,0 +1,8 @@ + +
+

+ {{ 'registries.new.infoText1' | translate }} + {{ 'common.links.clickHere' | translate }} + {{ 'registries.new.infoText2' | translate }} +

+
diff --git a/src/app/features/registries/components/new-registration/new-registration.component.scss b/src/app/features/registries/components/new-registration/new-registration.component.scss new file mode 100644 index 000000000..da0c027b5 --- /dev/null +++ b/src/app/features/registries/components/new-registration/new-registration.component.scss @@ -0,0 +1,5 @@ +:host { + display: flex; + flex-direction: column; + flex: 1; +} diff --git a/src/app/features/registries/components/new-registration/new-registration.component.spec.ts b/src/app/features/registries/components/new-registration/new-registration.component.spec.ts new file mode 100644 index 000000000..b6c711a95 --- /dev/null +++ b/src/app/features/registries/components/new-registration/new-registration.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NewRegistrationComponent } from './new-registration.component'; + +describe('NewRegistrationComponent', () => { + let component: NewRegistrationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NewRegistrationComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(NewRegistrationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/new-registration/new-registration.component.ts b/src/app/features/registries/components/new-registration/new-registration.component.ts new file mode 100644 index 000000000..8aae7fbff --- /dev/null +++ b/src/app/features/registries/components/new-registration/new-registration.component.ts @@ -0,0 +1,20 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; + +import { SubHeaderComponent } from '@osf/shared/components'; + +@Component({ + selector: 'osf-new-registration', + imports: [SubHeaderComponent, TranslatePipe], + templateUrl: './new-registration.component.html', + styleUrl: './new-registration.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NewRegistrationComponent { + private readonly fb = inject(FormBuilder); + draftForm = this.fb.group({ + provider: [''], + }); +} diff --git a/src/app/features/registries/models/index.ts b/src/app/features/registries/models/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/models/provider-json-api.model.ts b/src/app/features/registries/models/provider-json-api.model.ts new file mode 100644 index 000000000..07f8bca6e --- /dev/null +++ b/src/app/features/registries/models/provider-json-api.model.ts @@ -0,0 +1,14 @@ +import { ApiData, MetaJsonApi, PaginationLinksJsonApi } from '@osf/core/models'; + +export interface ProvidersResponseJsonApi { + data: ProviderDataJsonApi[]; + meta: MetaJsonApi; + links: PaginationLinksJsonApi; +} + +export type ProviderDataJsonApi = ApiData; + +interface ProviderAttributesJsonApi { + full_name: string; + permission_group: 'moderator' | 'admin'; +} diff --git a/src/app/features/registries/models/provider.model.ts b/src/app/features/registries/models/provider.model.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/registries.component.html b/src/app/features/registries/registries.component.html new file mode 100644 index 000000000..7efab5d8a --- /dev/null +++ b/src/app/features/registries/registries.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/features/registries/registries.component.scss b/src/app/features/registries/registries.component.scss new file mode 100644 index 000000000..da0c027b5 --- /dev/null +++ b/src/app/features/registries/registries.component.scss @@ -0,0 +1,5 @@ +:host { + display: flex; + flex-direction: column; + flex: 1; +} diff --git a/src/app/features/registries/registries.component.spec.ts b/src/app/features/registries/registries.component.spec.ts new file mode 100644 index 000000000..01003ff5e --- /dev/null +++ b/src/app/features/registries/registries.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegistriesComponent } from './registries.component'; + +describe('RegistriesComponent', () => { + let component: RegistriesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegistriesComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RegistriesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/registries.component.ts b/src/app/features/registries/registries.component.ts new file mode 100644 index 000000000..a3009cbcc --- /dev/null +++ b/src/app/features/registries/registries.component.ts @@ -0,0 +1,11 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +@Component({ + selector: 'osf-registries', + imports: [RouterModule], + templateUrl: './registries.component.html', + styleUrl: './registries.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegistriesComponent {} diff --git a/src/app/features/registries/services/index.ts b/src/app/features/registries/services/index.ts new file mode 100644 index 000000000..04f640163 --- /dev/null +++ b/src/app/features/registries/services/index.ts @@ -0,0 +1,3 @@ +export * from './projects.service'; +export * from './providers.service'; +export * from './registries.service'; diff --git a/src/app/features/registries/services/projects.service.ts b/src/app/features/registries/services/projects.service.ts new file mode 100644 index 000000000..1756bf9a5 --- /dev/null +++ b/src/app/features/registries/services/projects.service.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class ProjectsService {} diff --git a/src/app/features/registries/services/providers.service.ts b/src/app/features/registries/services/providers.service.ts new file mode 100644 index 000000000..f7c20ae97 --- /dev/null +++ b/src/app/features/registries/services/providers.service.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class ProvidersService {} diff --git a/src/app/features/registries/services/registries.service.ts b/src/app/features/registries/services/registries.service.ts new file mode 100644 index 000000000..6ba6815b3 --- /dev/null +++ b/src/app/features/registries/services/registries.service.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class RegistriesService {} diff --git a/src/app/features/registries/store/index.ts b/src/app/features/registries/store/index.ts new file mode 100644 index 000000000..92a9455c1 --- /dev/null +++ b/src/app/features/registries/store/index.ts @@ -0,0 +1,4 @@ +export * from './registries.actions'; +export * from './registries.model'; +export * from './registries.selectors'; +export * from './registries.state'; diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts new file mode 100644 index 000000000..6d8c241cf --- /dev/null +++ b/src/app/features/registries/store/registries.actions.ts @@ -0,0 +1,3 @@ +export class GetProviders { + static readonly type = '[Registries] Get Providers'; +} diff --git a/src/app/features/registries/store/registries.model.ts b/src/app/features/registries/store/registries.model.ts new file mode 100644 index 000000000..a498212de --- /dev/null +++ b/src/app/features/registries/store/registries.model.ts @@ -0,0 +1,11 @@ +import { AsyncStateModel } from '@osf/shared/models'; + +export interface Provider { + id: string; + title: string; +} + +export interface RegistriesStateModel { + providers: AsyncStateModel; + currentProviderId: string | null; +} diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts new file mode 100644 index 000000000..01b9a6aee --- /dev/null +++ b/src/app/features/registries/store/registries.selectors.ts @@ -0,0 +1,11 @@ +import { Selector } from '@ngxs/store'; + +import { Provider, RegistriesStateModel } from './registries.model'; +import { RegistriesState } from './registries.state'; + +export class RegistriesSelectors { + @Selector([RegistriesState]) + static getProviders(state: RegistriesStateModel): Provider[] { + return state.providers.data || []; + } +} diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts new file mode 100644 index 000000000..4ec5e4bb5 --- /dev/null +++ b/src/app/features/registries/store/registries.state.ts @@ -0,0 +1,25 @@ +import { State } from '@ngxs/store'; + +import { Injectable } from '@angular/core'; + +import { ProvidersService } from '../services'; + +import { RegistriesStateModel } from './registries.model'; + +const DefaultState: RegistriesStateModel = { + providers: { + data: [], + isLoading: false, + error: null, + }, + currentProviderId: null, +}; + +@State({ + name: 'registries', + defaults: { ...DefaultState }, +}) +@Injectable() +export class RegistriesState { + constructor(private providersService: ProvidersService) {} +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index c693d5445..11def31d2 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -38,6 +38,9 @@ "deleteConfirmation": { "header": "Delete", "message": "Are you sure you want to proceed?" + }, + "links": { + "clickHere": "Click here" } }, "navigation": { @@ -331,6 +334,19 @@ "updateProjectSettingsMessage": "Successfully updated project settings." } }, + "registries": { + "new": { + "addNewRegistry": "Add New Registry", + "infoText1": "You are submitting to OSF Registries.", + "infoText2": "to learn more about other hosted registries", + "steps": { + "step1": "Do you have content for registration in an existing OSF project?", + "step2": "Which project do you want to register?", + "step3": "Which type of registration would you like to create?", + "stepTwoInfo": "If your project includes components, you can select which components to include or exclude at the end of the registration." + } + } + }, "myProfile": { "editProfile": "Edit Profile" }, From 80325c08616a22998edc908a0a65d20ac91adc93 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Sun, 22 Jun 2025 12:56:51 +0300 Subject: [PATCH 02/15] feat(registries): work on new registrations --- .../core/constants/ngxs-states.constant.ts | 2 + .../institutions/moderation.service.ts | 51 ++++++++++++ .../new-registration.component.html | 43 ++++++++++ .../new-registration.component.ts | 27 ++++++- src/app/features/registries/mappers/index.ts | 0 .../registries/mappers/projects.mapper.ts | 11 +++ .../registries/mappers/providers.mapper.ts | 11 +++ src/app/features/registries/models/index.ts | 2 + src/app/features/registries/models/project.ts | 4 + .../models/projects-json-api.model.ts | 13 +++ .../registries/models/provider.model.ts | 4 + ...i.model.ts => providers-json-api.model.ts} | 3 +- .../registries/services/projects.service.ts | 26 +++++- .../registries/services/providers.service.ts | 26 +++++- .../registries/services/registries.service.ts | 20 ++++- .../registries/store/registries.actions.ts | 4 + .../registries/store/registries.model.ts | 6 +- .../registries/store/registries.selectors.ts | 11 ++- .../registries/store/registries.state.ts | 80 ++++++++++++++++++- src/assets/i18n/en.json | 7 +- 20 files changed, 331 insertions(+), 20 deletions(-) create mode 100644 src/app/features/institutions/moderation.service.ts create mode 100644 src/app/features/registries/mappers/index.ts create mode 100644 src/app/features/registries/mappers/projects.mapper.ts create mode 100644 src/app/features/registries/mappers/providers.mapper.ts create mode 100644 src/app/features/registries/models/project.ts create mode 100644 src/app/features/registries/models/projects-json-api.model.ts rename src/app/features/registries/models/{provider-json-api.model.ts => providers-json-api.model.ts} (84%) diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 7f622dcdd..b54e6fd70 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -9,6 +9,7 @@ import { ProjectOverviewState } from '@osf/features/project/overview/store'; import { RegistrationsState } from '@osf/features/project/registrations/store'; import { SettingsState } from '@osf/features/project/settings/store'; import { WikiState } from '@osf/features/project/wiki/store/wiki.state'; +import { RegistriesState } from '@osf/features/registries/store'; import { AccountSettingsState } from '@osf/features/settings/account-settings/store/account-settings.state'; import { DeveloperAppsState } from '@osf/features/settings/developer-apps/store'; import { NotificationSubscriptionState } from '@osf/features/settings/notifications/store'; @@ -34,4 +35,5 @@ export const STATES = [ WikiState, MeetingsState, RegistrationsState, + RegistriesState, ]; diff --git a/src/app/features/institutions/moderation.service.ts b/src/app/features/institutions/moderation.service.ts new file mode 100644 index 000000000..1ba57fd09 --- /dev/null +++ b/src/app/features/institutions/moderation.service.ts @@ -0,0 +1,51 @@ +import { map, Observable, of } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; +import { JsonApiService } from '@osf/core/services'; +import { PaginatedData } from '@osf/shared/models'; + +import { ModerationMapper } from '../mappers'; +import { ModeratorAddModel, ModeratorModel, ModeratorResponseJsonApi } from '../models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class ModerationService { + private readonly tesModeratorsUrl = 'assets/collection-moderators.json'; + private readonly jsonApiService = inject(JsonApiService); + + getCollectionModerators(providerId: string): Observable { + return ( + this.jsonApiService + // .get(`${this.baseUrl}/providers/collections/${providerId}/moderators/`) + .get(this.tesModeratorsUrl) + .pipe(map((response) => response.data.map((moderator) => ModerationMapper.fromModeratorResponse(moderator)))) + ); + } + + addCollectionModerator(providerId: string, data: ModeratorAddModel): Observable { + return of({} as ModeratorModel); + } + + updateCollectionModerator(providerId: string, data: ModeratorAddModel): Observable { + return of({} as ModeratorModel); + } + + deleteCollectionModerator(providerId: string, userId: string): Observable { + const baseUrl = ``; + + return this.jsonApiService.delete(baseUrl); + } + + searchUsers(value: string, page = 1): Observable> { + const baseUrl = `${environment.apiUrl}/users/?filter[full_name]=${value}&page=${page}`; + + return this.jsonApiService + .get>(baseUrl) + .pipe(map((response) => ModerationMapper.fromUsersWithPaginationGetResponse(response))); + } +} diff --git a/src/app/features/registries/components/new-registration/new-registration.component.html b/src/app/features/registries/components/new-registration/new-registration.component.html index f454762fd..52a6d56c9 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.html +++ b/src/app/features/registries/components/new-registration/new-registration.component.html @@ -6,3 +6,46 @@ {{ 'registries.new.infoText2' | translate }}

+
+ +

{{ ('registries.new.steps.title' | translate) + '1' }}

+

{{ 'registries.new.steps.step1' | translate }}

+
+ + +
+
+ +

{{ ('registries.new.steps.title' | translate) + '2' }}

+

{{ 'registries.new.steps.step2' | translate }}

+

{{ 'registries.new.steps.step2InfoText' | translate }}

+
+ +
+
+ +

{{ ('registries.new.steps.title' | translate) + (isProjectRegistration ? '3' : '2') }}

+

{{ 'registries.new.steps.step3' | translate }}

+
+ +
+
+
diff --git a/src/app/features/registries/components/new-registration/new-registration.component.ts b/src/app/features/registries/components/new-registration/new-registration.component.ts index 8aae7fbff..71401113c 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.ts +++ b/src/app/features/registries/components/new-registration/new-registration.component.ts @@ -1,20 +1,45 @@ +import { createDispatchMap, select } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { Select } from 'primeng/select'; + import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { SubHeaderComponent } from '@osf/shared/components'; +import { Project } from '../../models'; +import { GetProjects, GetProviders, RegistriesSelectors } from '../../store'; + @Component({ selector: 'osf-new-registration', - imports: [SubHeaderComponent, TranslatePipe], + imports: [SubHeaderComponent, TranslatePipe, Card, Button, Select], templateUrl: './new-registration.component.html', styleUrl: './new-registration.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class NewRegistrationComponent { private readonly fb = inject(FormBuilder); + protected readonly projects = select(RegistriesSelectors.getProjects); + protected readonly providers = select(RegistriesSelectors.getProviders); + protected actions = createDispatchMap({ + getProjects: GetProjects, + getProviders: GetProviders, + }); + isProjectRegistration = true; draftForm = this.fb.group({ provider: [''], }); + + constructor() { + this.actions.getProjects(); + this.actions.getProviders(); + } + + onSelectProject(project: Project) { + console.log('Project selected', project); + } } diff --git a/src/app/features/registries/mappers/index.ts b/src/app/features/registries/mappers/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/mappers/projects.mapper.ts b/src/app/features/registries/mappers/projects.mapper.ts new file mode 100644 index 000000000..4583e4583 --- /dev/null +++ b/src/app/features/registries/mappers/projects.mapper.ts @@ -0,0 +1,11 @@ +import { Project } from '../models'; +import { ProjectsResponseJsonApi } from '../models/projects-json-api.model'; + +export class ProjectsMapper { + static fromProjectsResponse(response: ProjectsResponseJsonApi): Project[] { + return response.data.map((item) => ({ + id: item.id, + title: item.attributes.title, + })); + } +} diff --git a/src/app/features/registries/mappers/providers.mapper.ts b/src/app/features/registries/mappers/providers.mapper.ts new file mode 100644 index 000000000..445769db9 --- /dev/null +++ b/src/app/features/registries/mappers/providers.mapper.ts @@ -0,0 +1,11 @@ +import { Provider } from '../models'; +import { ProvidersResponseJsonApi } from '../models/providers-json-api.model'; + +export class ProvidersMapper { + static fromProvidersResponse(response: ProvidersResponseJsonApi): Provider[] { + return response.data.map((item) => ({ + id: item.id, + name: item.attributes.name, + })); + } +} diff --git a/src/app/features/registries/models/index.ts b/src/app/features/registries/models/index.ts index e69de29bb..e5d1d4825 100644 --- a/src/app/features/registries/models/index.ts +++ b/src/app/features/registries/models/index.ts @@ -0,0 +1,2 @@ +export * from './project'; +export * from './provider.model'; diff --git a/src/app/features/registries/models/project.ts b/src/app/features/registries/models/project.ts new file mode 100644 index 000000000..870a40589 --- /dev/null +++ b/src/app/features/registries/models/project.ts @@ -0,0 +1,4 @@ +export interface Project { + id: string; + title: string; +} diff --git a/src/app/features/registries/models/projects-json-api.model.ts b/src/app/features/registries/models/projects-json-api.model.ts new file mode 100644 index 000000000..0ce2e620a --- /dev/null +++ b/src/app/features/registries/models/projects-json-api.model.ts @@ -0,0 +1,13 @@ +import { ApiData, MetaJsonApi, PaginationLinksJsonApi } from '@osf/core/models'; + +export interface ProjectsResponseJsonApi { + data: ProjectsDataJsonApi[]; + meta: MetaJsonApi; + links: PaginationLinksJsonApi; +} + +export type ProjectsDataJsonApi = ApiData; + +interface ProjectsAttributesJsonApi { + title: string; +} diff --git a/src/app/features/registries/models/provider.model.ts b/src/app/features/registries/models/provider.model.ts index e69de29bb..aa49d5327 100644 --- a/src/app/features/registries/models/provider.model.ts +++ b/src/app/features/registries/models/provider.model.ts @@ -0,0 +1,4 @@ +export interface Provider { + id: string; + name: string; +} diff --git a/src/app/features/registries/models/provider-json-api.model.ts b/src/app/features/registries/models/providers-json-api.model.ts similarity index 84% rename from src/app/features/registries/models/provider-json-api.model.ts rename to src/app/features/registries/models/providers-json-api.model.ts index 07f8bca6e..1b69240ff 100644 --- a/src/app/features/registries/models/provider-json-api.model.ts +++ b/src/app/features/registries/models/providers-json-api.model.ts @@ -9,6 +9,5 @@ export interface ProvidersResponseJsonApi { export type ProviderDataJsonApi = ApiData; interface ProviderAttributesJsonApi { - full_name: string; - permission_group: 'moderator' | 'admin'; + name: string; } diff --git a/src/app/features/registries/services/projects.service.ts b/src/app/features/registries/services/projects.service.ts index 1756bf9a5..6d051a176 100644 --- a/src/app/features/registries/services/projects.service.ts +++ b/src/app/features/registries/services/projects.service.ts @@ -1,6 +1,28 @@ -import { Injectable } from '@angular/core'; +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@osf/core/services'; + +import { ProjectsMapper } from '../mappers/projects.mapper'; +import { Project } from '../models'; +import { ProjectsResponseJsonApi } from '../models/projects-json-api.model'; + +import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class ProjectsService {} +export class ProjectsService { + private apiUrl = environment.apiUrl; + private readonly jsonApiService = inject(JsonApiService); + + getProjects(): Observable { + const params: Record = { + 'filter[current_user_permissions]': 'admin', + }; + return this.jsonApiService + .get(`${this.apiUrl}/nodes/`, params) + .pipe(map((response) => ProjectsMapper.fromProjectsResponse(response))); + } +} diff --git a/src/app/features/registries/services/providers.service.ts b/src/app/features/registries/services/providers.service.ts index f7c20ae97..371016e65 100644 --- a/src/app/features/registries/services/providers.service.ts +++ b/src/app/features/registries/services/providers.service.ts @@ -1,6 +1,28 @@ -import { Injectable } from '@angular/core'; +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@osf/core/services'; + +import { ProvidersMapper } from '../mappers/providers.mapper'; +import { Provider } from '../models'; +import { ProvidersResponseJsonApi } from '../models/providers-json-api.model'; + +import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class ProvidersService {} +export class ProvidersService { + private apiUrl = environment.apiUrl; + private readonly jsonApiService = inject(JsonApiService); + + getProviders(): Observable { + const params: Record = { + 'filter[current_user_permissions]': 'admin', + }; + return this.jsonApiService + .get(`${this.apiUrl}/nodes/`, params) + .pipe(map((response) => ProvidersMapper.fromProvidersResponse(response))); + } +} diff --git a/src/app/features/registries/services/registries.service.ts b/src/app/features/registries/services/registries.service.ts index 6ba6815b3..65f7e9691 100644 --- a/src/app/features/registries/services/registries.service.ts +++ b/src/app/features/registries/services/registries.service.ts @@ -1,6 +1,22 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@osf/core/services'; + +import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class RegistriesService {} +export class RegistriesService { + private apiUrl = environment.apiUrl; + private readonly jsonApiService = inject(JsonApiService); + + // getProjects(): Observable { + // const params: Record = { + // 'filter[current_user_permissions]': 'admin', + // }; + // return this.jsonApiService + // .get(`${this.apiUrl}/nodes/`, params) + // .pipe(map((response) => ProjectsMapper.fromProjectsResponse(response))); + // } +} diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index 6d8c241cf..0d5dadd4a 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -1,3 +1,7 @@ export class GetProviders { static readonly type = '[Registries] Get Providers'; } + +export class GetProjects { + static readonly type = '[Registries] Get Projects'; +} diff --git a/src/app/features/registries/store/registries.model.ts b/src/app/features/registries/store/registries.model.ts index a498212de..cf237eade 100644 --- a/src/app/features/registries/store/registries.model.ts +++ b/src/app/features/registries/store/registries.model.ts @@ -1,11 +1,9 @@ import { AsyncStateModel } from '@osf/shared/models'; -export interface Provider { - id: string; - title: string; -} +import { Project, Provider } from '../models'; export interface RegistriesStateModel { providers: AsyncStateModel; currentProviderId: string | null; + projects: AsyncStateModel; } diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts index 01b9a6aee..32cee3732 100644 --- a/src/app/features/registries/store/registries.selectors.ts +++ b/src/app/features/registries/store/registries.selectors.ts @@ -1,11 +1,18 @@ import { Selector } from '@ngxs/store'; -import { Provider, RegistriesStateModel } from './registries.model'; +import { Project, Provider } from '../models'; + +import { RegistriesStateModel } from './registries.model'; import { RegistriesState } from './registries.state'; export class RegistriesSelectors { @Selector([RegistriesState]) static getProviders(state: RegistriesStateModel): Provider[] { - return state.providers.data || []; + return state.providers.data; + } + + @Selector([RegistriesState]) + static getProjects(state: RegistriesStateModel): Project[] { + return state.projects.data; } } diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index 4ec5e4bb5..a5e509280 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -1,9 +1,13 @@ -import { State } from '@ngxs/store'; +import { Action, State, StateContext } from '@ngxs/store'; + +import { throwError } from 'rxjs'; import { Injectable } from '@angular/core'; -import { ProvidersService } from '../services'; +import { Project } from '../models'; +import { ProjectsService, ProvidersService } from '../services'; +import { GetProjects, GetProviders } from './registries.actions'; import { RegistriesStateModel } from './registries.model'; const DefaultState: RegistriesStateModel = { @@ -13,6 +17,11 @@ const DefaultState: RegistriesStateModel = { error: null, }, currentProviderId: null, + projects: { + data: [], + isLoading: false, + error: null, + }, }; @State({ @@ -21,5 +30,70 @@ const DefaultState: RegistriesStateModel = { }) @Injectable() export class RegistriesState { - constructor(private providersService: ProvidersService) {} + constructor( + private providersService: ProvidersService, + private projectsService: ProjectsService + ) {} + + @Action(GetProjects) + getProjects({ patchState }: StateContext) { + patchState({ + projects: { + ...DefaultState.projects, + isLoading: true, + }, + }); + return this.projectsService.getProjects().subscribe({ + next: (projects: Project[]) => { + patchState({ + projects: { + data: projects, + isLoading: false, + error: null, + }, + }); + }, + error: (error) => { + patchState({ + projects: { ...DefaultState.projects, isLoading: false, error }, + }); + }, + }); + } + + @Action(GetProviders) + getProviders({ patchState }: StateContext) { + patchState({ + providers: { + ...DefaultState.providers, + isLoading: true, + }, + }); + + return this.providersService.getProviders().subscribe({ + next: (providers) => { + patchState({ + providers: { + data: providers, + isLoading: false, + error: null, + }, + }); + }, + error: (error) => { + patchState({ + providers: { + ...DefaultState.providers, + isLoading: false, + error, + }, + }); + }, + }); + } + + private handleError(ctx: StateContext, error: Error) { + ctx.patchState({}); + return throwError(() => error); + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 11def31d2..787e54439 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -18,7 +18,9 @@ "skip": "Skip", "done": "Done", "select": "Select", - "deselect": "Deselect" + "deselect": "Deselect", + "yes": "Yes", + "no": "No" }, "search": { "title": "Search", @@ -340,10 +342,11 @@ "infoText1": "You are submitting to OSF Registries.", "infoText2": "to learn more about other hosted registries", "steps": { + "title": "Step", "step1": "Do you have content for registration in an existing OSF project?", "step2": "Which project do you want to register?", "step3": "Which type of registration would you like to create?", - "stepTwoInfo": "If your project includes components, you can select which components to include or exclude at the end of the registration." + "step2InfoText": "If your project includes components, you can select which components to include or exclude at the end of the registration." } } }, From 782ed307c1f5bea9468991413dfcac47c816f3a5 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Mon, 23 Jun 2025 14:59:06 +0300 Subject: [PATCH 03/15] feat(registries): add first registries page --- .../new-registration.component.html | 66 ++++++++++++------- .../new-registration.component.ts | 50 +++++++++++--- .../models/registries-json-api.model.ts | 23 +++++++ .../registries/models/registries.model.ts | 3 + .../registries/services/projects.service.ts | 2 +- .../registries/services/providers.service.ts | 5 +- .../registries/services/registries.service.ts | 36 +++++++--- .../registries/store/registries.actions.ts | 5 ++ .../registries/store/registries.model.ts | 3 +- .../registries/store/registries.selectors.ts | 5 ++ .../registries/store/registries.state.ts | 57 ++++++++++++++-- src/assets/i18n/en.json | 3 +- 12 files changed, 206 insertions(+), 52 deletions(-) create mode 100644 src/app/features/registries/models/registries-json-api.model.ts create mode 100644 src/app/features/registries/models/registries.model.ts diff --git a/src/app/features/registries/components/new-registration/new-registration.component.html b/src/app/features/registries/components/new-registration/new-registration.component.html index 52a6d56c9..6116c6feb 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.html +++ b/src/app/features/registries/components/new-registration/new-registration.component.html @@ -15,37 +15,57 @@

{{ ('registries.new.steps.title' | translate) + '1' }}

class="btn-full-width w-2 font-bold" severity="info" [label]="'common.buttons.yes' | translate" - [raised]="isProjectRegistration" - (click)="isProjectRegistration = true" + [raised]="fromProject" + (click)="toggleFromProject()" /> - -

{{ ('registries.new.steps.title' | translate) + '2' }}

-

{{ 'registries.new.steps.step2' | translate }}

-

{{ 'registries.new.steps.step2InfoText' | translate }}

-
- + @if (fromProject) { + +

{{ ('registries.new.steps.title' | translate) + '2' }}

+

{{ 'registries.new.steps.step2' | translate }}

+

{{ 'registries.new.steps.step2InfoText' | translate }}

+
+ +
+
+ } + +

{{ ('registries.new.steps.title' | translate) + (fromProject ? '3' : '2') }}

+

{{ 'registries.new.steps.step3' | translate }}

+
+ +
+
+
+
- - -

{{ ('registries.new.steps.title' | translate) + (isProjectRegistration ? '3' : '2') }}

-

{{ 'registries.new.steps.step3' | translate }}

-
- -
-
+ diff --git a/src/app/features/registries/components/new-registration/new-registration.component.ts b/src/app/features/registries/components/new-registration/new-registration.component.ts index 71401113c..217511821 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.ts +++ b/src/app/features/registries/components/new-registration/new-registration.component.ts @@ -7,31 +7,36 @@ import { Card } from 'primeng/card'; import { Select } from 'primeng/select'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { SubHeaderComponent } from '@osf/shared/components'; +import { ToastService } from '@osf/shared/services'; -import { Project } from '../../models'; -import { GetProjects, GetProviders, RegistriesSelectors } from '../../store'; +import { CreateDraft, GetProjects, GetProviders, RegistriesSelectors } from '../../store'; @Component({ selector: 'osf-new-registration', - imports: [SubHeaderComponent, TranslatePipe, Card, Button, Select], + imports: [SubHeaderComponent, TranslatePipe, Card, Button, ReactiveFormsModule, Select], templateUrl: './new-registration.component.html', styleUrl: './new-registration.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class NewRegistrationComponent { private readonly fb = inject(FormBuilder); + private readonly toastService = inject(ToastService); protected readonly projects = select(RegistriesSelectors.getProjects); protected readonly providers = select(RegistriesSelectors.getProviders); + protected readonly isDraftSubmitting = select(RegistriesSelectors.isDraftSubmitting); protected actions = createDispatchMap({ getProjects: GetProjects, getProviders: GetProviders, + createDraft: CreateDraft, }); - isProjectRegistration = true; + fromProject = false; + draftForm = this.fb.group({ - provider: [''], + provider: ['', Validators.required], + project: [''], }); constructor() { @@ -39,7 +44,36 @@ export class NewRegistrationComponent { this.actions.getProviders(); } - onSelectProject(project: Project) { - console.log('Project selected', project); + onSelectProject(projectId: string) { + this.draftForm.patchValue({ + project: projectId, + }); + } + + onSelectProvider(providerId: string) { + this.draftForm.patchValue({ + provider: providerId, + }); + } + + toggleFromProject() { + this.fromProject = !this.fromProject; + this.draftForm.get('project')?.setValidators(this.fromProject ? Validators.required : null); + this.draftForm.get('project')?.updateValueAndValidity(); + } + + createDraft() { + const { provider, project } = this.draftForm.value; + + if (this.draftForm.valid) { + this.actions + .createDraft({ + registrationSchemaId: provider!, + projectId: this.fromProject ? (project ?? undefined) : undefined, + }) + .subscribe(() => { + this.toastService.showSuccess('Draft created successfully'); + }); + } } } diff --git a/src/app/features/registries/models/registries-json-api.model.ts b/src/app/features/registries/models/registries-json-api.model.ts new file mode 100644 index 000000000..b9535ab03 --- /dev/null +++ b/src/app/features/registries/models/registries-json-api.model.ts @@ -0,0 +1,23 @@ +import { ApiData, MetaJsonApi, PaginationLinksJsonApi } from '@osf/core/models'; + +export interface RegistrationResponseJsonApi { + data: RegistrationDataJsonApi; + meta: MetaJsonApi; + links: PaginationLinksJsonApi; +} + +export type RegistrationDataJsonApi = ApiData; + +interface RegistrationAttributesJsonApi { + category: string; + current_user_permissions: string[]; + date_created: string; + datetime_updated: string; + description: string; + has_project: boolean; + node_license: string | null; + registration_metadata: Record; + registration_responses: Record; + tags: string[]; + title: string; +} diff --git a/src/app/features/registries/models/registries.model.ts b/src/app/features/registries/models/registries.model.ts new file mode 100644 index 000000000..e39086194 --- /dev/null +++ b/src/app/features/registries/models/registries.model.ts @@ -0,0 +1,3 @@ +export interface Registration { + id: string; +} diff --git a/src/app/features/registries/services/projects.service.ts b/src/app/features/registries/services/projects.service.ts index 6d051a176..414e5e497 100644 --- a/src/app/features/registries/services/projects.service.ts +++ b/src/app/features/registries/services/projects.service.ts @@ -22,7 +22,7 @@ export class ProjectsService { 'filter[current_user_permissions]': 'admin', }; return this.jsonApiService - .get(`${this.apiUrl}/nodes/`, params) + .get(`${this.apiUrl}/users/me/nodes/`, params) .pipe(map((response) => ProjectsMapper.fromProjectsResponse(response))); } } diff --git a/src/app/features/registries/services/providers.service.ts b/src/app/features/registries/services/providers.service.ts index 371016e65..721234bd8 100644 --- a/src/app/features/registries/services/providers.service.ts +++ b/src/app/features/registries/services/providers.service.ts @@ -18,11 +18,8 @@ export class ProvidersService { private readonly jsonApiService = inject(JsonApiService); getProviders(): Observable { - const params: Record = { - 'filter[current_user_permissions]': 'admin', - }; return this.jsonApiService - .get(`${this.apiUrl}/nodes/`, params) + .get(`${this.apiUrl}/providers/registrations/osf/schemas/`) .pipe(map((response) => ProvidersMapper.fromProvidersResponse(response))); } } diff --git a/src/app/features/registries/services/registries.service.ts b/src/app/features/registries/services/registries.service.ts index 65f7e9691..cd92686fd 100644 --- a/src/app/features/registries/services/registries.service.ts +++ b/src/app/features/registries/services/registries.service.ts @@ -1,7 +1,11 @@ +import { Observable } from 'rxjs'; + import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@osf/core/services'; +import { RegistrationDataJsonApi } from '../models/registries-json-api.model'; + import { environment } from 'src/environments/environment'; @Injectable({ @@ -11,12 +15,28 @@ export class RegistriesService { private apiUrl = environment.apiUrl; private readonly jsonApiService = inject(JsonApiService); - // getProjects(): Observable { - // const params: Record = { - // 'filter[current_user_permissions]': 'admin', - // }; - // return this.jsonApiService - // .get(`${this.apiUrl}/nodes/`, params) - // .pipe(map((response) => ProjectsMapper.fromProjectsResponse(response))); - // } + createDraft(registrationSchemaId: string, projectId?: string | undefined): Observable { + const payload = { + data: { + type: 'draft_registrations', + relationships: { + branched_from: projectId + ? { + data: { + type: 'nodes', + id: projectId, + }, + } + : undefined, + registration_schema: { + data: { + type: 'registration-schemas', + id: registrationSchemaId, + }, + }, + }, + }, + }; + return this.jsonApiService.post(`${this.apiUrl}/draft_registrations/`, payload); + } } diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index 0d5dadd4a..6afb4f009 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -5,3 +5,8 @@ export class GetProviders { export class GetProjects { static readonly type = '[Registries] Get Projects'; } + +export class CreateDraft { + static readonly type = '[Registries] Create Draft'; + constructor(public payload: { registrationSchemaId: string; projectId?: string }) {} +} diff --git a/src/app/features/registries/store/registries.model.ts b/src/app/features/registries/store/registries.model.ts index cf237eade..cf8d51dfe 100644 --- a/src/app/features/registries/store/registries.model.ts +++ b/src/app/features/registries/store/registries.model.ts @@ -1,9 +1,10 @@ import { AsyncStateModel } from '@osf/shared/models'; import { Project, Provider } from '../models'; +import { Registration } from '../models/registries.model'; export interface RegistriesStateModel { providers: AsyncStateModel; - currentProviderId: string | null; projects: AsyncStateModel; + registrations: AsyncStateModel; } diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts index 32cee3732..f63694d63 100644 --- a/src/app/features/registries/store/registries.selectors.ts +++ b/src/app/features/registries/store/registries.selectors.ts @@ -15,4 +15,9 @@ export class RegistriesSelectors { static getProjects(state: RegistriesStateModel): Project[] { return state.projects.data; } + + @Selector([RegistriesState]) + static isDraftSubmitting(state: RegistriesStateModel): boolean { + return state.registrations.isSubmitting ?? false; + } } diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index a5e509280..08f9a352f 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -1,13 +1,14 @@ import { Action, State, StateContext } from '@ngxs/store'; import { throwError } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Project } from '../models'; -import { ProjectsService, ProvidersService } from '../services'; +import { ProjectsService, ProvidersService, RegistriesService } from '../services'; -import { GetProjects, GetProviders } from './registries.actions'; +import { CreateDraft, GetProjects, GetProviders } from './registries.actions'; import { RegistriesStateModel } from './registries.model'; const DefaultState: RegistriesStateModel = { @@ -16,12 +17,17 @@ const DefaultState: RegistriesStateModel = { isLoading: false, error: null, }, - currentProviderId: null, projects: { data: [], isLoading: false, error: null, }, + registrations: { + isLoading: false, + data: null, + isSubmitting: false, + error: null, + }, }; @State({ @@ -32,7 +38,8 @@ const DefaultState: RegistriesStateModel = { export class RegistriesState { constructor( private providersService: ProvidersService, - private projectsService: ProjectsService + private projectsService: ProjectsService, + private registriesService: RegistriesService ) {} @Action(GetProjects) @@ -92,8 +99,46 @@ export class RegistriesState { }); } - private handleError(ctx: StateContext, error: Error) { - ctx.patchState({}); + @Action(CreateDraft) + createDraft(ctx: StateContext, { payload }: CreateDraft) { + ctx.patchState({ + registrations: { + ...ctx.getState().registrations, + isSubmitting: true, + }, + }); + + return this.registriesService.createDraft(payload.registrationSchemaId, payload.projectId).pipe( + tap(() => { + ctx.patchState({ + registrations: { + ...ctx.getState().registrations, + isSubmitting: false, + error: null, + }, + }); + }), + catchError((error) => { + ctx.patchState({ + registrations: { + ...ctx.getState().registrations, + isSubmitting: false, + error: error.message, + }, + }); + return this.handleError(ctx, 'registrations', error); + }) + ); + } + + private handleError(ctx: StateContext, section: keyof RegistriesStateModel, error: Error) { + ctx.patchState({ + [section]: { + ...ctx.getState()[section], + isLoading: false, + error: error.message, + }, + }); return throwError(() => error); } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 787e54439..8097ab69a 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -347,7 +347,8 @@ "step2": "Which project do you want to register?", "step3": "Which type of registration would you like to create?", "step2InfoText": "If your project includes components, you can select which components to include or exclude at the end of the registration." - } + }, + "createDraft": "Create draft" } }, "myProfile": { From ff1149b04462a2a4703867c66785478952e4e721 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Mon, 23 Jun 2025 17:36:43 +0300 Subject: [PATCH 04/15] feat(registries): fix comments --- src/app/app.routes.ts | 2 + .../core/constants/ngxs-states.constant.ts | 2 - .../institutions/moderation.service.ts | 51 ------------------- .../new-registration.component.ts | 9 +++- src/app/features/registries/mappers/index.ts | 2 + .../registries/mappers/projects.mapper.ts | 3 +- .../registries/mappers/providers.mapper.ts | 3 +- src/app/features/registries/models/index.ts | 4 ++ .../registries/registries.component.ts | 4 +- 9 files changed, 20 insertions(+), 60 deletions(-) delete mode 100644 src/app/features/institutions/moderation.service.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index f49f18beb..48df2606d 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -8,6 +8,7 @@ import { MyProfileResourceFiltersOptionsState } from './features/my-profile/comp import { MyProfileResourceFiltersState } from './features/my-profile/components/my-profile-resource-filters/store'; import { MyProfileState } from './features/my-profile/store'; import { ContributorsState } from './features/project/contributors/store'; +import { RegistriesState } from './features/registries/store'; import { ResourceFiltersOptionsState } from './features/search/components/filters/store'; import { ResourceFiltersState } from './features/search/components/resource-filters/store'; import { SearchState } from './features/search/store'; @@ -166,6 +167,7 @@ export const routes: Routes = [ path: 'registries', loadComponent: () => import('./features/registries/registries.component').then((mod) => mod.RegistriesComponent), + providers: [provideStates([RegistriesState])], children: [ { path: 'new', diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index b54e6fd70..7f622dcdd 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -9,7 +9,6 @@ import { ProjectOverviewState } from '@osf/features/project/overview/store'; import { RegistrationsState } from '@osf/features/project/registrations/store'; import { SettingsState } from '@osf/features/project/settings/store'; import { WikiState } from '@osf/features/project/wiki/store/wiki.state'; -import { RegistriesState } from '@osf/features/registries/store'; import { AccountSettingsState } from '@osf/features/settings/account-settings/store/account-settings.state'; import { DeveloperAppsState } from '@osf/features/settings/developer-apps/store'; import { NotificationSubscriptionState } from '@osf/features/settings/notifications/store'; @@ -35,5 +34,4 @@ export const STATES = [ WikiState, MeetingsState, RegistrationsState, - RegistriesState, ]; diff --git a/src/app/features/institutions/moderation.service.ts b/src/app/features/institutions/moderation.service.ts deleted file mode 100644 index 1ba57fd09..000000000 --- a/src/app/features/institutions/moderation.service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { map, Observable, of } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; -import { JsonApiService } from '@osf/core/services'; -import { PaginatedData } from '@osf/shared/models'; - -import { ModerationMapper } from '../mappers'; -import { ModeratorAddModel, ModeratorModel, ModeratorResponseJsonApi } from '../models'; - -import { environment } from 'src/environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class ModerationService { - private readonly tesModeratorsUrl = 'assets/collection-moderators.json'; - private readonly jsonApiService = inject(JsonApiService); - - getCollectionModerators(providerId: string): Observable { - return ( - this.jsonApiService - // .get(`${this.baseUrl}/providers/collections/${providerId}/moderators/`) - .get(this.tesModeratorsUrl) - .pipe(map((response) => response.data.map((moderator) => ModerationMapper.fromModeratorResponse(moderator)))) - ); - } - - addCollectionModerator(providerId: string, data: ModeratorAddModel): Observable { - return of({} as ModeratorModel); - } - - updateCollectionModerator(providerId: string, data: ModeratorAddModel): Observable { - return of({} as ModeratorModel); - } - - deleteCollectionModerator(providerId: string, userId: string): Observable { - const baseUrl = ``; - - return this.jsonApiService.delete(baseUrl); - } - - searchUsers(value: string, page = 1): Observable> { - const baseUrl = `${environment.apiUrl}/users/?filter[full_name]=${value}&page=${page}`; - - return this.jsonApiService - .get>(baseUrl) - .pipe(map((response) => ModerationMapper.fromUsersWithPaginationGetResponse(response))); - } -} diff --git a/src/app/features/registries/components/new-registration/new-registration.component.ts b/src/app/features/registries/components/new-registration/new-registration.component.ts index 217511821..0bbab2b19 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.ts +++ b/src/app/features/registries/components/new-registration/new-registration.component.ts @@ -6,7 +6,7 @@ import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; import { Select } from 'primeng/select'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { SubHeaderComponent } from '@osf/shared/components'; @@ -42,6 +42,13 @@ export class NewRegistrationComponent { constructor() { this.actions.getProjects(); this.actions.getProviders(); + effect(() => { + //set the provider value when the providers are loaded + const provider = this.draftForm.get('provider')?.value; + if (!provider) { + this.draftForm.get('provider')?.setValue(this.providers()[0]?.id); + } + }); } onSelectProject(projectId: string) { diff --git a/src/app/features/registries/mappers/index.ts b/src/app/features/registries/mappers/index.ts index e69de29bb..43fcc678e 100644 --- a/src/app/features/registries/mappers/index.ts +++ b/src/app/features/registries/mappers/index.ts @@ -0,0 +1,2 @@ +export * from './projects.mapper'; +export * from './providers.mapper'; diff --git a/src/app/features/registries/mappers/projects.mapper.ts b/src/app/features/registries/mappers/projects.mapper.ts index 4583e4583..df0729851 100644 --- a/src/app/features/registries/mappers/projects.mapper.ts +++ b/src/app/features/registries/mappers/projects.mapper.ts @@ -1,5 +1,4 @@ -import { Project } from '../models'; -import { ProjectsResponseJsonApi } from '../models/projects-json-api.model'; +import { Project, ProjectsResponseJsonApi } from '../models'; export class ProjectsMapper { static fromProjectsResponse(response: ProjectsResponseJsonApi): Project[] { diff --git a/src/app/features/registries/mappers/providers.mapper.ts b/src/app/features/registries/mappers/providers.mapper.ts index 445769db9..9442ade0a 100644 --- a/src/app/features/registries/mappers/providers.mapper.ts +++ b/src/app/features/registries/mappers/providers.mapper.ts @@ -1,5 +1,4 @@ -import { Provider } from '../models'; -import { ProvidersResponseJsonApi } from '../models/providers-json-api.model'; +import { Provider, ProvidersResponseJsonApi } from '../models'; export class ProvidersMapper { static fromProvidersResponse(response: ProvidersResponseJsonApi): Provider[] { diff --git a/src/app/features/registries/models/index.ts b/src/app/features/registries/models/index.ts index e5d1d4825..f18645392 100644 --- a/src/app/features/registries/models/index.ts +++ b/src/app/features/registries/models/index.ts @@ -1,2 +1,6 @@ export * from './project'; +export * from './projects-json-api.model'; export * from './provider.model'; +export * from './providers-json-api.model'; +export * from './registries.model'; +export * from './registries-json-api.model'; diff --git a/src/app/features/registries/registries.component.ts b/src/app/features/registries/registries.component.ts index a3009cbcc..78716bee1 100644 --- a/src/app/features/registries/registries.component.ts +++ b/src/app/features/registries/registries.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { RouterOutlet } from '@angular/router'; @Component({ selector: 'osf-registries', - imports: [RouterModule], + imports: [RouterOutlet], templateUrl: './registries.component.html', styleUrl: './registries.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, From f3a589ac3babe97ad18eda22cab3266ed81c6caf Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Tue, 24 Jun 2025 10:42:14 +0300 Subject: [PATCH 05/15] feat(registries): setup routes --- .../custom-step/custom-step.component.html | 1 + .../custom-step/custom-step.component.scss | 0 .../custom-step/custom-step.component.spec.ts | 22 +++++++++++ .../custom-step/custom-step.component.ts | 10 +++++ .../components/drafts/drafts.component.html | 2 + .../components/drafts/drafts.component.scss | 0 .../drafts/drafts.component.spec.ts | 22 +++++++++++ .../components/drafts/drafts.component.ts | 11 ++++++ .../contributors/contributors.component.html | 1 + .../contributors/contributors.component.scss | 0 .../contributors.component.spec.ts | 22 +++++++++++ .../contributors/contributors.component.ts | 10 +++++ .../metadata/license/license.component.html | 1 + .../metadata/license/license.component.scss | 0 .../license/license.component.spec.ts | 22 +++++++++++ .../metadata/license/license.component.ts | 10 +++++ .../metadata/metadata.component.html | 18 +++++++++ .../metadata/metadata.component.scss | 0 .../metadata/metadata.component.spec.ts | 22 +++++++++++ .../components/metadata/metadata.component.ts | 37 +++++++++++++++++++ .../metadata/subjects/subjects.component.html | 1 + .../metadata/subjects/subjects.component.scss | 0 .../subjects/subjects.component.spec.ts | 22 +++++++++++ .../metadata/subjects/subjects.component.ts | 10 +++++ .../metadata/tags/tags.component.html | 1 + .../metadata/tags/tags.component.scss | 0 .../metadata/tags/tags.component.spec.ts | 22 +++++++++++ .../metadata/tags/tags.component.ts | 10 +++++ .../components/review/review.component.html | 1 + .../components/review/review.component.scss | 0 .../review/review.component.spec.ts | 22 +++++++++++ .../components/review/review.component.ts | 10 +++++ 32 files changed, 310 insertions(+) create mode 100644 src/app/features/registries/components/custom-step/custom-step.component.html create mode 100644 src/app/features/registries/components/custom-step/custom-step.component.scss create mode 100644 src/app/features/registries/components/custom-step/custom-step.component.spec.ts create mode 100644 src/app/features/registries/components/custom-step/custom-step.component.ts create mode 100644 src/app/features/registries/components/drafts/drafts.component.html create mode 100644 src/app/features/registries/components/drafts/drafts.component.scss create mode 100644 src/app/features/registries/components/drafts/drafts.component.spec.ts create mode 100644 src/app/features/registries/components/drafts/drafts.component.ts create mode 100644 src/app/features/registries/components/metadata/contributors/contributors.component.html create mode 100644 src/app/features/registries/components/metadata/contributors/contributors.component.scss create mode 100644 src/app/features/registries/components/metadata/contributors/contributors.component.spec.ts create mode 100644 src/app/features/registries/components/metadata/contributors/contributors.component.ts create mode 100644 src/app/features/registries/components/metadata/license/license.component.html create mode 100644 src/app/features/registries/components/metadata/license/license.component.scss create mode 100644 src/app/features/registries/components/metadata/license/license.component.spec.ts create mode 100644 src/app/features/registries/components/metadata/license/license.component.ts create mode 100644 src/app/features/registries/components/metadata/metadata.component.html create mode 100644 src/app/features/registries/components/metadata/metadata.component.scss create mode 100644 src/app/features/registries/components/metadata/metadata.component.spec.ts create mode 100644 src/app/features/registries/components/metadata/metadata.component.ts create mode 100644 src/app/features/registries/components/metadata/subjects/subjects.component.html create mode 100644 src/app/features/registries/components/metadata/subjects/subjects.component.scss create mode 100644 src/app/features/registries/components/metadata/subjects/subjects.component.spec.ts create mode 100644 src/app/features/registries/components/metadata/subjects/subjects.component.ts create mode 100644 src/app/features/registries/components/metadata/tags/tags.component.html create mode 100644 src/app/features/registries/components/metadata/tags/tags.component.scss create mode 100644 src/app/features/registries/components/metadata/tags/tags.component.spec.ts create mode 100644 src/app/features/registries/components/metadata/tags/tags.component.ts create mode 100644 src/app/features/registries/components/review/review.component.html create mode 100644 src/app/features/registries/components/review/review.component.scss create mode 100644 src/app/features/registries/components/review/review.component.spec.ts create mode 100644 src/app/features/registries/components/review/review.component.ts diff --git a/src/app/features/registries/components/custom-step/custom-step.component.html b/src/app/features/registries/components/custom-step/custom-step.component.html new file mode 100644 index 000000000..68a42463d --- /dev/null +++ b/src/app/features/registries/components/custom-step/custom-step.component.html @@ -0,0 +1 @@ +

custom-step works!

diff --git a/src/app/features/registries/components/custom-step/custom-step.component.scss b/src/app/features/registries/components/custom-step/custom-step.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/components/custom-step/custom-step.component.spec.ts b/src/app/features/registries/components/custom-step/custom-step.component.spec.ts new file mode 100644 index 000000000..6c65cc772 --- /dev/null +++ b/src/app/features/registries/components/custom-step/custom-step.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CustomStepComponent } from './custom-step.component'; + +describe('CustomStepComponent', () => { + let component: CustomStepComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CustomStepComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CustomStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/custom-step/custom-step.component.ts b/src/app/features/registries/components/custom-step/custom-step.component.ts new file mode 100644 index 000000000..d6541af2c --- /dev/null +++ b/src/app/features/registries/components/custom-step/custom-step.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-custom-step', + imports: [], + templateUrl: './custom-step.component.html', + styleUrl: './custom-step.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CustomStepComponent {} diff --git a/src/app/features/registries/components/drafts/drafts.component.html b/src/app/features/registries/components/drafts/drafts.component.html new file mode 100644 index 000000000..ae6bfb804 --- /dev/null +++ b/src/app/features/registries/components/drafts/drafts.component.html @@ -0,0 +1,2 @@ +

drafts works!

+ diff --git a/src/app/features/registries/components/drafts/drafts.component.scss b/src/app/features/registries/components/drafts/drafts.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/components/drafts/drafts.component.spec.ts b/src/app/features/registries/components/drafts/drafts.component.spec.ts new file mode 100644 index 000000000..43a3cc2bb --- /dev/null +++ b/src/app/features/registries/components/drafts/drafts.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DraftsComponent } from './drafts.component'; + +describe('DraftsComponent', () => { + let component: DraftsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DraftsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DraftsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/drafts/drafts.component.ts b/src/app/features/registries/components/drafts/drafts.component.ts new file mode 100644 index 000000000..d563a2aac --- /dev/null +++ b/src/app/features/registries/components/drafts/drafts.component.ts @@ -0,0 +1,11 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'osf-drafts', + imports: [RouterOutlet], + templateUrl: './drafts.component.html', + styleUrl: './drafts.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DraftsComponent {} diff --git a/src/app/features/registries/components/metadata/contributors/contributors.component.html b/src/app/features/registries/components/metadata/contributors/contributors.component.html new file mode 100644 index 000000000..2ce2ae917 --- /dev/null +++ b/src/app/features/registries/components/metadata/contributors/contributors.component.html @@ -0,0 +1 @@ +

contributors works!

diff --git a/src/app/features/registries/components/metadata/contributors/contributors.component.scss b/src/app/features/registries/components/metadata/contributors/contributors.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/components/metadata/contributors/contributors.component.spec.ts b/src/app/features/registries/components/metadata/contributors/contributors.component.spec.ts new file mode 100644 index 000000000..fb45f2cde --- /dev/null +++ b/src/app/features/registries/components/metadata/contributors/contributors.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContributorsComponent } from './contributors.component'; + +describe('ContributorsComponent', () => { + let component: ContributorsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ContributorsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ContributorsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/metadata/contributors/contributors.component.ts b/src/app/features/registries/components/metadata/contributors/contributors.component.ts new file mode 100644 index 000000000..cbb11455f --- /dev/null +++ b/src/app/features/registries/components/metadata/contributors/contributors.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-contributors', + imports: [], + templateUrl: './contributors.component.html', + styleUrl: './contributors.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ContributorsComponent {} diff --git a/src/app/features/registries/components/metadata/license/license.component.html b/src/app/features/registries/components/metadata/license/license.component.html new file mode 100644 index 000000000..c2800e0d3 --- /dev/null +++ b/src/app/features/registries/components/metadata/license/license.component.html @@ -0,0 +1 @@ +

license works!

diff --git a/src/app/features/registries/components/metadata/license/license.component.scss b/src/app/features/registries/components/metadata/license/license.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/components/metadata/license/license.component.spec.ts b/src/app/features/registries/components/metadata/license/license.component.spec.ts new file mode 100644 index 000000000..4cd128657 --- /dev/null +++ b/src/app/features/registries/components/metadata/license/license.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LicenseComponent } from './license.component'; + +describe('LicenseComponent', () => { + let component: LicenseComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LicenseComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(LicenseComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/metadata/license/license.component.ts b/src/app/features/registries/components/metadata/license/license.component.ts new file mode 100644 index 000000000..19d291552 --- /dev/null +++ b/src/app/features/registries/components/metadata/license/license.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-license', + imports: [], + templateUrl: './license.component.html', + styleUrl: './license.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LicenseComponent {} diff --git a/src/app/features/registries/components/metadata/metadata.component.html b/src/app/features/registries/components/metadata/metadata.component.html new file mode 100644 index 000000000..62645a643 --- /dev/null +++ b/src/app/features/registries/components/metadata/metadata.component.html @@ -0,0 +1,18 @@ +

{{ " 'registries.new.metadata.title' | translate" }}

+

{{ "'registries.new.metadata.description' | translate" }}

+
+ +
+ + + + + +
+
+
+
+ + +
+
diff --git a/src/app/features/registries/components/metadata/metadata.component.scss b/src/app/features/registries/components/metadata/metadata.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/components/metadata/metadata.component.spec.ts b/src/app/features/registries/components/metadata/metadata.component.spec.ts new file mode 100644 index 000000000..311ac4e9f --- /dev/null +++ b/src/app/features/registries/components/metadata/metadata.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MetadataComponent } from './metadata.component'; + +describe('MetadataComponent', () => { + let component: MetadataComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MetadataComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MetadataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/metadata/metadata.component.ts b/src/app/features/registries/components/metadata/metadata.component.ts new file mode 100644 index 000000000..d9feda205 --- /dev/null +++ b/src/app/features/registries/components/metadata/metadata.component.ts @@ -0,0 +1,37 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; + +import { TextInputComponent } from '@osf/shared/components'; +import { InputLimits } from '@osf/shared/constants'; + +@Component({ + selector: 'osf-metadata', + imports: [Card, TextInputComponent, ReactiveFormsModule, Button, TranslatePipe], + templateUrl: './metadata.component.html', + styleUrl: './metadata.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MetadataComponent { + private readonly fb = inject(FormBuilder); + protected inputLimits = InputLimits; + + metadataForm = this.fb.group({ + title: ['', Validators.required], + description: [''], + }); + + submitMetadata(): void { + // Logic to submit metadata + console.log('Metadata submitted'); + } + + back(): void { + // Logic to navigate back + console.log('Navigating back'); + } +} diff --git a/src/app/features/registries/components/metadata/subjects/subjects.component.html b/src/app/features/registries/components/metadata/subjects/subjects.component.html new file mode 100644 index 000000000..290fbd8b7 --- /dev/null +++ b/src/app/features/registries/components/metadata/subjects/subjects.component.html @@ -0,0 +1 @@ +

subjects works!

diff --git a/src/app/features/registries/components/metadata/subjects/subjects.component.scss b/src/app/features/registries/components/metadata/subjects/subjects.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/components/metadata/subjects/subjects.component.spec.ts b/src/app/features/registries/components/metadata/subjects/subjects.component.spec.ts new file mode 100644 index 000000000..ba5495891 --- /dev/null +++ b/src/app/features/registries/components/metadata/subjects/subjects.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubjectsComponent } from './subjects.component'; + +describe('SubjectsComponent', () => { + let component: SubjectsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SubjectsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SubjectsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/metadata/subjects/subjects.component.ts b/src/app/features/registries/components/metadata/subjects/subjects.component.ts new file mode 100644 index 000000000..ddbd541ab --- /dev/null +++ b/src/app/features/registries/components/metadata/subjects/subjects.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-subjects', + imports: [], + templateUrl: './subjects.component.html', + styleUrl: './subjects.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SubjectsComponent {} diff --git a/src/app/features/registries/components/metadata/tags/tags.component.html b/src/app/features/registries/components/metadata/tags/tags.component.html new file mode 100644 index 000000000..1951e16ee --- /dev/null +++ b/src/app/features/registries/components/metadata/tags/tags.component.html @@ -0,0 +1 @@ +

tags works!

diff --git a/src/app/features/registries/components/metadata/tags/tags.component.scss b/src/app/features/registries/components/metadata/tags/tags.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/components/metadata/tags/tags.component.spec.ts b/src/app/features/registries/components/metadata/tags/tags.component.spec.ts new file mode 100644 index 000000000..235314eac --- /dev/null +++ b/src/app/features/registries/components/metadata/tags/tags.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TagsComponent } from './tags.component'; + +describe('TagsComponent', () => { + let component: TagsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TagsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TagsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/metadata/tags/tags.component.ts b/src/app/features/registries/components/metadata/tags/tags.component.ts new file mode 100644 index 000000000..6105502df --- /dev/null +++ b/src/app/features/registries/components/metadata/tags/tags.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-tags', + imports: [], + templateUrl: './tags.component.html', + styleUrl: './tags.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TagsComponent {} diff --git a/src/app/features/registries/components/review/review.component.html b/src/app/features/registries/components/review/review.component.html new file mode 100644 index 000000000..27a7cd2a1 --- /dev/null +++ b/src/app/features/registries/components/review/review.component.html @@ -0,0 +1 @@ +

review works!

diff --git a/src/app/features/registries/components/review/review.component.scss b/src/app/features/registries/components/review/review.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/components/review/review.component.spec.ts b/src/app/features/registries/components/review/review.component.spec.ts new file mode 100644 index 000000000..f555378f7 --- /dev/null +++ b/src/app/features/registries/components/review/review.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ReviewComponent } from './review.component'; + +describe('ReviewComponent', () => { + let component: ReviewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReviewComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ReviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/review/review.component.ts b/src/app/features/registries/components/review/review.component.ts new file mode 100644 index 000000000..e38cb2e4e --- /dev/null +++ b/src/app/features/registries/components/review/review.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-review', + imports: [], + templateUrl: './review.component.html', + styleUrl: './review.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReviewComponent {} From fd429e49fbceaf699e3d8a60a05e46e315194541 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Tue, 24 Jun 2025 10:44:19 +0300 Subject: [PATCH 06/15] feat(registries): setup routes --- src/app/app.routes.ts | 26 ++++++++++++++++++++++++++ src/assets/i18n/en.json | 9 ++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 3bc7ed669..60057b2c6 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -176,6 +176,32 @@ export const routes: Routes = [ (mod) => mod.NewRegistrationComponent ), }, + { + path: 'drafts', + loadComponent: () => + import('./features/registries/components/drafts/drafts.component').then((mod) => mod.DraftsComponent), + children: [ + { + path: ':id/metadata', + loadComponent: () => + import('./features/registries/components/metadata/metadata.component').then( + (mod) => mod.MetadataComponent + ), + }, + { + path: ':id/review', + loadComponent: () => + import('./features/registries/components/review/review.component').then((mod) => mod.ReviewComponent), + }, + { + path: ':id/:step', + loadComponent: () => + import('./features/registries/components/custom-step/custom-step.component').then( + (mod) => mod.CustomStepComponent + ), + }, + ], + }, ], }, { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 400f2ba75..0e4646f54 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -17,6 +17,7 @@ "disconnect": "Disconnect", "revert": "Revert", "next": "Next", + "back": "Back", "skip": "Skip", "done": "Done", "select": "Select", @@ -38,7 +39,9 @@ } }, "labels": { - "downloads": "Downloads" + "downloads": "Downloads", + "title": "Title", + "description": "Description" }, "deleteConfirmation": { "header": "Delete", @@ -352,6 +355,10 @@ "step3": "Which type of registration would you like to create?", "step2InfoText": "If your project includes components, you can select which components to include or exclude at the end of the registration." }, + "metadata": { + "title": "Registration Metadata", + "description": "This metadata applies only to the registration you are creating, and will not be applied to your project." + }, "createDraft": "Create draft" } }, From fe74ebf52cf7d93f5d1da15d01d70adf8a19cbd8 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Tue, 24 Jun 2025 17:44:42 +0300 Subject: [PATCH 07/15] feat(registries): add contributors section to metadata step --- src/app/app.routes.ts | 2 +- .../components/drafts/drafts.component.html | 1 - .../components/drafts/drafts.component.scss | 3 + .../contributors/contributors.component.html | 26 ++- .../contributors/contributors.component.ts | 208 +++++++++++++++++- .../metadata/metadata.component.html | 44 ++-- .../components/metadata/metadata.component.ts | 42 +++- .../registries/models/registries.model.ts | 2 + .../registries/services/registries.service.ts | 76 ++++++- .../registries/store/registries.actions.ts | 40 ++++ .../registries/store/registries.model.ts | 4 +- .../registries/store/registries.selectors.ts | 7 +- .../registries/store/registries.state.ts | 150 ++++++++++++- src/assets/i18n/en.json | 14 +- 14 files changed, 575 insertions(+), 44 deletions(-) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index fc2ea8b29..9068db62e 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -167,7 +167,7 @@ export const routes: Routes = [ path: 'registries', loadComponent: () => import('./features/registries/registries.component').then((mod) => mod.RegistriesComponent), - providers: [provideStates([RegistriesState])], + providers: [provideStates([RegistriesState, ContributorsState])], children: [ { path: 'new', diff --git a/src/app/features/registries/components/drafts/drafts.component.html b/src/app/features/registries/components/drafts/drafts.component.html index ae6bfb804..0680b43f9 100644 --- a/src/app/features/registries/components/drafts/drafts.component.html +++ b/src/app/features/registries/components/drafts/drafts.component.html @@ -1,2 +1 @@ -

drafts works!

diff --git a/src/app/features/registries/components/drafts/drafts.component.scss b/src/app/features/registries/components/drafts/drafts.component.scss index e69de29bb..683ae23fa 100644 --- a/src/app/features/registries/components/drafts/drafts.component.scss +++ b/src/app/features/registries/components/drafts/drafts.component.scss @@ -0,0 +1,3 @@ +:host { + height: 100%; +} diff --git a/src/app/features/registries/components/metadata/contributors/contributors.component.html b/src/app/features/registries/components/metadata/contributors/contributors.component.html index 2ce2ae917..256af9e13 100644 --- a/src/app/features/registries/components/metadata/contributors/contributors.component.html +++ b/src/app/features/registries/components/metadata/contributors/contributors.component.html @@ -1 +1,25 @@ -

contributors works!

+ +

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

+ +
+ @if (hasChanges) { +
+ + +
+ } + +
+
diff --git a/src/app/features/registries/components/metadata/contributors/contributors.component.ts b/src/app/features/registries/components/metadata/contributors/contributors.component.ts index cbb11455f..34d98c634 100644 --- a/src/app/features/registries/components/metadata/contributors/contributors.component.ts +++ b/src/app/features/registries/components/metadata/contributors/contributors.component.ts @@ -1,10 +1,212 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { DialogService } from 'primeng/dynamicdialog'; +import { TableModule } from 'primeng/table'; + +import { filter, forkJoin, map, of } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; + +import { + AddContributor, + DeleteContributor, + FetchContributors, + RegistriesSelectors, + UpdateContributor, +} from '@osf/features/registries/store'; +import { EducationHistoryDialogComponent, EmploymentHistoryDialogComponent } from '@osf/shared/components'; +import { + AddContributorDialogComponent, + AddUnregisteredContributorDialogComponent, + ContributorsListComponent, +} from '@osf/shared/components/contributors'; +import { BIBLIOGRAPHY_OPTIONS, PERMISSION_OPTIONS } from '@osf/shared/components/contributors/constants'; +import { AddContributorType, ContributorPermission } from '@osf/shared/components/contributors/enums'; +import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/components/contributors/models'; +import { ContributorsSelectors } from '@osf/shared/components/contributors/store'; +import { SelectOption } from '@osf/shared/models'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; +import { findChangedItems } from '@osf/shared/utils'; @Component({ selector: 'osf-contributors', - imports: [], + imports: [FormsModule, TableModule, ContributorsListComponent, TranslatePipe, Card, Button], templateUrl: './contributors.component.html', styleUrl: './contributors.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + providers: [DialogService], }) -export class ContributorsComponent {} +export class ContributorsComponent implements OnInit { + readonly destroyRef = inject(DestroyRef); + readonly translateService = inject(TranslateService); + readonly dialogService = inject(DialogService); + readonly toastService = inject(ToastService); + readonly customConfirmationService = inject(CustomConfirmationService); + + private readonly route = inject(ActivatedRoute); + private readonly draftId = toSignal(this.route.params.pipe(map((params) => params['id'])) ?? of(undefined)); + + protected readonly selectedPermission = signal(null); + protected readonly selectedBibliography = signal(null); + protected readonly permissionsOptions: SelectOption[] = PERMISSION_OPTIONS; + protected readonly bibliographyOptions: SelectOption[] = BIBLIOGRAPHY_OPTIONS; + + protected initialContributors = select(RegistriesSelectors.getContributors); + protected contributors = signal([]); + + protected readonly isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); + + protected actions = createDispatchMap({ + getContributors: FetchContributors, + deleteContributor: DeleteContributor, + updateContributor: UpdateContributor, + addContributor: AddContributor, + }); + + get hasChanges(): boolean { + return JSON.stringify(this.initialContributors()) !== JSON.stringify(this.contributors()); + } + + constructor() { + effect(() => { + this.contributors.set(JSON.parse(JSON.stringify(this.initialContributors()))); + }); + } + + ngOnInit(): void { + const draftId = this.draftId(); + if (draftId) { + this.actions.getContributors(draftId); + } + } + + onFocusOut() { + // TODO: make request to update contributor if changed + console.log('Focus out event:', 'Changed:', this.hasChanges); + } + + cancel() { + this.contributors.set(JSON.parse(JSON.stringify(this.initialContributors()))); + } + + save() { + const updatedContributors = findChangedItems(this.initialContributors(), this.contributors(), 'id'); + + const updateRequests = updatedContributors.map((payload) => + this.actions.updateContributor(this.draftId(), payload) + ); + + forkJoin(updateRequests).subscribe(() => { + this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage'); + }); + } + + openEmploymentHistory(contributor: ContributorModel) { + this.dialogService.open(EmploymentHistoryDialogComponent, { + width: '552px', + data: contributor.employment, + focusOnShow: false, + header: this.translateService.instant('project.contributors.table.headers.employment'), + closeOnEscape: true, + modal: true, + closable: true, + }); + } + + openEducationHistory(contributor: ContributorModel) { + this.dialogService.open(EducationHistoryDialogComponent, { + width: '552px', + data: contributor.education, + focusOnShow: false, + header: this.translateService.instant('project.contributors.table.headers.education'), + closeOnEscape: true, + modal: true, + closable: true, + }); + } + + openAddContributorDialog() { + const addedContributorIds = this.initialContributors().map((x) => x.userId); + + this.dialogService + .open(AddContributorDialogComponent, { + width: '448px', + data: addedContributorIds, + focusOnShow: false, + header: this.translateService.instant('project.contributors.addDialog.addRegisteredContributor'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe( + filter((res: ContributorDialogAddModel) => !!res), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((res: ContributorDialogAddModel) => { + if (res.type === AddContributorType.Unregistered) { + this.openAddUnregisteredContributorDialog(); + } else { + const addRequests = res.data.map((payload) => this.actions.addContributor(this.draftId(), payload)); + + forkJoin(addRequests).subscribe(() => { + this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); + }); + } + }); + } + + openAddUnregisteredContributorDialog() { + this.dialogService + .open(AddUnregisteredContributorDialogComponent, { + width: '448px', + focusOnShow: false, + header: this.translateService.instant('project.contributors.addDialog.addUnregisteredContributor'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe( + filter((res: ContributorDialogAddModel) => !!res), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((res: ContributorDialogAddModel) => { + if (res.type === AddContributorType.Registered) { + this.openAddContributorDialog(); + } else { + const successMessage = this.translateService.instant('project.contributors.toastMessages.addSuccessMessage'); + const params = { name: res.data[0].fullName }; + + this.actions.addContributor(this.draftId(), res.data[0]).subscribe({ + next: () => this.toastService.showSuccess(successMessage, params), + }); + } + }); + } + + removeContributor(contributor: ContributorModel) { + this.customConfirmationService.confirmDelete({ + headerKey: 'project.contributors.removeDialog.title', + messageKey: 'project.contributors.removeDialog.message', + messageParams: { name: contributor.fullName }, + acceptLabelKey: 'common.buttons.remove', + onConfirm: () => { + this.actions + .deleteContributor(this.draftId(), contributor.userId) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => + this.toastService.showSuccess('project.contributors.removeDialog.successMessage', { + name: contributor.fullName, + }), + }); + }, + }); + } +} diff --git a/src/app/features/registries/components/metadata/metadata.component.html b/src/app/features/registries/components/metadata/metadata.component.html index 62645a643..de1b7dec8 100644 --- a/src/app/features/registries/components/metadata/metadata.component.html +++ b/src/app/features/registries/components/metadata/metadata.component.html @@ -1,18 +1,34 @@ -

{{ " 'registries.new.metadata.title' | translate" }}

-

{{ "'registries.new.metadata.description' | translate" }}

-
- -
- - - - - -
-
-
+
+
+

{{ 'registries.metadata.title' | translate }}

+

{{ 'registries.metadata.description' | translate }}

+ +
+ + +
+ + +
+
+
+
+
+
- + +
diff --git a/src/app/features/registries/components/metadata/metadata.component.ts b/src/app/features/registries/components/metadata/metadata.component.ts index d9feda205..a2e083046 100644 --- a/src/app/features/registries/components/metadata/metadata.component.ts +++ b/src/app/features/registries/components/metadata/metadata.component.ts @@ -1,17 +1,34 @@ +import { createDispatchMap } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; +import { TextareaModule } from 'primeng/textarea'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; import { TextInputComponent } from '@osf/shared/components'; import { InputLimits } from '@osf/shared/constants'; +import { CustomValidators } from '@osf/shared/utils'; + +import { DeleteDraft } from '../../store'; + +import { ContributorsComponent } from './contributors/contributors.component'; @Component({ selector: 'osf-metadata', - imports: [Card, TextInputComponent, ReactiveFormsModule, Button, TranslatePipe], + imports: [ + Card, + TextInputComponent, + ReactiveFormsModule, + Button, + TranslatePipe, + TextareaModule, + ContributorsComponent, + ], templateUrl: './metadata.component.html', styleUrl: './metadata.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -19,19 +36,28 @@ import { InputLimits } from '@osf/shared/constants'; export class MetadataComponent { private readonly fb = inject(FormBuilder); protected inputLimits = InputLimits; + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly draftId = this.route.snapshot.params['id']; + + protected actions = createDispatchMap({ + deleteDraft: DeleteDraft, + }); metadataForm = this.fb.group({ - title: ['', Validators.required], - description: [''], + title: ['', CustomValidators.requiredTrimmed()], + description: ['', CustomValidators.requiredTrimmed()], }); submitMetadata(): void { - // Logic to submit metadata console.log('Metadata submitted'); } - back(): void { - // Logic to navigate back - console.log('Navigating back'); + deleteDraft(): void { + this.actions.deleteDraft(this.draftId).subscribe({ + next: () => { + this.router.navigateByUrl('/registries/new'); + }, + }); } } diff --git a/src/app/features/registries/models/registries.model.ts b/src/app/features/registries/models/registries.model.ts index e39086194..f5dbab474 100644 --- a/src/app/features/registries/models/registries.model.ts +++ b/src/app/features/registries/models/registries.model.ts @@ -1,3 +1,5 @@ export interface Registration { id: string; + title: string; + description: string; } diff --git a/src/app/features/registries/services/registries.service.ts b/src/app/features/registries/services/registries.service.ts index cd92686fd..9d5dc6f72 100644 --- a/src/app/features/registries/services/registries.service.ts +++ b/src/app/features/registries/services/registries.service.ts @@ -1,9 +1,14 @@ -import { Observable } from 'rxjs'; +import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { JsonApiResponse } from '@osf/core/models'; import { JsonApiService } from '@osf/core/services'; +import { AddContributorType } from '@osf/shared/components/contributors/enums'; +import { ContributorsMapper } from '@osf/shared/components/contributors/mappers'; +import { ContributorAddModel, ContributorModel, ContributorResponse } from '@osf/shared/components/contributors/models'; +import { Registration } from '../models'; import { RegistrationDataJsonApi } from '../models/registries-json-api.model'; import { environment } from 'src/environments/environment'; @@ -39,4 +44,73 @@ export class RegistriesService { }; return this.jsonApiService.post(`${this.apiUrl}/draft_registrations/`, payload); } + + updateDraft(draftId: string, data: Registration): Observable { + const payload = { + data: { + id: draftId, + type: 'draft_registrations', + attributes: { ...data }, + relationships: {}, + }, + }; + return this.jsonApiService.patch(`${this.apiUrl}/draft_registrations/${draftId}/`, payload); + } + + // addContributor(draftId: string, userId: string, permission: string): Observable { + // const payload = { + // data: { + // type: 'contributors', + // attributes: { + // permission, + // }, + // relationships: { + // users: { + // data: [ + // { + // type: 'users', + // id: userId, + // }, + // ], + // }, + // }, + // }, + // }; + // return this.jsonApiService.post(`${this.apiUrl}/draft_registrations/${draftId}/contributors/`, payload); + // } + + deleteDraft(draftId: string): Observable { + return this.jsonApiService.delete(`${this.apiUrl}/draft_registrations/${draftId}/`); + } + + getContributors(draftId: string): Observable { + return this.jsonApiService + .get>(`${this.apiUrl}/draft_registrations/${draftId}/contributors/`) + .pipe(map((contributors) => ContributorsMapper.fromResponse(contributors.data))); + } + + addContributor(draftId: string, data: ContributorAddModel): Observable { + const baseUrl = `${this.apiUrl}/draft_registrations/${draftId}/contributors/`; + const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; + + const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; + + return this.jsonApiService + .post(baseUrl, contributorData) + .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); + } + + updateContributor(draftId: string, data: ContributorModel): Observable { + const baseUrl = `${environment.apiUrl}/draft_registrations/${draftId}/contributors/${data.userId}`; + + const contributorData = { data: ContributorsMapper.toContributorAddRequest(data) }; + + return this.jsonApiService + .patch(baseUrl, contributorData) + .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); + } + + deleteContributor(draftId: string, contributorId: string): Observable { + return this.jsonApiService.delete(`${this.apiUrl}/draft_registrations/${draftId}/contributors/${contributorId}`); + } } diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index 6afb4f009..be20dd67a 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -1,3 +1,5 @@ +import { ContributorAddModel, ContributorModel } from '@osf/shared/components/contributors/models'; + export class GetProviders { static readonly type = '[Registries] Get Providers'; } @@ -10,3 +12,41 @@ export class CreateDraft { static readonly type = '[Registries] Create Draft'; constructor(public payload: { registrationSchemaId: string; projectId?: string }) {} } + +export class DeleteDraft { + static readonly type = '[Registries] Delete Draft'; + constructor(public draftId: string) {} +} + +export class FetchContributors { + static readonly type = '[Registries] Fetch Contributors'; + + constructor(public draftId: string) {} +} + +export class AddContributor { + static readonly type = '[Registries] Add Contributor'; + + constructor( + public draftId: string, + public contributor: ContributorAddModel + ) {} +} + +export class UpdateContributor { + static readonly type = '[Registries] Update Contributor'; + + constructor( + public draftId: string, + public contributor: ContributorModel + ) {} +} + +export class DeleteContributor { + static readonly type = '[Registries] Delete Contributor'; + + constructor( + public draftId: string, + public contributorId: string + ) {} +} diff --git a/src/app/features/registries/store/registries.model.ts b/src/app/features/registries/store/registries.model.ts index cf8d51dfe..d2e42cf47 100644 --- a/src/app/features/registries/store/registries.model.ts +++ b/src/app/features/registries/store/registries.model.ts @@ -1,3 +1,4 @@ +import { ContributorModel } from '@osf/shared/components/contributors/models'; import { AsyncStateModel } from '@osf/shared/models'; import { Project, Provider } from '../models'; @@ -6,5 +7,6 @@ import { Registration } from '../models/registries.model'; export interface RegistriesStateModel { providers: AsyncStateModel; projects: AsyncStateModel; - registrations: AsyncStateModel; + draftRegistration: AsyncStateModel; + contributorsList: AsyncStateModel; } diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts index f63694d63..1349e30af 100644 --- a/src/app/features/registries/store/registries.selectors.ts +++ b/src/app/features/registries/store/registries.selectors.ts @@ -18,6 +18,11 @@ export class RegistriesSelectors { @Selector([RegistriesState]) static isDraftSubmitting(state: RegistriesStateModel): boolean { - return state.registrations.isSubmitting ?? false; + return state.draftRegistration.isSubmitting ?? false; + } + + @Selector([RegistriesState]) + static getContributors(state: RegistriesStateModel) { + return state.contributorsList.data; } } diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index 08f9a352f..55cdd339c 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -8,7 +8,16 @@ import { Injectable } from '@angular/core'; import { Project } from '../models'; import { ProjectsService, ProvidersService, RegistriesService } from '../services'; -import { CreateDraft, GetProjects, GetProviders } from './registries.actions'; +import { + AddContributor, + CreateDraft, + DeleteContributor, + DeleteDraft, + FetchContributors, + GetProjects, + GetProviders, + UpdateContributor, +} from './registries.actions'; import { RegistriesStateModel } from './registries.model'; const DefaultState: RegistriesStateModel = { @@ -22,12 +31,17 @@ const DefaultState: RegistriesStateModel = { isLoading: false, error: null, }, - registrations: { + draftRegistration: { isLoading: false, data: null, isSubmitting: false, error: null, }, + contributorsList: { + data: [], + isLoading: false, + error: null, + }, }; @State({ @@ -102,8 +116,8 @@ export class RegistriesState { @Action(CreateDraft) createDraft(ctx: StateContext, { payload }: CreateDraft) { ctx.patchState({ - registrations: { - ...ctx.getState().registrations, + draftRegistration: { + ...ctx.getState().draftRegistration, isSubmitting: true, }, }); @@ -111,8 +125,8 @@ export class RegistriesState { return this.registriesService.createDraft(payload.registrationSchemaId, payload.projectId).pipe( tap(() => { ctx.patchState({ - registrations: { - ...ctx.getState().registrations, + draftRegistration: { + ...ctx.getState().draftRegistration, isSubmitting: false, error: null, }, @@ -120,17 +134,135 @@ export class RegistriesState { }), catchError((error) => { ctx.patchState({ - registrations: { - ...ctx.getState().registrations, + draftRegistration: { + ...ctx.getState().draftRegistration, isSubmitting: false, error: error.message, }, }); - return this.handleError(ctx, 'registrations', error); + return this.handleError(ctx, 'draftRegistration', error); }) ); } + @Action(DeleteDraft) + deleteDraft(ctx: StateContext, { draftId }: DeleteDraft) { + ctx.patchState({ + draftRegistration: { + ...ctx.getState().draftRegistration, + isSubmitting: true, + }, + }); + + return this.registriesService.deleteDraft(draftId).pipe( + tap(() => { + ctx.patchState({ + draftRegistration: { + ...ctx.getState().draftRegistration, + isSubmitting: false, + data: null, + error: null, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'draftRegistration', error)) + ); + } + + @Action(FetchContributors) + fetchContributors(ctx: StateContext, action: FetchContributors) { + const state = ctx.getState(); + + ctx.patchState({ + contributorsList: { ...state.contributorsList, isLoading: true, error: null }, + }); + + return this.registriesService.getContributors(action.draftId).pipe( + tap((contributors) => { + ctx.patchState({ + contributorsList: { + ...state.contributorsList, + data: contributors, + isLoading: false, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'contributorsList', error)) + ); + } + + @Action(AddContributor) + addContributor(ctx: StateContext, action: AddContributor) { + const state = ctx.getState(); + + ctx.patchState({ + contributorsList: { ...state.contributorsList, isLoading: true, error: null }, + }); + + return this.registriesService.addContributor(action.draftId, action.contributor).pipe( + tap((contributor) => { + const currentState = ctx.getState(); + + ctx.patchState({ + contributorsList: { + ...currentState.contributorsList, + data: [...currentState.contributorsList.data, contributor], + isLoading: false, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'contributorsList', error)) + ); + } + + @Action(UpdateContributor) + updateContributor(ctx: StateContext, action: UpdateContributor) { + const state = ctx.getState(); + + ctx.patchState({ + contributorsList: { ...state.contributorsList, isLoading: true, error: null }, + }); + + return this.registriesService.updateContributor(action.draftId, action.contributor).pipe( + tap((updatedContributor) => { + const currentState = ctx.getState(); + + ctx.patchState({ + contributorsList: { + ...currentState.contributorsList, + data: currentState.contributorsList.data.map((contributor) => + contributor.id === updatedContributor.id ? updatedContributor : contributor + ), + isLoading: false, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'contributorsList', error)) + ); + } + + @Action(DeleteContributor) + deleteContributor(ctx: StateContext, action: DeleteContributor) { + const state = ctx.getState(); + + ctx.patchState({ + contributorsList: { ...state.contributorsList, isLoading: true, error: null }, + }); + + return this.registriesService.deleteContributor(action.draftId, action.contributorId).pipe( + tap(() => { + ctx.patchState({ + contributorsList: { + ...state.contributorsList, + data: state.contributorsList.data.filter((contributor) => contributor.userId !== action.contributorId), + isLoading: false, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'contributorsList', error)) + ); + } + private handleError(ctx: StateContext, section: keyof RegistriesStateModel, error: Error) { ctx.patchState({ [section]: { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 0e4646f54..344a1c565 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -355,11 +355,17 @@ "step3": "Which type of registration would you like to create?", "step2InfoText": "If your project includes components, you can select which components to include or exclude at the end of the registration." }, - "metadata": { - "title": "Registration Metadata", - "description": "This metadata applies only to the registration you are creating, and will not be applied to your project." - }, + "createDraft": "Create draft" + }, + "deleteDraft": "Delete Draft", + "metadata": { + "title": "Registration Metadata", + "description": "This metadata applies only to the registration you are creating, and will not be applied to your project.", + "addContributors": "Add Contributor By Search" + }, + "review": { + "title": "Review Registration" } }, "myProfile": { From aa43a92ea410b4109b042d8e9e9dd6e829210934 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Wed, 25 Jun 2025 14:06:57 +0300 Subject: [PATCH 08/15] feat(registries): add licenses section --- .../metadata/license/license.component.html | 48 ++++++++++++++- .../metadata/license/license.component.scss | 4 ++ .../metadata/license/license.component.ts | 59 ++++++++++++++++++- .../metadata/metadata.component.html | 1 + .../components/metadata/metadata.component.ts | 2 + src/app/features/registries/mappers/index.ts | 1 + .../registries/mappers/licenses.mapper.ts | 14 +++++ src/app/features/registries/models/index.ts | 2 + .../registries/models/license.model.ts | 7 +++ .../models/licenses-json-api.model.ts | 16 +++++ src/app/features/registries/services/index.ts | 1 + .../registries/services/licenses.service.ts | 52 ++++++++++++++++ .../registries/store/registries.actions.ts | 4 ++ .../registries/store/registries.model.ts | 3 +- .../registries/store/registries.selectors.ts | 7 ++- .../registries/store/registries.state.ts | 33 ++++++++++- src/app/shared/pipes/index.ts | 1 + src/app/shared/pipes/interpolate.pipe.ts | 10 ++++ src/assets/i18n/en.json | 13 +++- 19 files changed, 269 insertions(+), 9 deletions(-) create mode 100644 src/app/features/registries/mappers/licenses.mapper.ts create mode 100644 src/app/features/registries/models/license.model.ts create mode 100644 src/app/features/registries/models/licenses-json-api.model.ts create mode 100644 src/app/features/registries/services/licenses.service.ts create mode 100644 src/app/shared/pipes/interpolate.pipe.ts diff --git a/src/app/features/registries/components/metadata/license/license.component.html b/src/app/features/registries/components/metadata/license/license.component.html index c2800e0d3..cc5f9c25a 100644 --- a/src/app/features/registries/components/metadata/license/license.component.html +++ b/src/app/features/registries/components/metadata/license/license.component.html @@ -1 +1,47 @@ -

license works!

+ +

{{ 'shared.license.title' | translate }}

+

+ {{ 'shared.license.description' | translate }} +

+

+ {{ 'shared.license.helpText' | translate }} + {{ 'common.links.helpGuide' | translate }}. +

+ + @if (selectedLicense) { + + @if (selectedLicense.requiredFields.length) { +
+
+ + +
+ + +
+ } + +

+ +

+ } +
diff --git a/src/app/features/registries/components/metadata/license/license.component.scss b/src/app/features/registries/components/metadata/license/license.component.scss index e69de29bb..7f863186d 100644 --- a/src/app/features/registries/components/metadata/license/license.component.scss +++ b/src/app/features/registries/components/metadata/license/license.component.scss @@ -0,0 +1,4 @@ +.highlight-block { + padding: 0.5rem; + background-color: var(--bg-blue-2); +} diff --git a/src/app/features/registries/components/metadata/license/license.component.ts b/src/app/features/registries/components/metadata/license/license.component.ts index 19d291552..ab302faf4 100644 --- a/src/app/features/registries/components/metadata/license/license.component.ts +++ b/src/app/features/registries/components/metadata/license/license.component.ts @@ -1,10 +1,63 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Card } from 'primeng/card'; +import { DatePicker } from 'primeng/datepicker'; +import { Divider } from 'primeng/divider'; +import { Select } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; + +import { License } from '@osf/features/registries/models'; +import { FetchLicenses, RegistriesSelectors } from '@osf/features/registries/store'; +import { TextInputComponent, TruncatedTextComponent } from '@osf/shared/components'; +import { InputLimits } from '@osf/shared/constants'; +import { InterpolatePipe } from '@osf/shared/pipes'; +import { CustomValidators } from '@osf/shared/utils'; @Component({ selector: 'osf-license', - imports: [], + imports: [ + Card, + TranslatePipe, + Select, + FormsModule, + Divider, + TruncatedTextComponent, + DatePicker, + TextInputComponent, + InterpolatePipe, + ReactiveFormsModule, + ], templateUrl: './license.component.html', styleUrl: './license.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class LicenseComponent {} +export class LicenseComponent { + private readonly route = inject(ActivatedRoute); + private readonly draftId = this.route.snapshot.params['id']; + private readonly fb = inject(FormBuilder); + + protected actions = createDispatchMap({ fetchLicenses: FetchLicenses }); + protected licenses = select(RegistriesSelectors.getLicenses); + protected inputLimits = InputLimits; + + selectedLicense: License | null = null; + currentYear = new Date(); + licenseYear = this.currentYear; + licenseForm = this.fb.group({ + year: [this.currentYear.getFullYear().toString(), CustomValidators.requiredTrimmed()], + copyrightHolders: ['', CustomValidators.requiredTrimmed()], + }); + + constructor() { + this.actions.fetchLicenses(); + } + + onSelectLicense(license: License): void { + console.log('Selected License:', license); + } +} diff --git a/src/app/features/registries/components/metadata/metadata.component.html b/src/app/features/registries/components/metadata/metadata.component.html index de1b7dec8..574934bb7 100644 --- a/src/app/features/registries/components/metadata/metadata.component.html +++ b/src/app/features/registries/components/metadata/metadata.component.html @@ -26,6 +26,7 @@

{{ 'registries.metadata.title' | translate }}

+
diff --git a/src/app/features/registries/components/metadata/metadata.component.ts b/src/app/features/registries/components/metadata/metadata.component.ts index a2e083046..5d7d036eb 100644 --- a/src/app/features/registries/components/metadata/metadata.component.ts +++ b/src/app/features/registries/components/metadata/metadata.component.ts @@ -17,6 +17,7 @@ import { CustomValidators } from '@osf/shared/utils'; import { DeleteDraft } from '../../store'; import { ContributorsComponent } from './contributors/contributors.component'; +import { LicenseComponent } from './license/license.component'; @Component({ selector: 'osf-metadata', @@ -28,6 +29,7 @@ import { ContributorsComponent } from './contributors/contributors.component'; TranslatePipe, TextareaModule, ContributorsComponent, + LicenseComponent, ], templateUrl: './metadata.component.html', styleUrl: './metadata.component.scss', diff --git a/src/app/features/registries/mappers/index.ts b/src/app/features/registries/mappers/index.ts index 43fcc678e..91c916d86 100644 --- a/src/app/features/registries/mappers/index.ts +++ b/src/app/features/registries/mappers/index.ts @@ -1,2 +1,3 @@ +export * from './licenses.mapper'; export * from './projects.mapper'; export * from './providers.mapper'; diff --git a/src/app/features/registries/mappers/licenses.mapper.ts b/src/app/features/registries/mappers/licenses.mapper.ts new file mode 100644 index 000000000..47cc364ad --- /dev/null +++ b/src/app/features/registries/mappers/licenses.mapper.ts @@ -0,0 +1,14 @@ +import { License, LicensesResponseJsonApi } from '../models'; + +export class LicensesMapper { + static fromLicensesResponse(response: LicensesResponseJsonApi): License[] { + console.log('LicensesMapper.fromLicensesResponse', response); + return response.data.map((item) => ({ + id: item.id, + name: item.attributes.name, + requiredFields: item.attributes.required_fields, + url: item.attributes.url, + text: item.attributes.text, + })); + } +} diff --git a/src/app/features/registries/models/index.ts b/src/app/features/registries/models/index.ts index f18645392..fb6440554 100644 --- a/src/app/features/registries/models/index.ts +++ b/src/app/features/registries/models/index.ts @@ -1,3 +1,5 @@ +export * from './license.model'; +export * from './licenses-json-api.model'; export * from './project'; export * from './projects-json-api.model'; export * from './provider.model'; diff --git a/src/app/features/registries/models/license.model.ts b/src/app/features/registries/models/license.model.ts new file mode 100644 index 000000000..c4a243a7f --- /dev/null +++ b/src/app/features/registries/models/license.model.ts @@ -0,0 +1,7 @@ +export interface License { + id: string; + name: string; + requiredFields: string[]; + url: string; + text: string; +} diff --git a/src/app/features/registries/models/licenses-json-api.model.ts b/src/app/features/registries/models/licenses-json-api.model.ts new file mode 100644 index 000000000..32e8fc049 --- /dev/null +++ b/src/app/features/registries/models/licenses-json-api.model.ts @@ -0,0 +1,16 @@ +import { ApiData, MetaJsonApi, PaginationLinksJsonApi } from '@osf/core/models'; + +export interface LicensesResponseJsonApi { + data: LicenseDataJsonApi[]; + meta: MetaJsonApi; + links: PaginationLinksJsonApi; +} + +export type LicenseDataJsonApi = ApiData; + +interface LicenseAttributesJsonApi { + name: string; + required_fields: string[]; + url: string; + text: string; +} diff --git a/src/app/features/registries/services/index.ts b/src/app/features/registries/services/index.ts index 04f640163..b6471bbb9 100644 --- a/src/app/features/registries/services/index.ts +++ b/src/app/features/registries/services/index.ts @@ -1,3 +1,4 @@ +export * from './licenses.service'; export * from './projects.service'; export * from './providers.service'; export * from './registries.service'; diff --git a/src/app/features/registries/services/licenses.service.ts b/src/app/features/registries/services/licenses.service.ts new file mode 100644 index 000000000..8879ed1c7 --- /dev/null +++ b/src/app/features/registries/services/licenses.service.ts @@ -0,0 +1,52 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@osf/core/services'; + +import { LicensesMapper } from '../mappers'; +import { License, LicensesResponseJsonApi } from '../models'; + +import { environment } from 'src/environments/environment'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const data: any = { + data: [ + { + id: '58fd62fdda3e2400012ca5d9', + type: 'licenses', + attributes: { + name: 'MIT License', + text: 'The MIT License (MIT)\n\nCopyright (c) {{year}} {{copyrightHolders}}\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n', + url: 'http://opensource.org/licenses/MIT', + required_fields: ['year', 'copyrightHolders'], + }, + links: { + self: 'https://api.test.osf.io/v2/licenses/58fd62fdda3e2400012ca5d9/', + }, + }, + ], +}; + +@Injectable({ + providedIn: 'root', +}) +export class LicensesService { + private apiUrl = environment.apiUrl; + private readonly jsonApiService = inject(JsonApiService); + + getLicenses(): Observable { + return this.jsonApiService + .get(`${this.apiUrl}/providers/registrations/osf/licenses/`, { + params: { + 'page[size]': 100, + }, + }) + .pipe( + map((licenses) => { + licenses.data.unshift(data.data[0]); // For testing purposes, remove in production + return LicensesMapper.fromLicensesResponse(licenses); + }) + ); + } +} diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index 67f29a371..364ee2b4e 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -54,3 +54,7 @@ export class DeleteContributor { public contributorId: string ) {} } + +export class FetchLicenses { + static readonly type = '[Registries] Fetch Licenses'; +} diff --git a/src/app/features/registries/store/registries.model.ts b/src/app/features/registries/store/registries.model.ts index 00653e404..b518e664b 100644 --- a/src/app/features/registries/store/registries.model.ts +++ b/src/app/features/registries/store/registries.model.ts @@ -1,7 +1,7 @@ import { ContributorModel } from '@osf/shared/components/contributors/models'; import { AsyncStateModel, Resource } from '@shared/models'; -import { Project, Provider } from '../models'; +import { License, Project, Provider } from '../models'; import { Registration } from '../models/registries.model'; export interface RegistriesStateModel { @@ -10,4 +10,5 @@ export interface RegistriesStateModel { draftRegistration: AsyncStateModel; contributorsList: AsyncStateModel; registries: AsyncStateModel; + licenses: AsyncStateModel; } diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts index 2e23b6fa4..3e379e530 100644 --- a/src/app/features/registries/store/registries.selectors.ts +++ b/src/app/features/registries/store/registries.selectors.ts @@ -2,7 +2,7 @@ import { Selector } from '@ngxs/store'; import { Resource } from '@shared/models'; -import { Project, Provider } from '../models'; +import { License, Project, Provider } from '../models'; import { RegistriesStateModel } from './registries.model'; import { RegistriesState } from './registries.state'; @@ -37,4 +37,9 @@ export class RegistriesSelectors { static isRegistriesLoading(state: RegistriesStateModel): boolean { return state.registries.isLoading; } + + @Selector([RegistriesState]) + static getLicenses(state: RegistriesStateModel): License[] { + return state.licenses.data; + } } diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index 15f325edf..a47e2193a 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -10,7 +10,7 @@ import { SearchService } from '@osf/shared/services'; import { getResourceTypes } from '@osf/shared/utils'; import { Project } from '../models'; -import { ProjectsService, ProvidersService, RegistriesService } from '../services'; +import { LicensesService, ProjectsService, ProvidersService, RegistriesService } from '../services'; import { AddContributor, @@ -18,6 +18,7 @@ import { DeleteContributor, DeleteDraft, FetchContributors, + FetchLicenses, GetProjects, GetProviders, GetRegistries, @@ -52,6 +53,11 @@ const DefaultState: RegistriesStateModel = { isLoading: false, error: null, }, + licenses: { + data: [], + isLoading: false, + error: null, + }, }; @State({ @@ -64,6 +70,8 @@ export class RegistriesState { providersService = inject(ProvidersService); projectsService = inject(ProjectsService); registriesService = inject(RegistriesService); + licensesService = inject(LicensesService); + @Action(GetRegistries) getRegistries(ctx: StateContext) { const state = ctx.getState(); @@ -297,6 +305,29 @@ export class RegistriesState { ); } + @Action(FetchLicenses) + fetchLicenses(ctx: StateContext) { + ctx.patchState({ + licenses: { + ...ctx.getState().licenses, + isLoading: true, + }, + }); + + return this.licensesService.getLicenses().pipe( + tap((licenses) => { + ctx.patchState({ + licenses: { + data: licenses, + isLoading: false, + error: null, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'licenses', error)) + ); + } + private handleError(ctx: StateContext, section: keyof RegistriesStateModel, error: Error) { ctx.patchState({ [section]: { diff --git a/src/app/shared/pipes/index.ts b/src/app/shared/pipes/index.ts index 05dc24e77..6e3ada82f 100644 --- a/src/app/shared/pipes/index.ts +++ b/src/app/shared/pipes/index.ts @@ -1,4 +1,5 @@ export { DecodeHtmlPipe } from './decode-html.pipe'; export { FileSizePipe } from './file-size.pipe'; +export { InterpolatePipe } from './interpolate.pipe'; export { MonthYearPipe } from './month-year.pipe'; export { WrapFnPipe } from './wrap-fn.pipe'; diff --git a/src/app/shared/pipes/interpolate.pipe.ts b/src/app/shared/pipes/interpolate.pipe.ts new file mode 100644 index 000000000..9a42f81ec --- /dev/null +++ b/src/app/shared/pipes/interpolate.pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'interpolate', +}) +export class InterpolatePipe implements PipeTransform { + transform(template: string, variables: Record): string { + return template.replace(/{{\s*(\w+)\s*}}/g, (_, key) => (variables[key] != null ? variables[key] : '')); + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 4561aef31..e488feb43 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -41,14 +41,16 @@ "labels": { "downloads": "Downloads", "title": "Title", - "description": "Description" + "description": "Description", + "year": "Year" }, "deleteConfirmation": { "header": "Delete", "message": "Are you sure you want to proceed?" }, "links": { - "clickHere": "Click here" + "clickHere": "Click here", + "helpGuide": "Help Guide" } }, "navigation": { @@ -1463,6 +1465,13 @@ "materials": "Materials", "papers": "Papers", "supplements": "Supplements" + }, + "license": { + "title": "License", + "selectLicense": "Select license", + "description": "A license tells others how they can use your work in the future and only applies to the information and files submitted with the registration.", + "helpText": "For more information, see this ", + "copyrightHolders": "Copyright Holders" } }, "pageNotFound": { From 874526318dc0093958a3ba6ca19af77513cc036e Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Wed, 25 Jun 2025 21:18:24 +0300 Subject: [PATCH 09/15] feat(registries): get schema-blocks for custom pages --- .../components/drafts/drafts.component.html | 1 + .../components/drafts/drafts.component.ts | 44 ++++++++++++++-- .../registries/constants/defaultSteps.ts | 10 ++++ .../features/registries/constants/index.ts | 1 + .../registries/enums/block-type.enum.ts | 8 +++ src/app/features/registries/enums/index.ts | 1 + src/app/features/registries/mappers/index.ts | 1 + .../registries/mappers/page-schema.mapper.ts | 50 +++++++++++++++++++ src/app/features/registries/models/index.ts | 1 + .../registries/models/page-schema.model.ts | 29 +++++++++++ .../models/schema-blocks-json-api.model.ts | 22 ++++++++ .../registries/services/registries.service.ts | 32 ++++-------- .../registries/store/registries.actions.ts | 5 ++ .../registries/store/registries.model.ts | 3 +- .../registries/store/registries.selectors.ts | 7 ++- .../registries/store/registries.state.ts | 28 ++++++++++- 16 files changed, 214 insertions(+), 29 deletions(-) create mode 100644 src/app/features/registries/constants/defaultSteps.ts create mode 100644 src/app/features/registries/constants/index.ts create mode 100644 src/app/features/registries/enums/block-type.enum.ts create mode 100644 src/app/features/registries/enums/index.ts create mode 100644 src/app/features/registries/mappers/page-schema.mapper.ts create mode 100644 src/app/features/registries/models/page-schema.model.ts create mode 100644 src/app/features/registries/models/schema-blocks-json-api.model.ts diff --git a/src/app/features/registries/components/drafts/drafts.component.html b/src/app/features/registries/components/drafts/drafts.component.html index 0680b43f9..2f28bf7b3 100644 --- a/src/app/features/registries/components/drafts/drafts.component.html +++ b/src/app/features/registries/components/drafts/drafts.component.html @@ -1 +1,2 @@ + diff --git a/src/app/features/registries/components/drafts/drafts.component.ts b/src/app/features/registries/components/drafts/drafts.component.ts index d563a2aac..5a4f11413 100644 --- a/src/app/features/registries/components/drafts/drafts.component.ts +++ b/src/app/features/registries/components/drafts/drafts.component.ts @@ -1,11 +1,49 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { createDispatchMap, select } from '@ngxs/store'; + +import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { StepperComponent } from '@osf/shared/components'; + +import { defaultSteps } from '../../constants'; +import { FetchSchemaBlocks, RegistriesSelectors } from '../../store'; + @Component({ selector: 'osf-drafts', - imports: [RouterOutlet], + imports: [RouterOutlet, StepperComponent], templateUrl: './drafts.component.html', styleUrl: './drafts.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DraftsComponent {} +export class DraftsComponent { + protected readonly pages = select(RegistriesSelectors.getPagesSchema); + + private readonly actions = createDispatchMap({ + getSchemaBlocks: FetchSchemaBlocks, + }); + + // TODO: get current registrationSchemaId from store + registrationSchemaId = '6797c0dedee44d144a2943fd'; + + currentStep = signal(0); + + steps = computed(() => { + const customSteps = this.pages().map((page) => ({ + label: page.title, + value: page.id, + })); + return [defaultSteps[0], ...customSteps, defaultSteps[1]]; + }); + + constructor() { + this.actions.getSchemaBlocks(this.registrationSchemaId); + setTimeout(() => { + console.log('DraftsComponent initialized with registrationSchemaId:', this.pages()); + }, 2000); + } + + stepChange(step: number): void { + this.currentStep.set(step); + console.log('Current step changed to:', step); + } +} diff --git a/src/app/features/registries/constants/defaultSteps.ts b/src/app/features/registries/constants/defaultSteps.ts new file mode 100644 index 000000000..fa338f1f6 --- /dev/null +++ b/src/app/features/registries/constants/defaultSteps.ts @@ -0,0 +1,10 @@ +export const defaultSteps = [ + { + label: 'Metadata', + value: 3, + }, + { + label: 'Review', + value: 6, + }, +]; diff --git a/src/app/features/registries/constants/index.ts b/src/app/features/registries/constants/index.ts new file mode 100644 index 000000000..92e572c61 --- /dev/null +++ b/src/app/features/registries/constants/index.ts @@ -0,0 +1 @@ +export * from './defaultSteps'; diff --git a/src/app/features/registries/enums/block-type.enum.ts b/src/app/features/registries/enums/block-type.enum.ts new file mode 100644 index 000000000..d97c2b9be --- /dev/null +++ b/src/app/features/registries/enums/block-type.enum.ts @@ -0,0 +1,8 @@ +export enum BlockType { + PageHeading = 'page-heading', + QuestionLabel = 'question-label', + LongTextInput = 'long-text-input', + Paragraph = 'paragraph', + SingleSelectInput = 'single-select-input', + SelectInputOption = 'select-input-option', +} diff --git a/src/app/features/registries/enums/index.ts b/src/app/features/registries/enums/index.ts new file mode 100644 index 000000000..83bdad606 --- /dev/null +++ b/src/app/features/registries/enums/index.ts @@ -0,0 +1 @@ +export * from './block-type.enum'; diff --git a/src/app/features/registries/mappers/index.ts b/src/app/features/registries/mappers/index.ts index 91c916d86..013703b21 100644 --- a/src/app/features/registries/mappers/index.ts +++ b/src/app/features/registries/mappers/index.ts @@ -1,3 +1,4 @@ export * from './licenses.mapper'; +export * from './page-schema.mapper'; export * from './projects.mapper'; export * from './providers.mapper'; diff --git a/src/app/features/registries/mappers/page-schema.mapper.ts b/src/app/features/registries/mappers/page-schema.mapper.ts new file mode 100644 index 000000000..79980d15c --- /dev/null +++ b/src/app/features/registries/mappers/page-schema.mapper.ts @@ -0,0 +1,50 @@ +import { BlockType } from '../enums'; +import { PageSchema, Question } from '../models'; +import { SchemaBlocksResponseJsonApi } from '../models/schema-blocks-json-api.model'; + +export class PageSchemaMapper { + static fromSchemaBlocksResponse(response: SchemaBlocksResponseJsonApi): PageSchema[] { + console.log('PageSchemaMapper.fromSchemaBlocksResponse', response); + const pages: PageSchema[] = []; + let currentPage!: PageSchema; + let currentQuestion!: Question; + response.data.map((item) => { + console.log('Processing item:', item); + switch (item.attributes.block_type) { + case BlockType.PageHeading: + currentPage = { + id: item.id, + title: item.attributes.display_text, + questions: [], + }; + pages.push(currentPage); + break; + + case BlockType.QuestionLabel: + console.log('QuestionLabel:'); + currentQuestion = { + id: item.id, + title: item.attributes.display_text, + description: item.attributes.help_text, + type: item.attributes.registration_response_key as Question['type'], + required: item.attributes.required, + groupKey: item.attributes.schema_block_group_key, + responseKey: item.attributes.registration_response_key || undefined, + }; + currentPage.questions?.push(currentQuestion); + break; + case BlockType.SelectInputOption: + console.log('SelectInputOption:', item); + currentQuestion.options = currentQuestion.options || []; + currentQuestion.options.push(item.attributes.display_text); + + break; + default: + console.warn(`Unexpected block type: ${item.attributes.block_type}`); + return; + } + }); + + return pages; + } +} diff --git a/src/app/features/registries/models/index.ts b/src/app/features/registries/models/index.ts index fb6440554..1aed02f16 100644 --- a/src/app/features/registries/models/index.ts +++ b/src/app/features/registries/models/index.ts @@ -1,5 +1,6 @@ export * from './license.model'; export * from './licenses-json-api.model'; +export * from './page-schema.model'; export * from './project'; export * from './projects-json-api.model'; export * from './provider.model'; diff --git a/src/app/features/registries/models/page-schema.model.ts b/src/app/features/registries/models/page-schema.model.ts new file mode 100644 index 000000000..2d9bb31e8 --- /dev/null +++ b/src/app/features/registries/models/page-schema.model.ts @@ -0,0 +1,29 @@ +export interface PageSchema { + id: string; + title: string; + questions?: Question[]; +} + +export interface Question { + id: string; + title: string; + description?: string; + type: QuestionType; + options?: string[]; + required: boolean; + groupKey?: string; + responseKey?: string; +} + +export enum QuestionType { + Text = 'text', + TextArea = 'textarea', + Select = 'select', + MultiSelect = 'multi-select', + Checkbox = 'checkbox', + Radio = 'radio', + Date = 'date', + Number = 'number', + Email = 'email', + Url = 'url', +} diff --git a/src/app/features/registries/models/schema-blocks-json-api.model.ts b/src/app/features/registries/models/schema-blocks-json-api.model.ts new file mode 100644 index 000000000..796eab5d1 --- /dev/null +++ b/src/app/features/registries/models/schema-blocks-json-api.model.ts @@ -0,0 +1,22 @@ +import { ApiData, MetaJsonApi, PaginationLinksJsonApi } from '@osf/core/models'; + +import { BlockType } from '../enums'; + +export interface SchemaBlocksResponseJsonApi { + data: SchemaBlockJsonApi[]; + meta: MetaJsonApi; + links: PaginationLinksJsonApi; +} + +export type SchemaBlockJsonApi = ApiData; + +interface SchemaBlockAttributesJsonApi { + block_type: BlockType; + display_text: string; + example_text: string; + help_text: string; + index: number; + registration_response_key: string | null; + required: boolean; + schema_block_group_key: string; +} diff --git a/src/app/features/registries/services/registries.service.ts b/src/app/features/registries/services/registries.service.ts index 9d5dc6f72..aa11c9ad6 100644 --- a/src/app/features/registries/services/registries.service.ts +++ b/src/app/features/registries/services/registries.service.ts @@ -8,8 +8,10 @@ import { AddContributorType } from '@osf/shared/components/contributors/enums'; import { ContributorsMapper } from '@osf/shared/components/contributors/mappers'; import { ContributorAddModel, ContributorModel, ContributorResponse } from '@osf/shared/components/contributors/models'; -import { Registration } from '../models'; +import { PageSchemaMapper } from '../mappers'; +import { PageSchema, Registration } from '../models'; import { RegistrationDataJsonApi } from '../models/registries-json-api.model'; +import { SchemaBlocksResponseJsonApi } from '../models/schema-blocks-json-api.model'; import { environment } from 'src/environments/environment'; @@ -57,32 +59,16 @@ export class RegistriesService { return this.jsonApiService.patch(`${this.apiUrl}/draft_registrations/${draftId}/`, payload); } - // addContributor(draftId: string, userId: string, permission: string): Observable { - // const payload = { - // data: { - // type: 'contributors', - // attributes: { - // permission, - // }, - // relationships: { - // users: { - // data: [ - // { - // type: 'users', - // id: userId, - // }, - // ], - // }, - // }, - // }, - // }; - // return this.jsonApiService.post(`${this.apiUrl}/draft_registrations/${draftId}/contributors/`, payload); - // } - deleteDraft(draftId: string): Observable { return this.jsonApiService.delete(`${this.apiUrl}/draft_registrations/${draftId}/`); } + getSchemaBlocks(registrationSchemaId: string): Observable { + return this.jsonApiService + .get(`${this.apiUrl}/schemas/registrations/${registrationSchemaId}/schema_blocks/`) + .pipe(map((response) => PageSchemaMapper.fromSchemaBlocksResponse(response))); + } + getContributors(draftId: string): Observable { return this.jsonApiService .get>(`${this.apiUrl}/draft_registrations/${draftId}/contributors/`) diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index 364ee2b4e..f409ad91b 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -22,6 +22,11 @@ export class DeleteDraft { constructor(public draftId: string) {} } +export class FetchSchemaBlocks { + static readonly type = '[Registries] Fetch Schema Blocks'; + constructor(public registrationSchemaId: string) {} +} + export class FetchContributors { static readonly type = '[Registries] Fetch Contributors'; diff --git a/src/app/features/registries/store/registries.model.ts b/src/app/features/registries/store/registries.model.ts index b518e664b..7700a4945 100644 --- a/src/app/features/registries/store/registries.model.ts +++ b/src/app/features/registries/store/registries.model.ts @@ -1,7 +1,7 @@ import { ContributorModel } from '@osf/shared/components/contributors/models'; import { AsyncStateModel, Resource } from '@shared/models'; -import { License, Project, Provider } from '../models'; +import { License, PageSchema, Project, Provider } from '../models'; import { Registration } from '../models/registries.model'; export interface RegistriesStateModel { @@ -11,4 +11,5 @@ export interface RegistriesStateModel { contributorsList: AsyncStateModel; registries: AsyncStateModel; licenses: AsyncStateModel; + pagesSchema: AsyncStateModel; } diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts index 3e379e530..272527046 100644 --- a/src/app/features/registries/store/registries.selectors.ts +++ b/src/app/features/registries/store/registries.selectors.ts @@ -2,7 +2,7 @@ import { Selector } from '@ngxs/store'; import { Resource } from '@shared/models'; -import { License, Project, Provider } from '../models'; +import { License, PageSchema, Project, Provider } from '../models'; import { RegistriesStateModel } from './registries.model'; import { RegistriesState } from './registries.state'; @@ -42,4 +42,9 @@ export class RegistriesSelectors { static getLicenses(state: RegistriesStateModel): License[] { return state.licenses.data; } + + @Selector([RegistriesState]) + static getPagesSchema(state: RegistriesStateModel): PageSchema[] { + return state.pagesSchema.data; + } } diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index a47e2193a..b622b962d 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -19,6 +19,7 @@ import { DeleteDraft, FetchContributors, FetchLicenses, + FetchSchemaBlocks, GetProjects, GetProviders, GetRegistries, @@ -58,6 +59,11 @@ const DefaultState: RegistriesStateModel = { isLoading: false, error: null, }, + pagesSchema: { + data: [], + isLoading: false, + error: null, + }, }; @State({ @@ -165,7 +171,7 @@ export class RegistriesState { }); return this.registriesService.createDraft(payload.registrationSchemaId, payload.projectId).pipe( - tap(() => { + tap((registration) => { ctx.patchState({ draftRegistration: { ...ctx.getState().draftRegistration, @@ -211,6 +217,26 @@ export class RegistriesState { ); } + @Action(FetchSchemaBlocks) + fetchSchemaBlocks(ctx: StateContext, action: FetchSchemaBlocks) { + const state = ctx.getState(); + ctx.patchState({ + pagesSchema: { ...state.pagesSchema, isLoading: true, error: null }, + }); + return this.registriesService.getSchemaBlocks(action.registrationSchemaId).pipe( + tap((schemaBlocks) => { + ctx.patchState({ + pagesSchema: { + data: schemaBlocks, + isLoading: false, + error: null, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'pagesSchema', error)) + ); + } + @Action(FetchContributors) fetchContributors(ctx: StateContext, action: FetchContributors) { const state = ctx.getState(); From f1e9da04bddc28af748b3cbe053ae05d3802220a Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Thu, 26 Jun 2025 15:28:09 +0300 Subject: [PATCH 10/15] feat(registries): implement api for custom steps, refactor steper --- .../submit-preprint-stepper.component.ts | 1 + .../custom-step/custom-step.component.html | 45 ++++++++++++++++- .../custom-step/custom-step.component.ts | 34 +++++++++++-- .../components/drafts/drafts.component.html | 9 +++- .../components/drafts/drafts.component.ts | 30 +++++++++--- .../registries/constants/defaultSteps.ts | 11 +++-- .../registries/enums/block-type.enum.ts | 3 ++ .../registries/enums/field-type.enum.ts | 12 +++++ src/app/features/registries/enums/index.ts | 1 + .../registries/mappers/page-schema.mapper.ts | 49 +++++++++++++++---- .../registries/models/page-schema.model.ts | 24 +++------ .../components/stepper/stepper.component.html | 14 +++++- .../components/stepper/stepper.component.scss | 6 +++ .../components/stepper/stepper.component.ts | 5 +- src/app/shared/models/index.ts | 1 + src/app/shared/models/step-option.model.ts | 6 +++ src/assets/i18n/en.json | 2 +- src/assets/styles/_variables.scss | 3 ++ 18 files changed, 210 insertions(+), 46 deletions(-) create mode 100644 src/app/features/registries/enums/field-type.enum.ts create mode 100644 src/app/shared/models/step-option.model.ts diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts index 9ee1edfb4..57bb6089d 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts @@ -90,6 +90,7 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy { } stepChange(step: number) { + console.log('Current step changed to:', step); if (step >= this.currentStep()) { return; } diff --git a/src/app/features/registries/components/custom-step/custom-step.component.html b/src/app/features/registries/components/custom-step/custom-step.component.html index 68a42463d..c099ba14a 100644 --- a/src/app/features/registries/components/custom-step/custom-step.component.html +++ b/src/app/features/registries/components/custom-step/custom-step.component.html @@ -1 +1,44 @@ -

custom-step works!

+
+ @if (currentPage()) { +

{{ currentPage().title }}

+ } + + @for (question of currentPage().questions; track question.id) { + + + @switch (question.fieldType) { + @case (FieldType.TextArea) { + + } + @case (FieldType.Radio) { +
+ @for (option of question.options; track option) { +
+ + +
+ } +
+ } + } +
+ } +
diff --git a/src/app/features/registries/components/custom-step/custom-step.component.ts b/src/app/features/registries/components/custom-step/custom-step.component.ts index d6541af2c..56b328285 100644 --- a/src/app/features/registries/components/custom-step/custom-step.component.ts +++ b/src/app/features/registries/components/custom-step/custom-step.component.ts @@ -1,10 +1,38 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { select } from '@ngxs/store'; + +import { Card } from 'primeng/card'; +import { RadioButton } from 'primeng/radiobutton'; +import { Textarea } from 'primeng/textarea'; + +import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; + +import { RegistriesSelectors } from '../../store'; + +import { FieldType } from './../../enums/field-type.enum'; @Component({ selector: 'osf-custom-step', - imports: [], + imports: [Card, Textarea, RadioButton, FormsModule], templateUrl: './custom-step.component.html', styleUrl: './custom-step.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CustomStepComponent {} +export class CustomStepComponent { + private readonly route = inject(ActivatedRoute); + step = signal(this.route.snapshot.params['step'].split('-')[0]); + protected readonly pages = select(RegistriesSelectors.getPagesSchema); + currentPage = computed(() => this.pages()[this.step() - 1]); + protected readonly FieldType = FieldType; + + radio = null; + + constructor() { + console.log('CustomStepComponent initialized with step:', this.step); + console.log('Current page:', this.currentPage); + this.route.params.subscribe((params) => { + this.step.set(+params['step'].split('-')[0]); + }); + } +} diff --git a/src/app/features/registries/components/drafts/drafts.component.html b/src/app/features/registries/components/drafts/drafts.component.html index 2f28bf7b3..ca310305a 100644 --- a/src/app/features/registries/components/drafts/drafts.component.html +++ b/src/app/features/registries/components/drafts/drafts.component.html @@ -1,2 +1,9 @@ - + + diff --git a/src/app/features/registries/components/drafts/drafts.component.ts b/src/app/features/registries/components/drafts/drafts.component.ts index 5a4f11413..2ed757c65 100644 --- a/src/app/features/registries/components/drafts/drafts.component.ts +++ b/src/app/features/registries/components/drafts/drafts.component.ts @@ -1,16 +1,19 @@ import { createDispatchMap, select } from '@ngxs/store'; -import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import { TranslatePipe } from '@ngx-translate/core'; -import { StepperComponent } from '@osf/shared/components'; +import { ChangeDetectionStrategy, Component, computed, inject, Signal, signal } from '@angular/core'; +import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; + +import { StepperComponent, SubHeaderComponent } from '@osf/shared/components'; +import { StepOption } from '@osf/shared/models'; import { defaultSteps } from '../../constants'; import { FetchSchemaBlocks, RegistriesSelectors } from '../../store'; @Component({ selector: 'osf-drafts', - imports: [RouterOutlet, StepperComponent], + imports: [RouterOutlet, StepperComponent, SubHeaderComponent, TranslatePipe], templateUrl: './drafts.component.html', styleUrl: './drafts.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -18,6 +21,9 @@ import { FetchSchemaBlocks, RegistriesSelectors } from '../../store'; export class DraftsComponent { protected readonly pages = select(RegistriesSelectors.getPagesSchema); + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + private readonly actions = createDispatchMap({ getSchemaBlocks: FetchSchemaBlocks, }); @@ -25,9 +31,10 @@ export class DraftsComponent { // TODO: get current registrationSchemaId from store registrationSchemaId = '6797c0dedee44d144a2943fd'; - currentStep = signal(0); + // TODO: get current step from route + currentStep = signal(this.route.snapshot.params['step'] ? +this.route.snapshot.params['step'].split('-')[0] : 1); - steps = computed(() => { + steps: Signal = computed(() => { const customSteps = this.pages().map((page) => ({ label: page.title, value: page.id, @@ -44,6 +51,15 @@ export class DraftsComponent { stepChange(step: number): void { this.currentStep.set(step); - console.log('Current step changed to:', step); + const pageStep = this.steps()[step]; + + console.log('Navigating to step:', pageStep, 'with label:', pageStep.label); + let pageLink = ''; + if (!pageStep.value) { + pageLink = `${pageStep.routeLink}`; + } else { + pageLink = `${step}-${pageStep.value}`; + } + this.router.navigate([`/registries/drafts/${this.registrationSchemaId}/`, pageLink]); } } diff --git a/src/app/features/registries/constants/defaultSteps.ts b/src/app/features/registries/constants/defaultSteps.ts index fa338f1f6..65030b71b 100644 --- a/src/app/features/registries/constants/defaultSteps.ts +++ b/src/app/features/registries/constants/defaultSteps.ts @@ -1,10 +1,15 @@ -export const defaultSteps = [ +import { StepOption } from '@osf/shared/models'; + +export const defaultSteps: StepOption[] = [ { label: 'Metadata', - value: 3, + value: '', + routeLink: 'metadata', + invalid: true, // Initially set to true, will be updated based on validation }, { label: 'Review', - value: 6, + value: '', + routeLink: 'review', }, ]; diff --git a/src/app/features/registries/enums/block-type.enum.ts b/src/app/features/registries/enums/block-type.enum.ts index d97c2b9be..51d2e40a2 100644 --- a/src/app/features/registries/enums/block-type.enum.ts +++ b/src/app/features/registries/enums/block-type.enum.ts @@ -1,8 +1,11 @@ export enum BlockType { PageHeading = 'page-heading', + SubsectionHeading = 'subsection-heading', QuestionLabel = 'question-label', LongTextInput = 'long-text-input', Paragraph = 'paragraph', SingleSelectInput = 'single-select-input', SelectInputOption = 'select-input-option', + FileInput = 'file-input', + MultiSelectInput = 'multi-select-input', } diff --git a/src/app/features/registries/enums/field-type.enum.ts b/src/app/features/registries/enums/field-type.enum.ts new file mode 100644 index 000000000..125656579 --- /dev/null +++ b/src/app/features/registries/enums/field-type.enum.ts @@ -0,0 +1,12 @@ +export enum FieldType { + Text = 'text', + TextArea = 'textarea', + Select = 'select', + MultiSelect = 'multi-select', + Checkbox = 'checkbox', + Radio = 'radio', + Date = 'date', + Number = 'number', + Email = 'email', + Url = 'url', +} diff --git a/src/app/features/registries/enums/index.ts b/src/app/features/registries/enums/index.ts index 83bdad606..609b32864 100644 --- a/src/app/features/registries/enums/index.ts +++ b/src/app/features/registries/enums/index.ts @@ -1 +1,2 @@ export * from './block-type.enum'; +export * from './field-type.enum'; diff --git a/src/app/features/registries/mappers/page-schema.mapper.ts b/src/app/features/registries/mappers/page-schema.mapper.ts index 79980d15c..b4ae8dbba 100644 --- a/src/app/features/registries/mappers/page-schema.mapper.ts +++ b/src/app/features/registries/mappers/page-schema.mapper.ts @@ -1,4 +1,4 @@ -import { BlockType } from '../enums'; +import { BlockType, FieldType } from '../enums'; import { PageSchema, Question } from '../models'; import { SchemaBlocksResponseJsonApi } from '../models/schema-blocks-json-api.model'; @@ -7,7 +7,7 @@ export class PageSchemaMapper { console.log('PageSchemaMapper.fromSchemaBlocksResponse', response); const pages: PageSchema[] = []; let currentPage!: PageSchema; - let currentQuestion!: Question; + let currentQuestion: Question | null = null; response.data.map((item) => { console.log('Processing item:', item); switch (item.attributes.block_type) { @@ -17,27 +17,58 @@ export class PageSchemaMapper { title: item.attributes.display_text, questions: [], }; + currentQuestion = null; pages.push(currentPage); break; + case BlockType.Paragraph: + if (!currentQuestion) { + currentPage.description = item.attributes.display_text; + } else { + currentQuestion.paragraphText = item.attributes.display_text; + } + break; + + case BlockType.SubsectionHeading: + currentQuestion = { + id: item.id, + displayText: item.attributes.display_text, + helpText: item.attributes.help_text, + exampleText: item.attributes.example_text, + required: item.attributes.required, + groupKey: item.attributes.schema_block_group_key, + responseKey: item.attributes.registration_response_key || undefined, + }; + currentPage.questions?.push(currentQuestion); + break; + case BlockType.QuestionLabel: console.log('QuestionLabel:'); currentQuestion = { id: item.id, - title: item.attributes.display_text, - description: item.attributes.help_text, - type: item.attributes.registration_response_key as Question['type'], + displayText: item.attributes.display_text, + helpText: item.attributes.help_text, + exampleText: item.attributes.example_text, required: item.attributes.required, groupKey: item.attributes.schema_block_group_key, responseKey: item.attributes.registration_response_key || undefined, }; currentPage.questions?.push(currentQuestion); break; - case BlockType.SelectInputOption: - console.log('SelectInputOption:', item); - currentQuestion.options = currentQuestion.options || []; - currentQuestion.options.push(item.attributes.display_text); + case BlockType.SelectInputOption: + if (currentQuestion) { + currentQuestion.fieldType = FieldType.Radio; + currentQuestion.options = currentQuestion?.options || []; + currentQuestion?.options.push(item.attributes.display_text); + } + break; + case BlockType.LongTextInput: + if (currentQuestion) { + currentQuestion.fieldType = FieldType.TextArea; + currentQuestion.exampleText = item.attributes.example_text; + currentQuestion.helpText = item.attributes.help_text; + } break; default: console.warn(`Unexpected block type: ${item.attributes.block_type}`); diff --git a/src/app/features/registries/models/page-schema.model.ts b/src/app/features/registries/models/page-schema.model.ts index 2d9bb31e8..d3d7de989 100644 --- a/src/app/features/registries/models/page-schema.model.ts +++ b/src/app/features/registries/models/page-schema.model.ts @@ -1,29 +1,21 @@ +import { FieldType } from '../enums'; + export interface PageSchema { id: string; title: string; + description?: string; questions?: Question[]; } export interface Question { id: string; - title: string; - description?: string; - type: QuestionType; + displayText: string; + exampleText?: string; + helpText?: string; + paragraphText?: string; + fieldType?: FieldType; options?: string[]; required: boolean; groupKey?: string; responseKey?: string; } - -export enum QuestionType { - Text = 'text', - TextArea = 'textarea', - Select = 'select', - MultiSelect = 'multi-select', - Checkbox = 'checkbox', - Radio = 'radio', - Date = 'date', - Number = 'number', - Email = 'email', - Url = 'url', -} diff --git a/src/app/shared/components/stepper/stepper.component.html b/src/app/shared/components/stepper/stepper.component.html index b647786b1..39c6c28c5 100644 --- a/src/app/shared/components/stepper/stepper.component.html +++ b/src/app/shared/components/stepper/stepper.component.html @@ -1,9 +1,19 @@
@for (step of steps(); track step.value; let i = $index) { -

{{ question.exampleText }}

@@ -74,11 +75,7 @@

> @if (option.helpText) { - + }

} diff --git a/src/app/features/registries/components/custom-step/custom-step.component.ts b/src/app/features/registries/components/custom-step/custom-step.component.ts index ce91247cc..fa37803cf 100644 --- a/src/app/features/registries/components/custom-step/custom-step.component.ts +++ b/src/app/features/registries/components/custom-step/custom-step.component.ts @@ -8,7 +8,6 @@ import { Inplace } from 'primeng/inplace'; import { InputText } from 'primeng/inputtext'; import { RadioButton } from 'primeng/radiobutton'; import { Textarea } from 'primeng/textarea'; -import { Tooltip } from 'primeng/tooltip'; import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; @@ -16,6 +15,8 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; +import { InfoIconComponent } from '@osf/shared/components'; + import { FieldType } from '../../enums'; import { RegistriesSelectors } from '../../store'; @@ -27,11 +28,12 @@ import { RegistriesSelectors } from '../../store'; RadioButton, FormsModule, Checkbox, - Tooltip, + InputText, NgTemplateOutlet, Inplace, TranslatePipe, + InfoIconComponent, ], templateUrl: './custom-step.component.html', styleUrl: './custom-step.component.scss', diff --git a/src/app/features/registries/components/new-registration/new-registration.component.ts b/src/app/features/registries/components/new-registration/new-registration.component.ts index 18996dd88..de7fbc31a 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.ts +++ b/src/app/features/registries/components/new-registration/new-registration.component.ts @@ -47,7 +47,6 @@ export class NewRegistrationComponent { this.actions.getProjects(); this.actions.getProviders(); effect(() => { - //set the provider value when the providers are loaded const provider = this.draftForm.get('provider')?.value; if (!provider) { this.draftForm.get('provider')?.setValue(this.providers()[0]?.id);