Skip to content

Commit 3fdcc68

Browse files
authored
[ENG-9275] P55 - NIR: No way to reorder components (#753)
- Ticket: [ENG-9275] - Feature flag: n/a ## Summary of Changes 1. Added components reorder. 2. Replaced models and mappers. 3. Added unit tests.
1 parent adbfcf2 commit 3fdcc68

File tree

39 files changed

+834
-319
lines changed

39 files changed

+834
-319
lines changed

src/app/features/analytics/components/view-duplicates/view-duplicates.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ <h2 class="flex align-items-center gap-2">
6262
<div class="flex align-items-start gap-1">
6363
<span class="font-bold">{{ 'common.labels.contributors' | translate }}:</span>
6464

65-
<osf-contributors-list [contributors]="duplicate.bibliographicContributors ?? []"></osf-contributors-list>
65+
<osf-contributors-list [contributors]="duplicate.bibliographicContributors"></osf-contributors-list>
6666
</div>
6767

6868
@if (duplicate.description) {

src/app/features/analytics/components/view-linked-projects/view-linked-projects.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ <h2 class="flex align-items-center gap-2">
3030
<div class="flex align-items-start gap-1">
3131
<span class="font-bold">{{ 'common.labels.contributors' | translate }}:</span>
3232

33-
<osf-contributors-list [contributors]="duplicate.bibliographicContributors ?? []"></osf-contributors-list>
33+
<osf-contributors-list [contributors]="duplicate.bibliographicContributors"></osf-contributors-list>
3434
</div>
3535

3636
@if (duplicate.description) {

src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
[showClear]="true"
2121
[loading]="fundersLoading()"
2222
[emptyFilterMessage]="filterMessage() | translate"
23-
[emptyMessage]="filterMessage()"
23+
[emptyMessage]="filterMessage() | translate"
2424
[autoOptionFocus]="false"
2525
(onChange)="onFunderSelected($event.value, $index)"
2626
(onFilter)="onFunderSearch($event.filter)"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<div class="node-wrapper flex flex-column p-3 gap-3">
2+
<div class="flex justify-content-between align-items-center">
3+
<h2 class="flex align-items-center gap-2">
4+
<osf-icon [iconClass]="component().isPublic ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
5+
6+
<a class="node-title cursor-pointer" (click)="handleNavigate(component().id)">
7+
{{ component().title }}
8+
</a>
9+
</h2>
10+
11+
@if (component().currentUserIsContributor) {
12+
<div>
13+
<p-button
14+
severity="contrast"
15+
icon="fas fa-ellipsis-vertical"
16+
raised
17+
variant="outlined"
18+
[ariaLabel]="'common.buttons.more' | translate"
19+
(onClick)="menu.toggle($event)"
20+
>
21+
</p-button>
22+
23+
<p-menu appendTo="body" [model]="componentActionItems()" popup #menu>
24+
<ng-template #item let-item>
25+
<a
26+
class="p-menu-item-link"
27+
(mousedown)="handleMenuAction(item.action)"
28+
(keydown.enter)="handleMenuAction(item.action)"
29+
>
30+
{{ item.label | translate }}
31+
</a>
32+
</ng-template>
33+
</p-menu>
34+
</div>
35+
}
36+
</div>
37+
38+
<div class="flex flex-wrap gap-1">
39+
<p class="font-bold">{{ 'common.labels.contributors' | translate }}:</p>
40+
41+
<osf-contributors-list
42+
[contributors]="component().bibliographicContributors"
43+
[anonymous]="anonymous()"
44+
></osf-contributors-list>
45+
</div>
46+
47+
@if (component().description) {
48+
<osf-truncated-text [text]="('resourceCard.labels.descriptionBold' | translate) + component().description" />
49+
}
50+
</div>

src/app/features/project/overview/components/component-card/component-card.component.scss

Whitespace-only changes.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { MockComponents } from 'ng-mocks';
2+
3+
import { ComponentFixture, TestBed } from '@angular/core/testing';
4+
5+
import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component';
6+
import { IconComponent } from '@osf/shared/components/icon/icon.component';
7+
8+
import { ComponentCardComponent } from './component-card.component';
9+
10+
import { MOCK_NODE_WITH_ADMIN, MOCK_NODE_WITHOUT_ADMIN } from '@testing/mocks/node.mock';
11+
12+
describe('ComponentCardComponent', () => {
13+
let component: ComponentCardComponent;
14+
let fixture: ComponentFixture<ComponentCardComponent>;
15+
16+
beforeEach(async () => {
17+
await TestBed.configureTestingModule({
18+
imports: [ComponentCardComponent, ...MockComponents(IconComponent, ContributorsListComponent)],
19+
}).compileComponents();
20+
21+
fixture = TestBed.createComponent(ComponentCardComponent);
22+
component = fixture.componentInstance;
23+
fixture.componentRef.setInput('component', MOCK_NODE_WITH_ADMIN);
24+
fixture.componentRef.setInput('anonymous', false);
25+
fixture.detectChanges();
26+
});
27+
28+
it('should emit navigate when handleNavigate is called', () => {
29+
const emitSpy = jest.spyOn(component.navigate, 'emit');
30+
component.handleNavigate('test-id');
31+
expect(emitSpy).toHaveBeenCalledWith('test-id');
32+
});
33+
34+
it('should emit menuAction when handleMenuAction is called', () => {
35+
const emitSpy = jest.spyOn(component.menuAction, 'emit');
36+
component.handleMenuAction('settings');
37+
expect(emitSpy).toHaveBeenCalledWith('settings');
38+
});
39+
40+
describe('componentActionItems', () => {
41+
it('should return base items for any component', () => {
42+
fixture.componentRef.setInput('component', MOCK_NODE_WITHOUT_ADMIN);
43+
fixture.detectChanges();
44+
const items = component.componentActionItems();
45+
expect(items).toHaveLength(2);
46+
expect(items[0].action).toBe('manageContributors');
47+
expect(items[1].action).toBe('settings');
48+
});
49+
50+
it('should include delete action when component has Admin permission', () => {
51+
fixture.componentRef.setInput('component', MOCK_NODE_WITH_ADMIN);
52+
fixture.detectChanges();
53+
const items = component.componentActionItems();
54+
expect(items).toHaveLength(3);
55+
expect(items[0].action).toBe('manageContributors');
56+
expect(items[1].action).toBe('settings');
57+
expect(items[2].action).toBe('delete');
58+
});
59+
60+
it('should exclude delete action when component does not have Admin permission', () => {
61+
fixture.componentRef.setInput('component', MOCK_NODE_WITHOUT_ADMIN);
62+
fixture.detectChanges();
63+
const items = component.componentActionItems();
64+
expect(items).toHaveLength(2);
65+
expect(items.every((item) => item.action !== 'delete')).toBe(true);
66+
});
67+
68+
it('should exclude delete action when hideDeleteAction is true', () => {
69+
fixture.componentRef.setInput('component', MOCK_NODE_WITH_ADMIN);
70+
fixture.componentRef.setInput('hideDeleteAction', true);
71+
fixture.detectChanges();
72+
const items = component.componentActionItems();
73+
expect(items).toHaveLength(2);
74+
expect(items.every((item) => item.action !== 'delete')).toBe(true);
75+
});
76+
});
77+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { TranslatePipe } from '@ngx-translate/core';
2+
3+
import { Button } from 'primeng/button';
4+
import { Menu } from 'primeng/menu';
5+
6+
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
7+
8+
import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component';
9+
import { IconComponent } from '@osf/shared/components/icon/icon.component';
10+
import { TruncatedTextComponent } from '@osf/shared/components/truncated-text/truncated-text.component';
11+
import { UserPermissions } from '@osf/shared/enums/user-permissions.enum';
12+
import { NodeModel } from '@osf/shared/models/nodes/base-node.model';
13+
14+
@Component({
15+
selector: 'osf-component-card',
16+
imports: [Button, Menu, TranslatePipe, TruncatedTextComponent, IconComponent, ContributorsListComponent],
17+
templateUrl: './component-card.component.html',
18+
styleUrl: './component-card.component.scss',
19+
changeDetection: ChangeDetectionStrategy.OnPush,
20+
})
21+
export class ComponentCardComponent {
22+
component = input.required<NodeModel>();
23+
anonymous = input.required<boolean>();
24+
hideDeleteAction = input<boolean>(false);
25+
26+
navigate = output<string>();
27+
menuAction = output<string>();
28+
29+
readonly componentActionItems = computed(() => {
30+
const component = this.component();
31+
32+
const baseItems = [
33+
{
34+
label: 'project.overview.actions.manageContributors',
35+
action: 'manageContributors',
36+
},
37+
{
38+
label: 'project.overview.actions.settings',
39+
action: 'settings',
40+
},
41+
];
42+
43+
if (!this.hideDeleteAction() && component.currentUserPermissions.includes(UserPermissions.Admin)) {
44+
baseItems.push({
45+
label: 'project.overview.actions.delete',
46+
action: 'delete',
47+
});
48+
}
49+
50+
return baseItems;
51+
});
52+
53+
handleNavigate(componentId: string): void {
54+
this.navigate.emit(componentId);
55+
}
56+
57+
handleMenuAction(action: string): void {
58+
this.menuAction.emit(action);
59+
}
60+
}

src/app/features/project/overview/components/linked-resources/linked-resources.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ <h2>{{ 'project.overview.linkedProjects.title' | translate }}</h2>
1818
<div class="linked-project-wrapper flex flex-column p-3 gap-3">
1919
<div class="flex justify-content-between align-items-center">
2020
<h2 class="flex align-items-center gap-2">
21-
<osf-icon [iconClass]="linkedResource.public ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
21+
<osf-icon [iconClass]="linkedResource.isPublic ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
2222
<a class="linked-project-title" [href]="linkedResource.id">{{ linkedResource.title }}</a>
2323
</h2>
2424
@if (canEdit()) {
@@ -39,7 +39,7 @@ <h2 class="flex align-items-center gap-2">
3939
<div class="flex flex-wrap gap-1">
4040
<p class="font-bold">{{ 'common.labels.contributors' | translate }}:</p>
4141

42-
<osf-contributors-list [contributors]="linkedResource.contributors"></osf-contributors-list>
42+
<osf-contributors-list [contributors]="linkedResource.bibliographicContributors"></osf-contributors-list>
4343
</div>
4444
@if (linkedResource.description) {
4545
<osf-truncated-text
Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,83 @@
1-
import { MockComponents } from 'ng-mocks';
1+
import { MockComponents, MockProvider } from 'ng-mocks';
22

33
import { ComponentFixture, TestBed } from '@angular/core/testing';
44

55
import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component';
66
import { IconComponent } from '@osf/shared/components/icon/icon.component';
7-
import { TruncatedTextComponent } from '@osf/shared/components/truncated-text/truncated-text.component';
7+
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
8+
import { NodeLinksSelectors } from '@osf/shared/stores/node-links';
9+
10+
import { DeleteNodeLinkDialogComponent } from '../delete-node-link-dialog/delete-node-link-dialog.component';
11+
import { LinkResourceDialogComponent } from '../link-resource-dialog/link-resource-dialog.component';
812

913
import { LinkedResourcesComponent } from './linked-resources.component';
1014

11-
describe.skip('LinkedProjectsComponent', () => {
15+
import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock';
16+
import { OSFTestingModule } from '@testing/osf.testing.module';
17+
import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock';
18+
import { provideMockStore } from '@testing/providers/store-provider.mock';
19+
20+
describe('LinkedProjectsComponent', () => {
1221
let component: LinkedResourcesComponent;
1322
let fixture: ComponentFixture<LinkedResourcesComponent>;
23+
let customDialogServiceMock: ReturnType<CustomDialogServiceMockBuilder['build']>;
24+
25+
const mockLinkedResources = [
26+
{ ...MOCK_NODE_WITH_ADMIN, id: 'resource-1', title: 'Linked Resource 1' },
27+
{ ...MOCK_NODE_WITH_ADMIN, id: 'resource-2', title: 'Linked Resource 2' },
28+
{ ...MOCK_NODE_WITH_ADMIN, id: 'resource-3', title: 'Linked Resource 3' },
29+
];
1430

1531
beforeEach(async () => {
32+
customDialogServiceMock = CustomDialogServiceMockBuilder.create().withDefaultOpen().build();
33+
1634
await TestBed.configureTestingModule({
1735
imports: [
1836
LinkedResourcesComponent,
19-
...MockComponents(TruncatedTextComponent, IconComponent, ContributorsListComponent),
37+
OSFTestingModule,
38+
...MockComponents(IconComponent, ContributorsListComponent),
39+
],
40+
providers: [
41+
provideMockStore({
42+
signals: [
43+
{ selector: NodeLinksSelectors.getLinkedResources, value: mockLinkedResources },
44+
{ selector: NodeLinksSelectors.getLinkedResourcesLoading, value: false },
45+
],
46+
}),
47+
MockProvider(CustomDialogService, customDialogServiceMock),
2048
],
2149
}).compileComponents();
2250

2351
fixture = TestBed.createComponent(LinkedResourcesComponent);
2452
component = fixture.componentInstance;
53+
fixture.componentRef.setInput('canEdit', true);
2554
fixture.detectChanges();
2655
});
2756

28-
it('should create', () => {
29-
expect(component).toBeTruthy();
57+
it('should open LinkResourceDialogComponent with correct config', () => {
58+
component.openLinkProjectModal();
59+
60+
expect(customDialogServiceMock.open).toHaveBeenCalledWith(LinkResourceDialogComponent, {
61+
header: 'project.overview.dialog.linkProject.header',
62+
width: '850px',
63+
});
64+
});
65+
66+
it('should find resource by id and open DeleteNodeLinkDialogComponent with correct config when resource exists', () => {
67+
component.openDeleteResourceModal('resource-2');
68+
69+
expect(customDialogServiceMock.open).toHaveBeenCalledWith(DeleteNodeLinkDialogComponent, {
70+
header: 'project.overview.dialog.deleteNodeLink.header',
71+
width: '650px',
72+
data: { currentLink: mockLinkedResources[1] },
73+
});
74+
});
75+
76+
it('should return early and not open dialog when resource is not found', () => {
77+
customDialogServiceMock.open.mockClear();
78+
79+
component.openDeleteResourceModal('non-existent-id');
80+
81+
expect(customDialogServiceMock.open).not.toHaveBeenCalled();
3082
});
3183
});

0 commit comments

Comments
 (0)