Skip to content

Feat/473 add project modal search #276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const routes: Routes = [
import('./features/home/pages/dashboard/dashboard.component').then((mod) => mod.DashboardComponent),
data: { skipBreadcrumbs: true },
canActivate: [authGuard],
providers: [provideStates([ProjectsState])],
},
{
path: 'confirm/:userId/:token',
Expand Down Expand Up @@ -73,7 +74,7 @@ export const routes: Routes = [
path: 'my-projects',
loadComponent: () =>
import('./features/my-projects/my-projects.component').then((mod) => mod.MyProjectsComponent),
providers: [provideStates([BookmarksState])],
providers: [provideStates([BookmarksState, ProjectsState])],
canActivate: [authGuard],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<p-step [value]="targetStepValue()" [disabled]="isDisabled()">
<ng-template #content>
<div class="flex flex-column gap-4 w-full">
<h3>{{ 'collections.addToCollection.projectMetadata' | translate }}</h3>
<h3>{{ 'collections.addToCollection.resourceMetadata' | translate }}</h3>
@if (!isDisabled() && stepperActiveValue() !== targetStepValue()) {
<div>
<p class="font-bold">{{ 'collections.addToCollection.form.title' | translate }}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,12 @@ <h3>{{ 'collections.addToCollection.selectProject' | translate }}</h3>
<p-step-panel [value]="targetStepValue()" class="p-0">
<ng-template class="m-0" #content>
<div class="mt-4 w-full lg:w-6">
<p-select
class="w-full"
[loading]="isProjectsLoading()"
[options]="projectsOptions()"
[filter]="true"
(onFilter)="handleFilterSearch($event)"
optionLabel="label"
optionValue="value"
[appendTo]="'body'"
[emptyFilterMessage]="filterMessage()"
[emptyMessage]="filterMessage()"
(onChange)="handleProjectChange($event)"
[placeholder]="'common.buttons.select' | translate"
[ngModel]="selectedProject()"
>
<ng-template #selectedItem let-selectedOption>
{{ selectedOption.label | translate }}
</ng-template>
</p-select>
<osf-project-selector
[excludeProjectIds]="excludedProjectIds()"
[(selectedProject)]="currentSelectedProject"
(projectChange)="handleProjectChange($event)"
(projectsLoaded)="handleProjectsLoaded($event)"
/>
</div>
</ng-template>
</p-step-panel>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,29 @@
import { createDispatchMap, select } from '@ngxs/store';

import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { TranslatePipe } from '@ngx-translate/core';

import { Button } from 'primeng/button';
import { Select, SelectChangeEvent, SelectFilterEvent } from 'primeng/select';
import { Step, StepItem, StepPanel } from 'primeng/stepper';

import { debounceTime, distinctUntilChanged, Subject, takeUntil } from 'rxjs';
import { ChangeDetectionStrategy, Component, computed, input, output, signal } from '@angular/core';

import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
effect,
inject,
input,
output,
signal,
} from '@angular/core';
import { FormsModule } from '@angular/forms';

import { UserSelectors } from '@core/store/user';
import { AddToCollectionSteps } from '@osf/features/collections/enums';
import { GetProjects, SetSelectedProject } from '@osf/shared/stores';
import { CustomOption } from '@shared/models';
import { SetSelectedProject } from '@osf/shared/stores';
import { ProjectSelectorComponent } from '@shared/components';
import { Project } from '@shared/models/projects';
import { CollectionsSelectors, GetUserCollectionSubmissions } from '@shared/stores/collections';
import { ProjectsSelectors } from '@shared/stores/projects/projects.selectors';

@Component({
selector: 'osf-select-project-step',
imports: [Button, TranslatePipe, Select, FormsModule, Step, StepItem, StepPanel],
imports: [Button, TranslatePipe, ProjectSelectorComponent, Step, StepItem, StepPanel],
templateUrl: './select-project-step.component.html',
styleUrl: './select-project-step.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SelectProjectStepComponent {
private readonly destroyRef = inject(DestroyRef);
private readonly translateService = inject(TranslateService);
private readonly destroy$ = new Subject<void>();
private readonly filterSubject = new Subject<string>();

protected projects = select(ProjectsSelectors.getProjects);
protected isProjectsLoading = select(ProjectsSelectors.getProjectsLoading);
protected selectedProject = select(ProjectsSelectors.getSelectedProject);
protected currentUser = select(UserSelectors.getCurrentUser);
protected currentUserSubmissions = select(CollectionsSelectors.getUserCollectionSubmissions);
protected isSubmissionsLoading = select(CollectionsSelectors.getUserCollectionSubmissionsLoading);

stepperActiveValue = input.required<number>();
targetStepValue = input.required<number>();
Expand All @@ -56,107 +32,36 @@ export class SelectProjectStepComponent {
stepChange = output<number>();
projectSelected = output<void>();

protected projectsOptions = signal<CustomOption<Project>[]>([]);
currentSelectedProject = signal<Project | null>(null);

protected filterMessage = computed(() => {
const isLoading = this.isProjectsLoading() || this.isSubmissionsLoading();
return isLoading
? this.translateService.instant('collections.addToCollection.form.loadingPlaceholder')
: this.translateService.instant('collections.addToCollection.form.noProjectsFound');
protected excludedProjectIds = computed(() => {
const submissions = this.currentUserSubmissions();
return submissions.map((submission) => submission.nodeId);
});

protected actions = createDispatchMap({
getProjects: GetProjects,
setSelectedProject: SetSelectedProject,
getUserCollectionSubmissions: GetUserCollectionSubmissions,
});

constructor() {
this.setupEffects();
this.setupFilterDebounce();
}

handleProjectChange(event: SelectChangeEvent) {
const project = event.value;
handleProjectChange(project: Project | null): void {
if (project) {
this.currentSelectedProject.set(project);
this.actions.setSelectedProject(project);
this.projectSelected.emit();
this.stepChange.emit(AddToCollectionSteps.ProjectMetadata);
}
}

handleFilterSearch(event: SelectFilterEvent) {
event.originalEvent.preventDefault();
this.filterSubject.next(event.filter);
handleProjectsLoaded(projects: Project[]): void {
const collectionId = this.collectionId();
if (collectionId && projects.length) {
const projectIds = projects.map((project) => project.id);
this.actions.getUserCollectionSubmissions(collectionId, projectIds);
}
}

handleEditStep() {
this.stepChange.emit(this.targetStepValue());
}

private setupEffects(): void {
effect(() => {
const currentUser = this.currentUser();
if (currentUser) {
this.actions.getProjects(currentUser.id);
}
});

effect(() => {
const projects = this.projects();
const collectionId = this.collectionId();
const isProjectsLoading = this.isProjectsLoading();

if (projects.length && collectionId && !isProjectsLoading) {
const projectIds = projects.map((project) => project.id);
this.actions.getUserCollectionSubmissions(collectionId, projectIds);
}
});

effect(() => {
const isProjectsLoading = this.isProjectsLoading();
const isSubmissionsLoading = this.isSubmissionsLoading();
const projects = this.projects();
const submissions = this.currentUserSubmissions();

if (isProjectsLoading || isSubmissionsLoading || !projects.length) {
this.projectsOptions.set([]);
}

if (!isProjectsLoading && !isSubmissionsLoading && projects.length) {
const submissionProjectIds = new Set(submissions.map((submission) => submission.nodeId));
const availableProjects = projects.filter((project) => !submissionProjectIds.has(project.id));

const options = availableProjects.map((project) => ({
label: project.title,
value: project,
}));

this.projectsOptions.set(options);
}
});

effect(() => {
this.destroyRef.onDestroy(() => {
this.destroy$.next();
this.destroy$.complete();
});
});
}

private setupFilterDebounce(): void {
this.filterSubject
.pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.destroy$))
.subscribe((filterValue) => {
const currentUser = this.currentUser();
if (!currentUser) return;

const params: Record<string, string> = {
'filter[current_user_permissions]': 'admin',
'filter[title]': filterValue,
};

this.actions.getProjects(currentUser.id, params);
});
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<osf-add-project-form [templates]="templates()" [projectForm]="projectForm" />
<osf-add-project-form [projectForm]="projectForm" />

<div class="flex justify-content-end gap-2 mt-4">
<p-button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { TranslatePipe } from '@ngx-translate/core';
import { Button } from 'primeng/button';
import { DynamicDialogRef } from 'primeng/dynamicdialog';

import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

import { MY_PROJECTS_TABLE_PARAMS } from '@osf/shared/constants';
import { CustomValidators } from '@osf/shared/helpers';
import { AddProjectFormComponent } from '@shared/components';
import { ProjectFormControls } from '@shared/enums';
import { IdName, ProjectForm } from '@shared/models';
import { ProjectForm } from '@shared/models';
import { CreateProject, GetMyProjects, MyResourcesSelectors } from '@shared/stores';

@Component({
Expand All @@ -22,25 +22,14 @@ import { CreateProject, GetMyProjects, MyResourcesSelectors } from '@shared/stor
styleUrl: './create-project-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateProjectDialogComponent implements OnInit {
export class CreateProjectDialogComponent {
protected readonly dialogRef = inject(DynamicDialogRef);

private actions = createDispatchMap({
getMyProjects: GetMyProjects,
createProject: CreateProject,
});

private projects = select(MyResourcesSelectors.getProjects);

readonly templates = computed(() => {
return this.projects().map(
(project) =>
({
id: project.id,
name: project.title,
}) as IdName
);
});
readonly isProjectSubmitting = select(MyResourcesSelectors.isProjectSubmitting);

readonly projectForm = new FormGroup<ProjectForm>({
Expand All @@ -63,10 +52,6 @@ export class CreateProjectDialogComponent implements OnInit {
}),
});

ngOnInit(): void {
this.actions.getMyProjects(1, MY_PROJECTS_TABLE_PARAMS.rows, {});
}

submitForm(): void {
if (this.projectForm.invalid) {
this.projectForm.markAllAsTouched();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ <h2>{{ 'preprints.preprintStepper.supplements.title' | translate }}</h2>
</p-card>
} @else if (selectedSupplementOption() === SupplementOptions.CreateNewProject) {
<p-card styleClass="m-t-24" class="card">
<osf-add-project-form [templates]="availableProjects()" [projectForm]="createProjectForm" />
<osf-add-project-form [projectForm]="createProjectForm" />
</p-card>
}
} @else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,12 @@ <h3 class="font-normal mb-2">
<label for="template">
{{ 'myProjects.createProject.template.label' | translate }}
</label>
<p-select
<osf-project-selector
id="template"
[options]="templates()"
optionValue="id"
optionLabel="name"
[formControlName]="ProjectFormControls.Template"
[placeholder]="'myProjects.createProject.template.placeholder' | translate"
[filter]="true"
styleClass="w-full"
appendTo="body"
placeholder="myProjects.createProject.template.placeholder"
[showClear]="hasTemplateSelected()"
[(selectedProject)]="selectedTemplate"
(projectChange)="onTemplateChange($event)"
/>
</div>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { ChangeDetectionStrategy, Component, input, OnInit, signal } from '@angu
import { FormGroup, ReactiveFormsModule } from '@angular/forms';

import { ProjectFormControls } from '@osf/shared/enums';
import { IdName, ProjectForm } from '@osf/shared/models';
import { ProjectForm } from '@osf/shared/models';
import { Project } from '@osf/shared/models/projects';
import { ProjectSelectorComponent } from '@shared/components/project-selector/project-selector.component';
import { FetchUserInstitutions, InstitutionsSelectors } from '@shared/stores';
import { FetchRegions, RegionsSelectors } from '@shared/stores/regions';

Expand All @@ -29,6 +31,7 @@ import { FetchRegions, RegionsSelectors } from '@shared/stores/regions';
Textarea,
NgOptimizedImage,
TranslatePipe,
ProjectSelectorComponent,
],
templateUrl: './add-project-form.component.html',
styleUrl: './add-project-form.component.scss',
Expand All @@ -40,11 +43,10 @@ export class AddProjectFormComponent implements OnInit {
fetchRegions: FetchRegions,
});

templates = input.required<IdName[]>();

ProjectFormControls = ProjectFormControls;

hasTemplateSelected = signal(false);
selectedTemplate = signal<Project | null>(null);
isSubmitting = signal(false);
storageLocations = select(RegionsSelectors.getRegions);
areStorageLocationsLoading = select(RegionsSelectors.areRegionsLoading);
Expand All @@ -65,6 +67,13 @@ export class AddProjectFormComponent implements OnInit {
});
}

onTemplateChange(project: Project | null): void {
if (!project) return;
this.selectedTemplate.set(project);
this.projectForm().get(ProjectFormControls.Template)?.setValue(project.id);
this.hasTemplateSelected.set(!!project);
}

selectAllAffiliations(): void {
const allAffiliationValues = this.affiliations().map((aff) => aff.id);
this.projectForm().get(ProjectFormControls.Affiliations)?.setValue(allAffiliationValues);
Expand Down
1 change: 1 addition & 0 deletions src/app/shared/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { MarkdownComponent } from './markdown/markdown.component';
export { MyProjectsTableComponent } from './my-projects-table/my-projects-table.component';
export { PasswordInputHintComponent } from './password-input-hint/password-input-hint.component';
export { PieChartComponent } from './pie-chart/pie-chart.component';
export { ProjectSelectorComponent } from './project-selector/project-selector.component';
export { ReadonlyInputComponent } from './readonly-input/readonly-input.component';
export { RegistrationBlocksDataComponent } from './registration-blocks-data/registration-blocks-data.component';
export { ResourceCardComponent } from './resource-card/resource-card.component';
Expand Down
Loading