Skip to content

Commit ec4e6e0

Browse files
committed
feat(view-only-links): added components to the vol creation process
1 parent 6e66be3 commit ec4e6e0

30 files changed

Lines changed: 401 additions & 181 deletions

src/app/app.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { STATES } from '@core/constants';
1515
import { provideTranslation } from '@core/helpers';
1616

1717
import { GlobalErrorHandler } from './core/handlers';
18-
import { authInterceptor, errorInterceptor } from './core/interceptors';
18+
import { authInterceptor, errorInterceptor, viewOnlyInterceptor } from './core/interceptors';
1919
import CustomPreset from './core/theme/custom-preset';
2020
import { routes } from './app.routes';
2121

@@ -37,7 +37,7 @@ export const appConfig: ApplicationConfig = {
3737
},
3838
}),
3939
provideAnimations(),
40-
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
40+
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor, viewOnlyInterceptor])),
4141
importProvidersFrom(TranslateModule.forRoot(provideTranslation())),
4242
ConfirmationService,
4343
MessageService,

src/app/core/interceptors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './auth.interceptor';
22
export * from './error.interceptor';
3+
export * from './view-only.interceptor';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Observable } from 'rxjs';
2+
3+
import { HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
4+
import { inject } from '@angular/core';
5+
import { Router } from '@angular/router';
6+
7+
export const viewOnlyInterceptor: HttpInterceptorFn = (
8+
req: HttpRequest<unknown>,
9+
next: HttpHandlerFn
10+
): Observable<HttpEvent<unknown>> => {
11+
const router = inject(Router);
12+
13+
const currentUrl = router.url;
14+
const urlParams = new URLSearchParams(currentUrl.split('?')[1] || '');
15+
const viewOnlyParam = urlParams.get('view_only');
16+
17+
if (!req.url.includes('/api.crossref.org/funders') && viewOnlyParam) {
18+
const separator = req.url.includes('?') ? '&' : '?';
19+
const updatedUrl = `${req.url}${separator}view_only=${encodeURIComponent(viewOnlyParam)}`;
20+
21+
const viewOnlyReq = req.clone({
22+
url: updatedUrl,
23+
});
24+
25+
return next(viewOnlyReq);
26+
} else {
27+
return next(req);
28+
}
29+
};

src/app/features/project/analytics/components/view-duplicates/view-duplicates.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export class ViewDuplicatesComponent {
151151
viewOnlyLinksCount: 0,
152152
forksCount: resource.forksCount,
153153
resourceType: resourceType,
154+
isAnonymous: resource.isAnonymous,
154155
} as ToolbarResource;
155156
}
156157
return null;

src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,49 @@
2929
</span>
3030
</p>
3131

32-
<div class="flex flex-column gap-2">
33-
@for (item of config.data.sharedComponents; track item.id) {
34-
<div class="flex gap-1">
35-
<p-checkbox
36-
variant="filled"
37-
binary="true"
38-
[ngModel]="selectedComponents()[item.id]"
39-
(ngModelChange)="onCheckboxToggle(item.id, $event)"
40-
[disabled]="isCurrentProject(item)"
41-
>
42-
</p-checkbox>
43-
<p>{{ item.title }}</p>
44-
</div>
45-
}
46-
</div>
32+
@if (isLoading()) {
33+
<osf-loading-spinner />
34+
} @else {
35+
<div class="flex flex-column gap-2">
36+
@for (item of allComponents; track item.id) {
37+
<div class="flex gap-1">
38+
<p-checkbox
39+
variant="filled"
40+
binary="true"
41+
[class.pl-4]="!isCurrentProject(item)"
42+
[ngModel]="selectedComponents()[item.id]"
43+
(ngModelChange)="onCheckboxToggle(item.id, $event)"
44+
[disabled]="isCurrentProject(item)"
45+
>
46+
</p-checkbox>
47+
<p>
48+
{{ item.title }}
49+
@if (isCurrentProject(item)) {
50+
<span>
51+
{{ 'myProjects.settings.viewOnlyLinkCurrentProject' | translate }}
52+
</span>
53+
}
54+
</p>
55+
</div>
56+
}
57+
@if (allComponents.length > 1) {
58+
<div class="flex gap-2 justify-content-end">
59+
<p-button
60+
severity="secondary"
61+
size="small"
62+
[label]="'myProjects.createProject.affiliation.selectAll' | translate"
63+
(click)="selectAllComponents()"
64+
/>
65+
<p-button
66+
severity="secondary"
67+
size="small"
68+
[label]="'myProjects.createProject.affiliation.removeAll' | translate"
69+
(click)="deselectAllComponents()"
70+
/>
71+
</div>
72+
}
73+
</div>
74+
}
4775

