Skip to content

Commit 14b069a

Browse files
authored
Feat/473 add project modal search (#276)
* feat(add-project-modal-search): added reusable project selector component * feat(add-project-modal-search): used project selector in the select project step component * feat(add-project-modal-search): removed comment * feat(add-project-modal-search): removed destroy subjet
1 parent 0f42cde commit 14b069a

File tree

14 files changed

+260
-165
lines changed

14 files changed

+260
-165
lines changed

src/app/app.routes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const routes: Routes = [
3535
import('./features/home/pages/dashboard/dashboard.component').then((mod) => mod.DashboardComponent),
3636
data: { skipBreadcrumbs: true },
3737
canActivate: [authGuard],
38+
providers: [provideStates([ProjectsState])],
3839
},
3940
{
4041
path: 'confirm/:userId/:token',
@@ -73,7 +74,7 @@ export const routes: Routes = [
7374
path: 'my-projects',
7475
loadComponent: () =>
7576
import('./features/my-projects/my-projects.component').then((mod) => mod.MyProjectsComponent),
76-
providers: [provideStates([BookmarksState])],
77+
providers: [provideStates([BookmarksState, ProjectsState])],
7778
canActivate: [authGuard],
7879
},
7980
{

src/app/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<p-step [value]="targetStepValue()" [disabled]="isDisabled()">
88
<ng-template #content>
99
<div class="flex flex-column gap-4 w-full">
10-
<h3>{{ 'collections.addToCollection.projectMetadata' | translate }}</h3>
10+
<h3>{{ 'collections.addToCollection.resourceMetadata' | translate }}</h3>
1111
@if (!isDisabled() && stepperActiveValue() !== targetStepValue()) {
1212
<div>
1313
<p class="font-bold">{{ 'collections.addToCollection.form.title' | translate }}</p>

src/app/features/collections/components/add-to-collection/select-project-step/select-project-step.component.html

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,12 @@ <h3>{{ 'collections.addToCollection.selectProject' | translate }}</h3>
2222
<p-step-panel [value]="targetStepValue()" class="p-0">
2323
<ng-template class="m-0" #content>
2424
<div class="mt-4 w-full lg:w-6">
25-
<p-select
26-
class="w-full"
27-
[loading]="isProjectsLoading()"
28-
[options]="projectsOptions()"
29-
[filter]="true"
30-
(onFilter)="handleFilterSearch($event)"
31-
optionLabel="label"
32-
optionValue="value"
33-
[appendTo]="'body'"
34-
[emptyFilterMessage]="filterMessage()"
35-
[emptyMessage]="filterMessage()"
36-
(onChange)="handleProjectChange($event)"
37-
[placeholder]="'common.buttons.select' | translate"
38-
[ngModel]="selectedProject()"
39-
>
40-
<ng-template #selectedItem let-selectedOption>
41-
{{ selectedOption.label | translate }}
42-
</ng-template>
43-
</p-select>
25+
<osf-project-selector
26+
[excludeProjectIds]="excludedProjectIds()"
27+
[(selectedProject)]="currentSelectedProject"
28+
(projectChange)="handleProjectChange($event)"
29+
(projectsLoaded)="handleProjectsLoaded($event)"
30+
/>
4431
</div>
4532
</ng-template>
4633
</p-step-panel>
Lines changed: 17 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,29 @@
11
import { createDispatchMap, select } from '@ngxs/store';
22

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

55
import { Button } from 'primeng/button';
6-
import { Select, SelectChangeEvent, SelectFilterEvent } from 'primeng/select';
76
import { Step, StepItem, StepPanel } from 'primeng/stepper';
87

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

11-
import {
12-
ChangeDetectionStrategy,
13-
Component,
14-
computed,
15-
DestroyRef,
16-
effect,
17-
inject,
18-
input,
19-
output,
20-
signal,
21-
} from '@angular/core';
22-
import { FormsModule } from '@angular/forms';
23-
24-
import { UserSelectors } from '@core/store/user';
2510
import { AddToCollectionSteps } from '@osf/features/collections/enums';
26-
import { GetProjects, SetSelectedProject } from '@osf/shared/stores';
27-
import { CustomOption } from '@shared/models';
11+
import { SetSelectedProject } from '@osf/shared/stores';
12+
import { ProjectSelectorComponent } from '@shared/components';
2813
import { Project } from '@shared/models/projects';
2914
import { CollectionsSelectors, GetUserCollectionSubmissions } from '@shared/stores/collections';
3015
import { ProjectsSelectors } from '@shared/stores/projects/projects.selectors';
3116

3217
@Component({
3318
selector: 'osf-select-project-step',
34-
imports: [Button, TranslatePipe, Select, FormsModule, Step, StepItem, StepPanel],
19+
imports: [Button, TranslatePipe, ProjectSelectorComponent, Step, StepItem, StepPanel],
3520
templateUrl: './select-project-step.component.html',
3621
styleUrl: './select-project-step.component.scss',
3722
changeDetection: ChangeDetectionStrategy.OnPush,
3823
})
3924
export class SelectProjectStepComponent {
40-
private readonly destroyRef = inject(DestroyRef);
41-
private readonly translateService = inject(TranslateService);
42-
private readonly destroy$ = new Subject<void>();
43-
private readonly filterSubject = new Subject<string>();
44-
45-
protected projects = select(ProjectsSelectors.getProjects);
46-
protected isProjectsLoading = select(ProjectsSelectors.getProjectsLoading);
4725
protected selectedProject = select(ProjectsSelectors.getSelectedProject);
48-
protected currentUser = select(UserSelectors.getCurrentUser);
4926
protected currentUserSubmissions = select(CollectionsSelectors.getUserCollectionSubmissions);
50-
protected isSubmissionsLoading = select(CollectionsSelectors.getUserCollectionSubmissionsLoading);
5127

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

59-
protected projectsOptions = signal<CustomOption<Project>[]>([]);
35+
currentSelectedProject = signal<Project | null>(null);
6036

61-
protected filterMessage = computed(() => {
62-
const isLoading = this.isProjectsLoading() || this.isSubmissionsLoading();
63-
return isLoading
64-
? this.translateService.instant('collections.addToCollection.form.loadingPlaceholder')
65-
: this.translateService.instant('collections.addToCollection.form.noProjectsFound');
37+
protected excludedProjectIds = computed(() => {
38+
const submissions = this.currentUserSubmissions();
39+
return submissions.map((submission) => submission.nodeId);
6640
});
6741

6842
protected actions = createDispatchMap({
69-
getProjects: GetProjects,
7043
setSelectedProject: SetSelectedProject,
7144
getUserCollectionSubmissions: GetUserCollectionSubmissions,
7245
});
7346

74-
constructor() {
75-
this.setupEffects();
76-
this.setupFilterDebounce();
77-
}
78-
79-
handleProjectChange(event: SelectChangeEvent) {
80-
const project = event.value;
47+
handleProjectChange(project: Project | null): void {
8148
if (project) {
49+
this.currentSelectedProject.set(project);
8250
this.actions.setSelectedProject(project);
8351
this.projectSelected.emit();
8452
this.stepChange.emit(AddToCollectionSteps.ProjectMetadata);
8553
}
8654
}
8755

88-
handleFilterSearch(event: SelectFilterEvent) {
89-
event.originalEvent.preventDefault();
90-
this.filterSubject.next(event.filter);
56+
handleProjectsLoaded(projects: Project[]): void {
57+
const collectionId = this.collectionId();
58+
if (collectionId && projects.length) {
59+
const projectIds = projects.map((project) => project.id);
60+
this.actions.getUserCollectionSubmissions(collectionId, projectIds);
61+
}
9162
}
9263

9364
handleEditStep() {
9465
this.stepChange.emit(this.targetStepValue());
9566
}
96-
97-
private setupEffects(): void {
98-
effect(() => {
99-
const currentUser = this.currentUser();
100-
if (currentUser) {
101-
this.actions.getProjects(currentUser.id);
102-
}
103-
});
104-
105-
effect(() => {
106-
const projects = this.projects();
107-
const collectionId = this.collectionId();
108-
const isProjectsLoading = this.isProjectsLoading();
109-
110-
if (projects.length && collectionId && !isProjectsLoading) {
111-
const projectIds = projects.map((project) => project.id);
112-
this.actions.getUserCollectionSubmissions(collectionId, projectIds);
113-
}
114-
});
115-
116-
effect(() => {
117-
const isProjectsLoading = this.isProjectsLoading();
118-
const isSubmissionsLoading = this.isSubmissionsLoading();
119-
const projects = this.projects();
120-
const submissions = this.currentUserSubmissions();
121-
122-
if (isProjectsLoading || isSubmissionsLoading || !projects.length) {
123-
this.projectsOptions.set([]);
124-
}
125-
126-
if (!isProjectsLoading && !isSubmissionsLoading && projects.length) {
127-
const submissionProjectIds = new Set(submissions.map((submission) => submission.nodeId));
128-
const availableProjects = projects.filter((project) => !submissionProjectIds.has(project.id));
129-
130-
const options = availableProjects.map((project) => ({
131-
label: project.title,
132-
value: project,
133-
}));
134-
135-
this.projectsOptions.set(options);
136-
}
137-
});
138-
139-
effect(() => {
140-
this.destroyRef.onDestroy(() => {
141-
this.destroy$.next();
142-
this.destroy$.complete();
143-
});
144-
});
145-
}
146-
147-
private setupFilterDebounce(): void {
148-
this.filterSubject
149-
.pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.destroy$))
150-
.subscribe((filterValue) => {
151-
const currentUser = this.currentUser();
152-
if (!currentUser) return;
153-
154-
const params: Record<string, string> = {
155-
'filter[current_user_permissions]': 'admin',
156-
'filter[title]': filterValue,
157-
};
158-
159-
this.actions.getProjects(currentUser.id, params);
160-
});
161-
}
16267
}

src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<osf-add-project-form [templates]="templates()" [projectForm]="projectForm" />
1+
<osf-add-project-form [projectForm]="projectForm" />
22

33
<div class="flex justify-content-end gap-2 mt-4">
44
<p-button

src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import { TranslatePipe } from '@ngx-translate/core';
55
import { Button } from 'primeng/button';
66
import { DynamicDialogRef } from 'primeng/dynamicdialog';
77

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

1111
import { MY_PROJECTS_TABLE_PARAMS } from '@osf/shared/constants';
1212
import { CustomValidators } from '@osf/shared/helpers';
1313
import { AddProjectFormComponent } from '@shared/components';
1414
import { ProjectFormControls } from '@shared/enums';
15-
import { IdName, ProjectForm } from '@shared/models';
15+
import { ProjectForm } from '@shared/models';
1616
import { CreateProject, GetMyProjects, MyResourcesSelectors } from '@shared/stores';
1717

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

2828
private actions = createDispatchMap({
2929
getMyProjects: GetMyProjects,
3030
createProject: CreateProject,
3131
});
3232

33-
private projects = select(MyResourcesSelectors.getProjects);
34-
35-
readonly templates = computed(() => {
36-
return this.projects().map(
37-
(project) =>
38-
({
39-
id: project.id,
40-
name: project.title,
41-
}) as IdName
42-
);
43-
});
4433
readonly isProjectSubmitting = select(MyResourcesSelectors.isProjectSubmitting);
4534

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

66-
ngOnInit(): void {
67-
this.actions.getMyProjects(1, MY_PROJECTS_TABLE_PARAMS.rows, {});
68-
}
69-
7055
submitForm(): void {
7156
if (this.projectForm.invalid) {
7257
this.projectForm.markAllAsTouched();

src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ <h2>{{ 'preprints.preprintStepper.supplements.title' | translate }}</h2>
5858
</p-card>
5959
} @else if (selectedSupplementOption() === SupplementOptions.CreateNewProject) {
6060
<p-card styleClass="m-t-24" class="card">
61-
<osf-add-project-form [templates]="availableProjects()" [projectForm]="createProjectForm" />
61+
<osf-add-project-form [projectForm]="createProjectForm" />
6262
</p-card>
6363
}
6464
} @else {

src/app/shared/components/add-project-form/add-project-form.component.html

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,12 @@ <h3 class="font-normal mb-2">
6969
<label for="template">
7070
{{ 'myProjects.createProject.template.label' | translate }}
7171
</label>
72-
<p-select
72+
<osf-project-selector
7373
id="template"
74-
[options]="templates()"
75-
optionValue="id"
76-
optionLabel="name"
77-
[formControlName]="ProjectFormControls.Template"
78-
[placeholder]="'myProjects.createProject.template.placeholder' | translate"
79-
[filter]="true"
80-
styleClass="w-full"
81-
appendTo="body"
74+
placeholder="myProjects.createProject.template.placeholder"
8275
[showClear]="hasTemplateSelected()"
76+
[(selectedProject)]="selectedTemplate"
77+
(projectChange)="onTemplateChange($event)"
8378
/>
8479
</div>
8580
</form>

src/app/shared/components/add-project-form/add-project-form.component.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import { ChangeDetectionStrategy, Component, input, OnInit, signal } from '@angu
1313
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
1414

1515
import { ProjectFormControls } from '@osf/shared/enums';
16-
import { IdName, ProjectForm } from '@osf/shared/models';
16+
import { ProjectForm } from '@osf/shared/models';
17+
import { Project } from '@osf/shared/models/projects';
18+
import { ProjectSelectorComponent } from '@shared/components/project-selector/project-selector.component';
1719
import { FetchUserInstitutions, InstitutionsSelectors } from '@shared/stores';
1820
import { FetchRegions, RegionsSelectors } from '@shared/stores/regions';
1921

@@ -29,6 +31,7 @@ import { FetchRegions, RegionsSelectors } from '@shared/stores/regions';
2931
Textarea,
3032
NgOptimizedImage,
3133
TranslatePipe,
34+
ProjectSelectorComponent,
3235
],
3336
templateUrl: './add-project-form.component.html',
3437
styleUrl: './add-project-form.component.scss',
@@ -40,11 +43,10 @@ export class AddProjectFormComponent implements OnInit {
4043
fetchRegions: FetchRegions,
4144
});
4245

43-
templates = input.required<IdName[]>();
44-
4546
ProjectFormControls = ProjectFormControls;
4647

4748
hasTemplateSelected = signal(false);
49+
selectedTemplate = signal<Project | null>(null);
4850
isSubmitting = signal(false);
4951
storageLocations = select(RegionsSelectors.getRegions);
5052
areStorageLocationsLoading = select(RegionsSelectors.areRegionsLoading);
@@ -65,6 +67,13 @@ export class AddProjectFormComponent implements OnInit {
6567
});
6668
}
6769

70+
onTemplateChange(project: Project | null): void {
71+
if (!project) return;
72+
this.selectedTemplate.set(project);
73+
this.projectForm().get(ProjectFormControls.Template)?.setValue(project.id);
74+
this.hasTemplateSelected.set(!!project);
75+
}
76+
6877
selectAllAffiliations(): void {
6978
const allAffiliationValues = this.affiliations().map((aff) => aff.id);
7079
this.projectForm().get(ProjectFormControls.Affiliations)?.setValue(allAffiliationValues);

src/app/shared/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export { MarkdownComponent } from './markdown/markdown.component';
2525
export { MyProjectsTableComponent } from './my-projects-table/my-projects-table.component';
2626
export { PasswordInputHintComponent } from './password-input-hint/password-input-hint.component';
2727
export { PieChartComponent } from './pie-chart/pie-chart.component';
28+
export { ProjectSelectorComponent } from './project-selector/project-selector.component';
2829
export { ReadonlyInputComponent } from './readonly-input/readonly-input.component';
2930
export { RegistrationBlocksDataComponent } from './registration-blocks-data/registration-blocks-data.component';
3031
export { ResourceCardComponent } from './resource-card/resource-card.component';

0 commit comments

Comments
 (0)