Skip to content

Commit 147aeda

Browse files
nsemetsNazar690
andauthored
Fix/improvements (#319)
* fix(meetings): fixed meetings small issues * fix(tooltips): added tooltips * fix(table): updated sorting * fix(settings): fixed update project * fix(bookmarks): updated bookmarks * fix(my-registrations): fixed my registrations * fix(developer-apps): fixed developer apps * fix(settings): updated tokens and notifications * fix(translation): removed dot * fix(info-icon): updated info icon translate * fix(profile-settings): fixed profile settings * fix(test): updated tests * fix(settings): updated settings * fix(user-emails): updated adding emails to user account * fix(tests): updated tests * fix(clean-up): clean up * fix(models): updated models * fix(models): updated region and license models * fix(styles): moved styles from assets * fix(test): fixed institution loading and test for view only link * fix(analytics): added check if is public * fix(analytics): updated analytics feature * fix(analytics): show message when data loaded * fix(view-only-links): updated view only links * fix(view only links): added shared components * fix(tests): fixed tests * fix(unit-tests): updated jest config * fix(view-only-links): update view only links for components * fix(create-view-link): added logic for uncheck --------- Co-authored-by: Nazar Semets <nazar690@gmail.com>
1 parent cf5041d commit 147aeda

21 files changed

+206
-141
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<div class="flex gap-1" [class.disabled]="item().disabled">
2+
<p-checkbox
3+
variant="filled"
4+
binary="true"
5+
[(ngModel)]="item().checked"
6+
[disabled]="item().disabled"
7+
(onChange)="onCheckboxChange()"
8+
></p-checkbox>
9+
10+
<p class="item-title">
11+
{{ item().title }}
12+
@if (item().isCurrentResource) {
13+
<span>
14+
{{ 'myProjects.settings.viewOnlyLinkCurrentProject' | translate }}
15+
</span>
16+
}
17+
</p>
18+
19+
@if (item().disabled && !item().isCurrentResource) {
20+
<osf-info-icon [tooltipText]="'myProjects.settings.parentsNeedToBeChecked' | translate"></osf-info-icon>
21+
}
22+
</div>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.disabled .item-title {
2+
opacity: 0.5;
3+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { ComponentCheckboxItemComponent } from './component-checkbox-item.component';
4+
5+
describe.skip('ComponentCheckboxItemComponent', () => {
6+
let component: ComponentCheckboxItemComponent;
7+
let fixture: ComponentFixture<ComponentCheckboxItemComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [ComponentCheckboxItemComponent],
12+
}).compileComponents();
13+
14+
fixture = TestBed.createComponent(ComponentCheckboxItemComponent);
15+
component = fixture.componentInstance;
16+
});
17+
18+
it('should create', () => {
19+
expect(component).toBeTruthy();
20+
});
21+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { TranslatePipe } from '@ngx-translate/core';
2+
3+
import { Checkbox } from 'primeng/checkbox';
4+
5+
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
6+
import { FormsModule } from '@angular/forms';
7+
8+
import { InfoIconComponent } from '@osf/shared/components';
9+
10+
import { ViewOnlyLinkComponentItem } from '../../models';
11+
12+
@Component({
13+
selector: 'osf-component-checkbox-item',
14+
imports: [Checkbox, FormsModule, InfoIconComponent, TranslatePipe],
15+
templateUrl: './component-checkbox-item.component.html',
16+
styleUrl: './component-checkbox-item.component.scss',
17+
changeDetection: ChangeDetectionStrategy.OnPush,
18+
})
19+
export class ComponentCheckboxItemComponent {
20+
item = input.required<ViewOnlyLinkComponentItem>();
21+
checkboxChange = output<void>();
22+
23+
onCheckboxChange(): void {
24+
this.checkboxChange.emit();
25+
}
26+
}

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

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -33,42 +33,8 @@
3333
<osf-loading-spinner />
3434
} @else {
3535
<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]="!item.isCurrentResource"
42-
[ngModel]="selectedComponents()[item.id]"
43-
(ngModelChange)="onCheckboxToggle(item.id, $event)"
44-
[disabled]="item.isCurrentResource"
45-
>
46-
</p-checkbox>
47-
<p>
48-
{{ item.title }}
49-
@if (item.isCurrentResource) {
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>
36+
@for (item of componentsList(); track item.id) {
37+
<osf-component-checkbox-item [item]="item" (checkboxChange)="onCheckboxChange(item)" />
7238
}
7339
</div>
7440
}

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

Lines changed: 88 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import { Button } from 'primeng/button';
66
import { Checkbox } from 'primeng/checkbox';
77
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
88

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

1212
import { LoadingSpinnerComponent, TextInputComponent } from '@osf/shared/components';
1313
import { InputLimits } from '@osf/shared/constants';
1414
import { CustomValidators } from '@osf/shared/helpers';
15-
import { CurrentResourceSelectors, GetResourceChildren } from '@osf/shared/stores';
16-
import { ViewOnlyLinkChildren } from '@shared/models';
15+
import { CurrentResourceSelectors, GetResourceWithChildren } from '@osf/shared/stores';
1716