4876
<div class="flex gap-2">
4977
<p-button
@@ -58,6 +86,7 @@
5886
class="w-full"
5987
styleClass="w-full"
6088
(click)="addLink()"
89+
[disabled]="!isFormValid"
6190
[label]="'project.contributors.addDialog.next' | translate"
6291
></p-button>
6392
</div>
Lines changed: 117 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
1+
import { createDispatchMap, select } from '@ngxs/store';
2+
13
import { TranslatePipe } from '@ngx-translate/core';
24

35
import { Button } from 'primeng/button';
46
import { Checkbox } from 'primeng/checkbox';
57
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
68

7-
import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';
9+
import { ChangeDetectionStrategy, Component, effect, inject, OnInit, signal } from '@angular/core';
810
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
911

10-
import { TextInputComponent } from '@osf/shared/components';
12+
import { GetComponents, ProjectOverviewSelectors } from '@osf/features/project/overview/store';
13+
import { LoadingSpinnerComponent, TextInputComponent } from '@osf/shared/components';
1114
import { InputLimits } from '@osf/shared/constants';
1215
import { CustomValidators } from '@osf/shared/helpers';
13-
import { ViewOnlyLinkNodeModel } from '@osf/shared/models';
16+
import { ViewOnlyLinkComponent } from '@shared/models';
1417

