diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 4c70b0a90..afb99d181 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -4,6 +4,7 @@ import { AddonsState } from '@core/store/settings/addons'; import { UserState } from '@core/store/user'; import { MyProjectsState } from '@core/store/my-projects'; import { SearchState } from '@osf/features/search/store'; +import { InstitutionsState } from '@core/store/institutions'; export const STATES = [ AuthState, @@ -12,4 +13,5 @@ export const STATES = [ UserState, SearchState, MyProjectsState, + InstitutionsState, ]; diff --git a/src/app/core/constants/storage-locations.constant.ts b/src/app/core/constants/storage-locations.constant.ts new file mode 100644 index 000000000..bfa9226e6 --- /dev/null +++ b/src/app/core/constants/storage-locations.constant.ts @@ -0,0 +1,8 @@ +import { StorageLocation } from '@osf/features/my-projects/entities/storage-location.interface'; + +export const STORAGE_LOCATIONS: StorageLocation[] = [ + { label: 'United States', value: 'us' }, + { label: 'Canada - Montréal', value: 'ca-1' }, + { label: 'Germany - Frankfurt', value: 'de-1' }, + { label: 'Australia - Sydney', value: 'de-1' }, +]; diff --git a/src/app/core/services/json-api/json-api.service.ts b/src/app/core/services/json-api/json-api.service.ts index c1ce33697..b5eb8e6ac 100644 --- a/src/app/core/services/json-api/json-api.service.ts +++ b/src/app/core/services/json-api/json-api.service.ts @@ -44,9 +44,16 @@ export class JsonApiService { return httpParams; } - post(url: string, body: unknown): Observable { + post( + url: string, + body: unknown, + params?: Record, + ): Observable { return this.http - .post>(url, body, { headers: this.#headers }) + .post>(url, body, { + headers: this.#headers, + params: this.buildHttpParams(params), + }) .pipe(map((response) => response.data)); } diff --git a/src/app/core/store/institutions/index.ts b/src/app/core/store/institutions/index.ts new file mode 100644 index 000000000..63e4169a1 --- /dev/null +++ b/src/app/core/store/institutions/index.ts @@ -0,0 +1,4 @@ +export * from './institutions.state'; +export * from './institutions.selectors'; +export * from './institutions.model'; +export * from './institutions.actions'; diff --git a/src/app/core/store/institutions/institutions.actions.ts b/src/app/core/store/institutions/institutions.actions.ts new file mode 100644 index 000000000..2af0d870e --- /dev/null +++ b/src/app/core/store/institutions/institutions.actions.ts @@ -0,0 +1,3 @@ +export class GetUserInstitutions { + static readonly type = '[Institutions] Get User Institutions'; +} diff --git a/src/app/core/store/institutions/institutions.model.ts b/src/app/core/store/institutions/institutions.model.ts new file mode 100644 index 000000000..1849f2abf --- /dev/null +++ b/src/app/core/store/institutions/institutions.model.ts @@ -0,0 +1,5 @@ +import { Institution } from '@osf/features/institutions/entities/institutions.models'; + +export interface InstitutionsStateModel { + userInstitutions: Institution[]; +} diff --git a/src/app/core/store/institutions/institutions.selectors.ts b/src/app/core/store/institutions/institutions.selectors.ts new file mode 100644 index 000000000..cb6ab2b0b --- /dev/null +++ b/src/app/core/store/institutions/institutions.selectors.ts @@ -0,0 +1,10 @@ +import { Selector } from '@ngxs/store'; +import { InstitutionsStateModel } from './institutions.model'; +import { InstitutionsState } from './institutions.state'; + +export class InstitutionsSelectors { + @Selector([InstitutionsState]) + static getUserInstitutions(state: InstitutionsStateModel) { + return state.userInstitutions; + } +} diff --git a/src/app/core/store/institutions/institutions.state.ts b/src/app/core/store/institutions/institutions.state.ts new file mode 100644 index 000000000..0ef67fe23 --- /dev/null +++ b/src/app/core/store/institutions/institutions.state.ts @@ -0,0 +1,28 @@ +import { inject, Injectable } from '@angular/core'; +import { State, Action, StateContext } from '@ngxs/store'; +import { InstitutionsStateModel } from './institutions.model'; +import { GetUserInstitutions } from './institutions.actions'; +import { InstitutionsService } from '@osf/features/institutions/institutions.service'; +import { tap } from 'rxjs'; + +@State({ + name: 'institutions', + defaults: { + userInstitutions: [], + }, +}) +@Injectable() +export class InstitutionsState { + #institutionsService = inject(InstitutionsService); + + @Action(GetUserInstitutions) + getUserInstitutions(ctx: StateContext) { + return this.#institutionsService.getUserInstitutions().pipe( + tap((institutions) => { + ctx.patchState({ + userInstitutions: institutions, + }); + }), + ); + } +} diff --git a/src/app/core/store/my-projects/my-projects.actions.ts b/src/app/core/store/my-projects/my-projects.actions.ts index 170bc8bc1..1773823fa 100644 --- a/src/app/core/store/my-projects/my-projects.actions.ts +++ b/src/app/core/store/my-projects/my-projects.actions.ts @@ -47,3 +47,15 @@ export class GetBookmarksCollectionId { export class ClearMyProjects { static readonly type = '[My Projects] Clear Projects'; } + +export class CreateProject { + static readonly type = '[MyProjects] Create Project'; + + constructor( + public title: string, + public description: string, + public templateFrom: string, + public region: string, + public affiliations: string[], + ) {} +} diff --git a/src/app/core/store/my-projects/my-projects.state.ts b/src/app/core/store/my-projects/my-projects.state.ts index 819b34c20..afdca89b8 100644 --- a/src/app/core/store/my-projects/my-projects.state.ts +++ b/src/app/core/store/my-projects/my-projects.state.ts @@ -8,6 +8,7 @@ import { GetMyBookmarks, GetBookmarksCollectionId, ClearMyProjects, + CreateProject, } from './my-projects.actions'; import { MyProjectsService } from '@osf/features/my-projects/my-projects.service'; import { tap } from 'rxjs'; @@ -124,4 +125,28 @@ export class MyProjectsState { totalBookmarks: 0, }); } + + @Action(CreateProject) + createProject( + ctx: StateContext, + action: CreateProject, + ) { + return this.myProjectsService + .createProject( + action.title, + action.description, + action.templateFrom, + action.region, + action.affiliations, + ) + .pipe( + tap((project) => { + const state = ctx.getState(); + ctx.patchState({ + projects: [project, ...state.projects], + totalProjects: state.totalProjects + 1, + }); + }), + ); + } } diff --git a/src/app/features/home/home.component.html b/src/app/features/home/home.component.html index 9e1317b38..bb496f3d0 100644 --- a/src/app/features/home/home.component.html +++ b/src/app/features/home/home.component.html @@ -8,6 +8,7 @@ [title]="'Dashboard'" [icon]="'home'" [buttonLabel]="'Create New Project'" + (buttonClick)="createProject()" />
diff --git a/src/app/features/home/home.component.ts b/src/app/features/home/home.component.ts index dfcabefd3..fd210e573 100644 --- a/src/app/features/home/home.component.ts +++ b/src/app/features/home/home.component.ts @@ -27,6 +27,9 @@ import { import { debounceTime, distinctUntilChanged, Subject } from 'rxjs'; import { MyProjectsSearchFilters } from '@osf/features/my-projects/entities/my-projects-search-filters.models'; import { MyProjectsItem } from '@osf/features/my-projects/entities/my-projects.entities'; +import { GetUserInstitutions } from '@osf/core/store/institutions'; +import { DialogService } from 'primeng/dynamicdialog'; +import { AddProjectFormComponent } from '@shared/components/add-project-form/add-project-form.component'; @Component({ selector: 'osf-home', @@ -34,18 +37,23 @@ import { MyProjectsItem } from '@osf/features/my-projects/entities/my-projects.e imports: [RouterLink, Button, SubHeaderComponent, MyProjectsTableComponent], templateUrl: './home.component.html', styleUrl: './home.component.scss', + providers: [DialogService], }) export class HomeComponent implements OnInit { readonly #destroyRef = inject(DestroyRef); readonly #store = inject(Store); readonly #router = inject(Router); readonly #route = inject(ActivatedRoute); + readonly #dialogService = inject(DialogService); + readonly #isXSmall$ = inject(IS_XSMALL); + readonly #isMedium$ = inject(IS_MEDIUM); readonly #searchSubject = new Subject(); protected readonly isLoading = signal(false); + protected readonly isSubmitting = signal(false); - protected readonly isMedium = toSignal(inject(IS_MEDIUM)); - protected readonly isMobile = toSignal(inject(IS_XSMALL)); + protected readonly isMedium = toSignal(this.#isMedium$); + protected readonly isMobile = toSignal(this.#isXSmall$); protected readonly activeProject = signal(null); protected readonly searchValue = signal(''); @@ -77,6 +85,7 @@ export class HomeComponent implements OnInit { ngOnInit() { this.#setupQueryParamsSubscription(); + this.#store.dispatch(new GetUserInstitutions()); } #setupQueryParamsSubscription(): void { @@ -214,4 +223,22 @@ export class HomeComponent implements OnInit { this.activeProject.set(project); this.#router.navigate(['/my-projects', project.id]); } + + protected createProject(): void { + const dialogWidth = this.isMobile() ? '95vw' : '850px'; + this.isSubmitting.set(true); + + const dialogRef = this.#dialogService.open(AddProjectFormComponent, { + width: dialogWidth, + focusOnShow: false, + header: 'Create Project', + closeOnEscape: true, + modal: true, + closable: true, + }); + + dialogRef.onClose.subscribe(() => { + this.isSubmitting.set(false); + }); + } } diff --git a/src/app/features/institutions/entities/institutions.models.ts b/src/app/features/institutions/entities/institutions.models.ts new file mode 100644 index 000000000..4f1f8b8a9 --- /dev/null +++ b/src/app/features/institutions/entities/institutions.models.ts @@ -0,0 +1,35 @@ +export interface InstitutionAssets { + logo: string; + logo_rounded: string; + banner: string; +} + +export interface InstitutionAttributes { + name: string; + description: string; + iri: string; + ror_iri: string | null; + iris: string[]; + assets: InstitutionAssets; + institutional_request_access_enabled: boolean; + logo_path: string; +} + +export interface UserInstitutionGetResponse { + id: string; + type: string; + attributes: InstitutionAttributes; +} + +export interface Institution { + id: string; + type: string; + name: string; + description: string; + iri: string; + rorIri: string | null; + iris: string[]; + assets: InstitutionAssets; + institutionalRequestAccessEnabled: boolean; + logoPath: string; +} diff --git a/src/app/features/institutions/institutions.service.ts b/src/app/features/institutions/institutions.service.ts new file mode 100644 index 000000000..21d453ddd --- /dev/null +++ b/src/app/features/institutions/institutions.service.ts @@ -0,0 +1,29 @@ +import { inject, Injectable } from '@angular/core'; +import { JsonApiService } from '@core/services/json-api/json-api.service'; +import { Observable } from 'rxjs'; +import { + Institution, + UserInstitutionGetResponse, +} from './entities/institutions.models'; +import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; +import { map } from 'rxjs/operators'; +import { InstitutionsMapper } from './mappers/institutions.mapper'; + +@Injectable({ + providedIn: 'root', +}) +export class InstitutionsService { + #baseUrl = 'https://api.staging4.osf.io/v2/'; + #jsonApiService = inject(JsonApiService); + + getUserInstitutions(): Observable { + const url = this.#baseUrl + 'users/me/institutions/'; + return this.#jsonApiService + .get>(url) + .pipe( + map((response) => + response.data.map((item) => InstitutionsMapper.fromResponse(item)), + ), + ); + } +} diff --git a/src/app/features/institutions/mappers/institutions.mapper.ts b/src/app/features/institutions/mappers/institutions.mapper.ts new file mode 100644 index 000000000..c4e4f9254 --- /dev/null +++ b/src/app/features/institutions/mappers/institutions.mapper.ts @@ -0,0 +1,22 @@ +import { + Institution, + UserInstitutionGetResponse, +} from '../entities/institutions.models'; + +export class InstitutionsMapper { + static fromResponse(response: UserInstitutionGetResponse): Institution { + return { + id: response.id, + type: response.type, + name: response.attributes.name, + description: response.attributes.description, + iri: response.attributes.iri, + rorIri: response.attributes.ror_iri, + iris: response.attributes.iris, + assets: response.attributes.assets, + institutionalRequestAccessEnabled: + response.attributes.institutional_request_access_enabled, + logoPath: response.attributes.logo_path, + }; + } +} diff --git a/src/app/features/my-projects/entities/create-project.entities.ts b/src/app/features/my-projects/entities/create-project.entities.ts new file mode 100644 index 000000000..ea95d68ef --- /dev/null +++ b/src/app/features/my-projects/entities/create-project.entities.ts @@ -0,0 +1,25 @@ +export interface CreateProjectPayload { + data: { + type: 'nodes'; + attributes: { + title: string; + description?: string; + category: 'project'; + template_from?: string; + }; + relationships: { + region: { + data: { + type: 'regions'; + id: string; + }; + }; + affiliated_institutions?: { + data: { + type: 'institutions'; + id: string; + }[]; + }; + }; + }; +} diff --git a/src/app/features/my-projects/entities/my-projects.types.ts b/src/app/features/my-projects/entities/my-projects.types.ts index 58cbe7285..9bf202d9a 100644 --- a/src/app/features/my-projects/entities/my-projects.types.ts +++ b/src/app/features/my-projects/entities/my-projects.types.ts @@ -2,4 +2,4 @@ export type EndpointType = | 'nodes' | 'registrations' | 'preprints' - | `collections/${string}/linked_nodes`; + | `collections/${string}/linked_nodes/`; diff --git a/src/app/features/my-projects/entities/storage-location.interface.ts b/src/app/features/my-projects/entities/storage-location.interface.ts new file mode 100644 index 000000000..e41bb3b43 --- /dev/null +++ b/src/app/features/my-projects/entities/storage-location.interface.ts @@ -0,0 +1,4 @@ +export interface StorageLocation { + label: string; + value: string; +} diff --git a/src/app/features/my-projects/mappers/my-preprints.mapper.ts b/src/app/features/my-projects/mappers/my-preprints.mapper.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/features/my-projects/mappers/my-registrations.mapper.ts b/src/app/features/my-projects/mappers/my-registrations.mapper.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/features/my-projects/my-projects.component.ts b/src/app/features/my-projects/my-projects.component.ts index 8fdcaf079..83f194776 100644 --- a/src/app/features/my-projects/my-projects.component.ts +++ b/src/app/features/my-projects/my-projects.component.ts @@ -19,7 +19,7 @@ import { TabOption } from '@shared/entities/tab-option.interface'; import { TablePageEvent } from 'primeng/table'; import type { SortEvent } from 'primeng/api'; import { DialogService } from 'primeng/dynamicdialog'; -import { AddProjectFormComponent } from './add-project-form/add-project-form.component'; +import { AddProjectFormComponent } from '@shared/components/add-project-form/add-project-form.component'; import { Store } from '@ngxs/store'; import { ActivatedRoute, Router } from '@angular/router'; import { @@ -40,6 +40,7 @@ import { SortOrder } from '@shared/utils/sort-order.enum'; import { MyProjectsItem } from '@osf/features/my-projects/entities/my-projects.entities'; import { QueryParams } from '@osf/shared/entities/query-params.interface'; import { MyProjectsTableComponent } from '@shared/components/my-projects-table/my-projects-table.component'; +import { GetUserInstitutions } from '@core/store/institutions'; @Component({ selector: 'osf-my-projects', @@ -130,6 +131,7 @@ export class MyProjectsComponent implements OnInit { } ngOnInit(): void { + this.#store.dispatch(new GetUserInstitutions()); this.#store.dispatch(new GetBookmarksCollectionId()); } @@ -263,32 +265,6 @@ export class MyProjectsComponent implements OnInit { this.isLoading.set(false); }, }); - - // switch (this.selectedTab()) { - // case 0: - // this.#store.dispatch(new GetMyProjects(pageNumber, pageSize, filters)); - // break; - // case 1: - // this.#store.dispatch( - // new GetMyRegistrations(pageNumber, pageSize, filters), - // ); - // break; - // case 2: - // this.#store.dispatch(new GetMyPreprints(pageNumber, pageSize, filters)); - // break; - // case 3: - // if (this.bookmarksCollectionId()) { - // this.#store.dispatch( - // new GetMyBookmarks( - // this.bookmarksCollectionId(), - // pageNumber, - // pageSize, - // filters, - // ), - // ); - // } - // break; - // } } #createFilters(params: QueryParams): MyProjectsSearchFilters { diff --git a/src/app/features/my-projects/my-projects.service.ts b/src/app/features/my-projects/my-projects.service.ts index ce9a73a3d..2d8e7be38 100644 --- a/src/app/features/my-projects/my-projects.service.ts +++ b/src/app/features/my-projects/my-projects.service.ts @@ -8,10 +8,12 @@ import { MyProjectsItemGetResponse, MyProjectsJsonApiResponse, SparseCollectionsResponse, + MyProjectsItem, } from '@osf/features/my-projects/entities/my-projects.entities'; import { map } from 'rxjs/operators'; import { SortOrder } from '@shared/utils/sort-order.enum'; import { EndpointType } from '@osf/features/my-projects/entities/my-projects.types'; +import { CreateProjectPayload } from './entities/create-project.entities'; @Injectable({ providedIn: 'root', @@ -56,7 +58,7 @@ export class MyProjectsService { } else { params['sort'] = '-date_modified'; } - + // const url = this.#baseUrl + endpoint + '/'; const url = endpoint.startsWith('collections/') ? this.#baseUrl + endpoint : this.#baseUrl + 'users/me/' + endpoint; @@ -87,7 +89,7 @@ export class MyProjectsService { }; return this.#jsonApiService - .get(this.#baseUrl + 'collections', params) + .get(this.#baseUrl + 'collections/', params) .pipe( map((response) => { const bookmarksCollection = response.data.find( @@ -123,10 +125,60 @@ export class MyProjectsService { pageSize?: number, ): Observable { return this.#getMyItems( - `collections/${collectionId}/linked_nodes`, + `collections/${collectionId}/linked_nodes/`, filters, pageNumber, pageSize, ); } + + createProject( + title: string, + description: string, + templateFrom: string, + region: string, + affiliations: string[], + ): Observable { + const payload: CreateProjectPayload = { + data: { + type: 'nodes', + attributes: { + title, + ...(description && { description }), + category: 'project', + ...(templateFrom && { template_from: templateFrom }), + }, + relationships: { + region: { + data: { + type: 'regions', + id: region, + }, + }, + ...(affiliations.length > 0 && { + affiliated_institutions: { + data: affiliations.map((id) => ({ + type: 'institutions', + id, + })), + }, + }), + }, + }, + }; + + const params: Record = { + 'embed[]': ['bibliographic_contributors'], + 'fields[nodes]': 'title,date_modified,public,bibliographic_contributors', + 'fields[users]': 'family_name,full_name,given_name,middle_name', + }; + + return this.#jsonApiService + .post( + `${this.#baseUrl}nodes/`, + payload, + params, + ) + .pipe(map((response) => MyProjectsMapper.fromResponse(response))); + } } diff --git a/src/app/features/settings/addons/addons.service.ts b/src/app/features/settings/addons/addons.service.ts index 39aba6b0e..f4abc3843 100644 --- a/src/app/features/settings/addons/addons.service.ts +++ b/src/app/features/settings/addons/addons.service.ts @@ -49,7 +49,7 @@ export class AddonsService { return this.#jsonApiService .get< JsonApiResponse - >(this.#baseUrl + 'user-references', params) + >(this.#baseUrl + 'user-references/', params) .pipe(map((response) => response.data)); } @@ -63,7 +63,7 @@ export class AddonsService { return this.#jsonApiService .get< JsonApiResponse - >(this.#baseUrl + `user-references/${referenceId}/authorized_${addonType}_accounts?include=external-${addonType}-service`, params) + >(this.#baseUrl + `user-references/${referenceId}/authorized_${addonType}_accounts/?include=external-${addonType}-service`, params) .pipe( map((response) => { return response.data.map((item) => @@ -78,7 +78,7 @@ export class AddonsService { addonType: string, ): Observable { return this.#jsonApiService.post( - this.#baseUrl + `authorized-${addonType}-accounts`, + this.#baseUrl + `authorized-${addonType}-accounts/`, addonRequestPayload, ); } @@ -89,14 +89,14 @@ export class AddonsService { addonId: string, ): Observable { return this.#jsonApiService.patch( - this.#baseUrl + `authorized-${addonType}-accounts/${addonId}`, + this.#baseUrl + `authorized-${addonType}-accounts/${addonId}/`, addonRequestPayload, ); } deleteAuthorizedAddon(id: string, addonType: string): Observable { return this.#jsonApiService.delete( - this.#baseUrl + `authorized-${addonType}-accounts/${id}`, + this.#baseUrl + `authorized-${addonType}-accounts/${id}/`, ); } } diff --git a/src/app/features/settings/tokens/tokens.service.ts b/src/app/features/settings/tokens/tokens.service.ts index 8017bbe47..b6f6c4034 100644 --- a/src/app/features/settings/tokens/tokens.service.ts +++ b/src/app/features/settings/tokens/tokens.service.ts @@ -20,13 +20,13 @@ export class TokensService { getScopes(): Observable { return this.jsonApiService - .get>(this.baseUrl + 'scopes') + .get>(this.baseUrl + 'scopes/') .pipe(map((responses) => responses.data)); } getTokens(): Observable { return this.jsonApiService - .get>(this.baseUrl + 'tokens') + .get>(this.baseUrl + 'tokens/') .pipe( map((responses) => { return responses.data.map((response) => @@ -40,7 +40,7 @@ export class TokensService { return this.jsonApiService .get< JsonApiResponse - >(this.baseUrl + `tokens/${tokenId}`) + >(this.baseUrl + `tokens/${tokenId}/`) .pipe(map((response) => TokenMapper.fromGetResponse(response.data))); } @@ -60,11 +60,11 @@ export class TokensService { const request = TokenMapper.toRequest(name, scopes); return this.jsonApiService - .patch(this.baseUrl + `tokens/${tokenId}`, request) + .patch(this.baseUrl + `tokens/${tokenId}/`, request) .pipe(map((response) => TokenMapper.fromCreateResponse(response))); } deleteToken(tokenId: string): Observable { - return this.jsonApiService.delete(this.baseUrl + `tokens/${tokenId}`); + return this.jsonApiService.delete(this.baseUrl + `tokens/${tokenId}/`); } } diff --git a/src/app/features/my-projects/add-project-form/add-project-form.component.html b/src/app/shared/components/add-project-form/add-project-form.component.html similarity index 52% rename from src/app/features/my-projects/add-project-form/add-project-form.component.html rename to src/app/shared/components/add-project-form/add-project-form.component.html index 8cca85a2b..59711c971 100644 --- a/src/app/features/my-projects/add-project-form/add-project-form.component.html +++ b/src/app/shared/components/add-project-form/add-project-form.component.html @@ -25,50 +25,52 @@
-
-
-

Affiliation

-
- - Select All - - - Remove All - + @if (affiliations().length) { +
+
+

Affiliation

+
+ + Select All + + + Remove All + +
-
-
- @for (affiliation of affiliations; track affiliation.value) { -
- - OSF Logo -
- } +
+ @for (affiliation of affiliations(); track affiliation.id) { +
+ + OSF Logo +
+ } +
-
+ }
@@ -103,12 +105,14 @@

Affiliation

label="Cancel" severity="info" (click)="dialogRef.close()" + [disabled]="isSubmitting()" />
diff --git a/src/app/features/my-projects/add-project-form/add-project-form.component.scss b/src/app/shared/components/add-project-form/add-project-form.component.scss similarity index 100% rename from src/app/features/my-projects/add-project-form/add-project-form.component.scss rename to src/app/shared/components/add-project-form/add-project-form.component.scss diff --git a/src/app/features/my-projects/add-project-form/add-project-form.component.spec.ts b/src/app/shared/components/add-project-form/add-project-form.component.spec.ts similarity index 100% rename from src/app/features/my-projects/add-project-form/add-project-form.component.spec.ts rename to src/app/shared/components/add-project-form/add-project-form.component.spec.ts diff --git a/src/app/features/my-projects/add-project-form/add-project-form.component.ts b/src/app/shared/components/add-project-form/add-project-form.component.ts similarity index 63% rename from src/app/features/my-projects/add-project-form/add-project-form.component.ts rename to src/app/shared/components/add-project-form/add-project-form.component.ts index 40ca025ab..102e7a8ef 100644 --- a/src/app/features/my-projects/add-project-form/add-project-form.component.ts +++ b/src/app/shared/components/add-project-form/add-project-form.component.ts @@ -4,6 +4,7 @@ import { computed, inject, signal, + OnInit, } from '@angular/core'; import { CommonModule, NgOptimizedImage } from '@angular/common'; import { @@ -22,23 +23,16 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; import { Store } from '@ngxs/store'; -import { MyProjectsSelectors } from '@core/store/my-projects'; - -enum ProjectFormControls { - Title = 'title', - StorageLocation = 'storageLocation', - Affiliations = 'affiliations', - Description = 'description', - Template = 'template', -} - -interface ProjectForm { - [ProjectFormControls.Title]: FormControl; - [ProjectFormControls.StorageLocation]: FormControl; - [ProjectFormControls.Affiliations]: FormControl; - [ProjectFormControls.Description]: FormControl; - [ProjectFormControls.Template]: FormControl; -} +import { STORAGE_LOCATIONS } from '@core/constants/storage-locations.constant'; +import { + CreateProject, + GetMyProjects, + MyProjectsSelectors, +} from '@core/store/my-projects'; +import { InstitutionsSelectors } from '@core/store/institutions'; +import { MY_PROJECTS_TABLE_PARAMS } from '@core/constants/my-projects-table.constants'; +import { ProjectForm } from '@shared/entities/create-project-form.interface'; +import { ProjectFormControls } from '@osf/shared/entities/create-project-form-controls.enum'; @Component({ selector: 'osf-add-project-form', @@ -58,7 +52,7 @@ interface ProjectForm { styleUrl: './add-project-form.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AddProjectFormComponent { +export class AddProjectFormComponent implements OnInit { #store = inject(Store); protected readonly projects = this.#store.selectSignal( MyProjectsSelectors.getProjects, @@ -67,23 +61,18 @@ export class AddProjectFormComponent { protected readonly dialogRef = inject(DynamicDialogRef); protected readonly ProjectFormControls = ProjectFormControls; protected readonly hasTemplateSelected = signal(false); + protected readonly isSubmitting = signal(false); - protected readonly storageLocations = [ - { label: 'United States', value: 'us' }, - { label: 'Canada - Montréal', value: 'ca' }, - { label: 'Germany - Frankfurt', value: 'de-1' }, - ]; + protected readonly storageLocations = STORAGE_LOCATIONS; - protected readonly affiliations = [ - { label: 'Affiliation 1', value: 'aff1' }, - { label: 'Affiliation 2', value: 'aff2' }, - { label: 'Affiliation 3', value: 'aff3' }, - ]; + protected readonly affiliations = this.#store.selectSignal( + InstitutionsSelectors.getUserInstitutions, + ); protected projectTemplateOptions = computed(() => { return this.projects().map((project) => ({ label: project.title, - value: project.title, + value: project.id, })); }); @@ -115,8 +104,14 @@ export class AddProjectFormComponent { }); } + ngOnInit(): void { + this.#store.dispatch( + new GetMyProjects(1, MY_PROJECTS_TABLE_PARAMS.rows, {}), + ); + } + selectAllAffiliations(): void { - const allAffiliationValues = this.affiliations.map((aff) => aff.value); + const allAffiliationValues = this.affiliations().map((aff) => aff.id); this.projectForm .get(ProjectFormControls.Affiliations) ?.setValue(allAffiliationValues); @@ -132,7 +127,30 @@ export class AddProjectFormComponent { return; } - // TODO: Integrate with API - this.dialogRef.close(); + const formValue = this.projectForm.getRawValue(); + this.isSubmitting.set(true); + + this.#store + .dispatch( + new CreateProject( + formValue.title, + formValue.description, + formValue.template, + formValue.storageLocation, + formValue.affiliations, + ), + ) + .subscribe({ + next: () => { + this.#store.dispatch( + new GetMyProjects(1, MY_PROJECTS_TABLE_PARAMS.rows, {}), + ); + this.dialogRef.close(); + }, + error: (error) => { + console.error('Failed to create project:', error); + this.isSubmitting.set(false); + }, + }); } } diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.html b/src/app/shared/components/my-projects-table/my-projects-table.component.html index 2c61deb38..5b9193a72 100644 --- a/src/app/shared/components/my-projects-table/my-projects-table.component.html +++ b/src/app/shared/components/my-projects-table/my-projects-table.component.html @@ -1,4 +1,4 @@ -
+
; + [ProjectFormControls.StorageLocation]: FormControl; + [ProjectFormControls.Affiliations]: FormControl; + [ProjectFormControls.Description]: FormControl; + [ProjectFormControls.Template]: FormControl; +} diff --git a/src/assets/styles/overrides/input.scss b/src/assets/styles/overrides/input.scss index 4c9313f92..6de8e1c53 100644 --- a/src/assets/styles/overrides/input.scss +++ b/src/assets/styles/overrides/input.scss @@ -63,6 +63,7 @@ p-password.ng-touched.ng-invalid { font-size: 16px; border-radius: 0.57rem; box-shadow: none; + resize: none; } .addon-input { diff --git a/src/assets/styles/overrides/table.scss b/src/assets/styles/overrides/table.scss index 52362e375..5953d0892 100644 --- a/src/assets/styles/overrides/table.scss +++ b/src/assets/styles/overrides/table.scss @@ -48,6 +48,10 @@ } .my-projects-table { + tr:not(.loading-row) { + cursor: pointer; + } + &.loading { table { tr.loading-row { @@ -58,6 +62,14 @@ width: 100%; } } + + tr:hover { + background: inherit; + + td { + background: inherit; + } + } } } }