18-
import { ResourceInfoModel } from '../../models';
17+
import { ResourceInfoModel, ViewOnlyLinkComponentItem } from '../../models';
18+
import { ComponentCheckboxItemComponent } from '../component-checkbox-item/component-checkbox-item.component';
1919

2020
@Component({
2121
selector: 'osf-create-view-link-dialog',
@@ -27,6 +27,7 @@ import { ResourceInfoModel } from '../../models';
2727
Checkbox,
2828
TextInputComponent,
2929
LoadingSpinnerComponent,
30+
ComponentCheckboxItemComponent,
3031
],
3132
templateUrl: './create-view-link-dialog.component.html',
3233
styleUrl: './create-view-link-dialog.component.scss',
@@ -38,122 +39,126 @@ export class CreateViewLinkDialogComponent implements OnInit {
3839
readonly inputLimits = InputLimits;
3940

4041
linkName = new FormControl('', { nonNullable: true, validators: [CustomValidators.requiredTrimmed()] });
41-
4242
anonymous = signal(true);
43-
selectedComponents = signal<Record<string, boolean>>({});
44-
components = select(CurrentResourceSelectors.getResourceChildren);
45-
isLoading = select(CurrentResourceSelectors.isResourceChildrenLoading);
46-
47-
actions = createDispatchMap({ getComponents: GetResourceChildren });
48-
49-
get currentResource() {
50-
return this.config.data as ResourceInfoModel;
51-
}
52-
53-
get allComponents(): ViewOnlyLinkChildren[] {
54-
const currentResourceData = this.currentResource;
55-
const components = this.components();
5643

57-
const result: ViewOnlyLinkChildren[] = [];
44+
readonly components = select(CurrentResourceSelectors.getResourceWithChildren);
45+
readonly isLoading = select(CurrentResourceSelectors.isResourceWithChildrenLoading);
46+
readonly actions = createDispatchMap({ getComponents: GetResourceWithChildren });
5847

59-
if (currentResourceData) {
60-
result.push({
61-
id: currentResourceData.id,
62-
title: currentResourceData.title,
63-
isCurrentResource: true,
64-
});
65-
}
66-
67-
components.forEach((comp) => {
68-
result.push({
69-
id: comp.id,
70-
title: comp.title,
71-
isCurrentResource: false,
72-
});
73-
});
74-
75-
return result;
76-
}
48+
componentsList: WritableSignal<ViewOnlyLinkComponentItem[]> = signal([]);
7749

7850
constructor() {
7951
effect(() => {
80-
const components = this.allComponents;
81-
if (components.length) {
82-
this.initializeSelection();
83-
}
52+
const currentResource = this.config.data as ResourceInfoModel;
53+
const components = this.components();
54+
55+
const items: ViewOnlyLinkComponentItem[] = components.map((item) => ({
56+
id: item.id,
57+
title: item.title,
58+
isCurrentResource: currentResource.id === item.id,
59+
parentId: item.parentId,
60+
checked: currentResource.id === item.id,
61+
disabled: currentResource.id === item.id,
62+
}));
63+
64+
const updatedItems = items.map((item) => ({
65+
...item,
66+
disabled: item.isCurrentResource ? item.disabled : !this.isParentChecked(item, items),
67+
}));
68+
69+
this.componentsList.set(updatedItems);
8470
});
8571
}
8672

8773
ngOnInit(): void {
88-
const projectId = this.currentResource.id;
74+
const currentResource = this.config.data as ResourceInfoModel;
75+
const { id, type } = currentResource;
8976

90-
if (projectId) {
91-
this.actions.getComponents(projectId, this.currentResource.type);
92-
} else {
93-
this.initializeSelection();
77+
if (id) {
78+
this.actions.getComponents(id, type);
9479
}
9580
}
9681

97-
private initializeSelection(): void {
98-
const initialState: Record<string, boolean> = {};
82+
onCheckboxChange(changedItem: ViewOnlyLinkComponentItem): void {
83+
this.componentsList.update((items) => {
84+
let updatedItems = [...items];
9985

100-
this.allComponents.forEach((component) => {
101-
initialState[component.id] = component.isCurrentResource;
102-
});
86+
if (!changedItem.checked) {
87+
updatedItems = this.uncheckChildren(changedItem.id, updatedItems);
88+
}
10389

104-
this.selectedComponents.set(initialState);
90+
return updatedItems.map((item) => ({
91+
...item,
92+
disabled: item.isCurrentResource ? item.disabled : !this.isParentChecked(item, updatedItems),
93+
}));
94+
});
10595
}
10696

10797
addLink(): void {
10898
if (this.linkName.invalid) return;
10999

110-
const selectedIds = Object.entries(this.selectedComponents())
111-
.filter(([, checked]) => checked)
112-
.map(([id]) => id);
100+
const currentResource = this.config.data as ResourceInfoModel;
101+
const selectedIds = this.componentsList()
102+
.filter((x) => x.checked)
103+
.map((x) => x.id);
113104

114-
const rootProjectId = this.currentResource.id;
115-
const rootProject = selectedIds.includes(rootProjectId) ? [{ id: rootProjectId, type: 'nodes' }] : [];
105+
const data = this.buildLinkData(selectedIds, currentResource.id, this.linkName.value, this.anonymous());
106+
107+
this.dialogRef.close(data);
108+
}
109+
110+
private isParentChecked(item: ViewOnlyLinkComponentItem, items: ViewOnlyLinkComponentItem[]): boolean {
111+
if (!item.parentId) {
112+
return true;
113+
}
114+
115+
const parent = items.find((x) => x.id === item.parentId);
116116

117+
return parent?.checked ?? true;
118+
}
119+
120+
private uncheckChildren(parentId: string, items: ViewOnlyLinkComponentItem[]): ViewOnlyLinkComponentItem[] {
121+
let updatedItems = items.map((item) => {
122+
if (item.parentId === parentId) {
123+
return { ...item, checked: false };
124+
}
125+
return item;
126+
});
127+
128+
const directChildren = updatedItems.filter((item) => item.parentId === parentId);
129+
130+
for (const child of directChildren) {
131+
updatedItems = this.uncheckChildren(child.id, updatedItems);
132+
}
133+
134+
return updatedItems;
135+
}
136+
137+
private buildLinkData(
138+
selectedIds: string[],
139+
rootProjectId: string,
140+
linkName: string,
141+
isAnonymous: boolean
142+
): Record<string, unknown> {
143+
const rootProject = selectedIds.includes(rootProjectId) ? [{ id: rootProjectId, type: 'nodes' }] : [];
117144
const relationshipComponents = selectedIds
118145
.filter((id) => id !== rootProjectId)
119146
.map((id) => ({ id, type: 'nodes' }));
120147

121148
const data: Record<string, unknown> = {
122149
attributes: {
123-
name: this.linkName.value,
124-
anonymous: this.anonymous(),
150+
name: linkName,
151+
anonymous: isAnonymous,
125152
},
126153
nodes: rootProject,
127154
};
128155

129156
if (relationshipComponents.length) {
130157
data['relationships'] = {
131-
nodes: {
132-
data: relationshipComponents,
133-
},
158+
nodes: { data: relationshipComponents },
134159
};
135160
}
136161

137-
this.dialogRef.close(data);
138-
}
139-
140-
onCheckboxToggle(id: string, checked: boolean): void {
141-
this.selectedComponents.update((prev) => ({ ...prev, [id]: checked }));
142-
}
143-
144-
selectAllComponents(): void {
145-
const allIds: Record<string, boolean> = {};
146-
this.allComponents.forEach((component) => {
147-
allIds[component.id] = true;
148-
});
149-
this.selectedComponents.set(allIds);
150-
}
151-
152-
deselectAllComponents(): void {
153-
const allIds: Record<string, boolean> = {};
154-
this.allComponents.forEach((component) => {
155-
allIds[component.id] = component.isCurrentResource;
156-
});
157-
this.selectedComponents.set(allIds);
162+
return data;
158163
}
159164
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export { ComponentCheckboxItemComponent } from './component-checkbox-item/component-checkbox-item.component';
12
export { CreateViewLinkDialogComponent } from './create-view-link-dialog/create-view-link-dialog.component';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './resource-info.model';
2+
export * from './view-only-components.models';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface ViewOnlyLinkComponentItem {
2+
id: string;
3+
title: string;
4+
isCurrentResource?: boolean;
5+
disabled: boolean;
6+
checked: boolean;
7+
parentId?: string | null;
8+
}

src/app/shared/mappers/nodes/base-node.mapper.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import { BaseNodeDataJsonApi, BaseNodeModel } from '@osf/shared/models';
1+
import { BaseNodeDataJsonApi, BaseNodeModel, NodeShortInfoModel } from '@osf/shared/models';
22

33
export class BaseNodeMapper {
44
static getNodesData(data: BaseNodeDataJsonApi[]): BaseNodeModel[] {
55
return data.map((item) => this.getNodeData(item));
66
}
77

8+
static getNodesWithChildren(data: BaseNodeDataJsonApi[]): NodeShortInfoModel[] {
9+
return data.map((item) => ({
10+
id: item.id,
11+
title: item.attributes.title,
12+
parentId: item.relationships.parent?.data?.id,
13+
}));
14+
}
15+
816
static getNodeData(data: BaseNodeDataJsonApi): BaseNodeModel {
917
return {
1018
id: data.id,

0 commit comments

Comments
 (0)