1518
@Component({
1619
selector: 'osf-create-view-link-dialog',
17-
imports: [Button, TranslatePipe, ReactiveFormsModule, FormsModule, Checkbox, TextInputComponent],
20+
imports: [
21+
Button,
22+
TranslatePipe,
23+
ReactiveFormsModule,
24+
FormsModule,
25+
Checkbox,
26+
TextInputComponent,
27+
LoadingSpinnerComponent,
28+
],
1829
templateUrl: './create-view-link-dialog.component.html',
1930
styleUrl: './create-view-link-dialog.component.scss',
2031
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -28,54 +39,129 @@ export class CreateViewLinkDialogComponent implements OnInit {
2839

2940
anonymous = signal(true);
3041
protected selectedComponents = signal<Record<string, boolean>>({});
31-
readonly projectId = signal('');
42+
protected components = select(ProjectOverviewSelectors.getComponents);
43+
protected isLoading = select(ProjectOverviewSelectors.getComponentsLoading);
44+
45+
protected actions = createDispatchMap({
46+
getComponents: GetComponents,
47+
});
48+
49+
get currentProjectId(): string {
50+
return this.config.data?.['projectId'] || '';
51+
}
52+
53+
get allComponents(): ViewOnlyLinkComponent[] {
54+
const currentProjectData = this.config.data?.['currentProject'];
55+
const components = this.components();
56+
57+
const result: ViewOnlyLinkComponent[] = [];
58+
59+
if (currentProjectData) {
60+
result.push({
61+
id: currentProjectData.id,
62+
title: currentProjectData.title,
63+
isCurrentProject: true,
64+
});
65+
}
66+
67+
components.forEach((comp) => {
68+
result.push({
69+
id: comp.id,
70+
title: comp.title,
71+
isCurrentProject: false,
72+
});
73+
});
74+
75+
return result;
76+
}
77+
78+
constructor() {
79+
effect(() => {
80+
const components = this.allComponents;
81+
if (components.length) {
82+
this.initializeSelection();
83+
}
84+
});
85+
}
3286

3387
ngOnInit(): void {
34-
const data = (this.config.data?.['sharedComponents'] as ViewOnlyLinkNodeModel[]) || [];
35-
this.projectId.set(this.config.data?.projectId);
36-
const initialState = data.reduce(
37-
(acc, curr) => {
38-
if (curr.id) {
39-
acc[curr.id] = true;
40-
}
41-
return acc;
42-
},
43-
{} as Record<string, boolean>
44-
);
88+
const projectId = this.currentProjectId;
89+
90+
if (projectId) {
91+
this.actions.getComponents(projectId);
92+
} else {
93+
this.initializeSelection();
94+
}
95+
}
96+
97+
private initializeSelection(): void {
98+
const initialState: Record<string, boolean> = {};
99+
100+
this.allComponents.forEach((component) => {
101+
initialState[component.id] = component.isCurrentProject;
102+
});
103+
45104
this.selectedComponents.set(initialState);
46105
}
47106

48-
isCurrentProject(item: ViewOnlyLinkNodeModel): boolean {
49-
return item.category === 'project' && item.id === this.projectId();
107+
isCurrentProject(item: ViewOnlyLinkComponent): boolean {
108+
return item.isCurrentProject;
109+
}
110+
111+
get isFormValid(): boolean {
112+
return this.linkName.valid && !!this.linkName.value.trim().length;
50113
}
51114

52115
addLink(): void {
53-
if (!this.linkName.value) return;
116+
if (!this.isFormValid) return;
54117

55-
const components = (this.config.data?.['sharedComponents'] as ViewOnlyLinkNodeModel[]) || [];
56-
const selectedIds = Object.entries(this.selectedComponents()).filter(([component, checked]) => checked);
118+
const selectedIds = Object.entries(this.selectedComponents())
119+
.filter(([, checked]) => checked)
120+
.map(([id]) => id);
57121

58-
const selected = components
59-
.filter((comp: ViewOnlyLinkNodeModel) =>
60-
selectedIds.find(([id, checked]: [string, boolean]) => id === comp.id && checked)
61-
)
62-
.map((comp) => ({
63-
id: comp.id,
64-
type: 'nodes',
65-
}));
122+
const rootProjectId = this.currentProjectId;
123+
const rootProject = selectedIds.includes(rootProjectId) ? [{ id: rootProjectId, type: 'nodes' }] : [];
124+
125+
const relationshipComponents = selectedIds
126+
.filter((id) => id !== rootProjectId)
127+
.map((id) => ({ id, type: 'nodes' }));
66128

67-
const data = {
129+
const data: Record<string, unknown> = {
68130
attributes: {
69131
name: this.linkName.value,
70132
anonymous: this.anonymous(),
71133
},
72-
nodes: selected,
134+
nodes: rootProject,
73135
};
74136

137+
if (relationshipComponents.length) {
138+
data['relationships'] = {
139+
nodes: {
140+
data: relationshipComponents,
141+
},
142+
};
143+
}
144+
75145
this.dialogRef.close(data);
76146
}
77147

78148
onCheckboxToggle(id: string, checked: boolean): void {
79149
this.selectedComponents.update((prev) => ({ ...prev, [id]: checked }));
80150
}
151+
152+
selectAllComponents(): void {
153+
const allIds: Record<string, boolean> = {};
154+
this.allComponents.forEach((component) => {
155+
allIds[component.id] = true;
156+
});
157+
this.selectedComponents.set(allIds);
158+
}
159+
160+
deselectAllComponents(): void {
161+
const allIds: Record<string, boolean> = {};
162+
this.allComponents.forEach((component) => {
163+
allIds[component.id] = component.isCurrentProject;
164+
});
165+
this.selectedComponents.set(allIds);
166+
}
81167
}

src/app/features/project/contributors/contributors.component.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,12 @@ <h2>{{ 'project.contributors.viewOnly' | translate }}</h2>
8989

9090
<p>{{ 'project.contributors.createLink' | translate }}</p>
9191

92-
<p-button class="w-10rem" [label]="'project.contributors.createButton' | translate" (click)="createViewLink()">
92+
<p-button
93+
class="w-10rem"
94+
[label]="'project.contributors.createButton' | translate"
95+
[disabled]="!canCreateViewLink()"
96+
(click)="createViewLink()"
97+
>
9398
</p-button>
9499

95100
<osf-view-only-table

0 commit comments

Comments
 (0)