Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ <h2 class="flex align-items-center gap-2">
<div class="flex align-items-start gap-1">
<span class="font-bold">{{ 'common.labels.contributors' | translate }}:</span>

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

@if (duplicate.description) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ <h2 class="flex align-items-center gap-2">
<div class="flex align-items-start gap-1">
<span class="font-bold">{{ 'common.labels.contributors' | translate }}:</span>

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

@if (duplicate.description) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
[showClear]="true"
[loading]="fundersLoading()"
[emptyFilterMessage]="filterMessage() | translate"
[emptyMessage]="filterMessage()"
[emptyMessage]="filterMessage() | translate"
[autoOptionFocus]="false"
(onChange)="onFunderSelected($event.value, $index)"
(onFilter)="onFunderSearch($event.filter)"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<div class="node-wrapper flex flex-column p-3 gap-3">
<div class="flex justify-content-between align-items-center">
<h2 class="flex align-items-center gap-2">
<osf-icon [iconClass]="component().isPublic ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>

<a class="node-title cursor-pointer" (click)="handleNavigate(component().id)">
{{ component().title }}
</a>
</h2>

@if (component().currentUserIsContributor) {
<div>
<p-button
severity="contrast"
icon="fas fa-ellipsis-vertical"
raised
variant="outlined"
[ariaLabel]="'common.buttons.more' | translate"
(onClick)="menu.toggle($event)"
>
</p-button>

<p-menu appendTo="body" [model]="componentActionItems()" popup #menu>
<ng-template #item let-item>
<a
class="p-menu-item-link"
(mousedown)="handleMenuAction(item.action)"
(keydown.enter)="handleMenuAction(item.action)"
>
{{ item.label | translate }}
</a>
</ng-template>
</p-menu>
</div>
}
</div>

<div class="flex flex-wrap gap-1">
<p class="font-bold">{{ 'common.labels.contributors' | translate }}:</p>

<osf-contributors-list
[contributors]="component().bibliographicContributors"
[anonymous]="anonymous()"
></osf-contributors-list>
</div>

@if (component().description) {
<osf-truncated-text [text]="('resourceCard.labels.descriptionBold' | translate) + component().description" />
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { MockComponents } from 'ng-mocks';

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

import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component';
import { IconComponent } from '@osf/shared/components/icon/icon.component';

import { ComponentCardComponent } from './component-card.component';

import { MOCK_NODE_WITH_ADMIN, MOCK_NODE_WITHOUT_ADMIN } from '@testing/mocks/node.mock';

describe('ComponentCardComponent', () => {
let component: ComponentCardComponent;
let fixture: ComponentFixture<ComponentCardComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ComponentCardComponent, ...MockComponents(IconComponent, ContributorsListComponent)],
}).compileComponents();

fixture = TestBed.createComponent(ComponentCardComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('component', MOCK_NODE_WITH_ADMIN);
fixture.componentRef.setInput('anonymous', false);
fixture.detectChanges();
});

it('should emit navigate when handleNavigate is called', () => {
const emitSpy = jest.spyOn(component.navigate, 'emit');
component.handleNavigate('test-id');
expect(emitSpy).toHaveBeenCalledWith('test-id');
});

it('should emit menuAction when handleMenuAction is called', () => {
const emitSpy = jest.spyOn(component.menuAction, 'emit');
component.handleMenuAction('settings');
expect(emitSpy).toHaveBeenCalledWith('settings');
});

describe('componentActionItems', () => {
it('should return base items for any component', () => {
fixture.componentRef.setInput('component', MOCK_NODE_WITHOUT_ADMIN);
fixture.detectChanges();
const items = component.componentActionItems();
expect(items).toHaveLength(2);
expect(items[0].action).toBe('manageContributors');
expect(items[1].action).toBe('settings');
});

it('should include delete action when component has Admin permission', () => {
fixture.componentRef.setInput('component', MOCK_NODE_WITH_ADMIN);
fixture.detectChanges();
const items = component.componentActionItems();
expect(items).toHaveLength(3);
expect(items[0].action).toBe('manageContributors');
expect(items[1].action).toBe('settings');
expect(items[2].action).toBe('delete');
});

it('should exclude delete action when component does not have Admin permission', () => {
fixture.componentRef.setInput('component', MOCK_NODE_WITHOUT_ADMIN);
fixture.detectChanges();
const items = component.componentActionItems();
expect(items).toHaveLength(2);
expect(items.every((item) => item.action !== 'delete')).toBe(true);
});

it('should exclude delete action when hideDeleteAction is true', () => {
fixture.componentRef.setInput('component', MOCK_NODE_WITH_ADMIN);
fixture.componentRef.setInput('hideDeleteAction', true);
fixture.detectChanges();
const items = component.componentActionItems();
expect(items).toHaveLength(2);
expect(items.every((item) => item.action !== 'delete')).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { TranslatePipe } from '@ngx-translate/core';

import { Button } from 'primeng/button';
import { Menu } from 'primeng/menu';

import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';

import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component';
import { IconComponent } from '@osf/shared/components/icon/icon.component';
import { TruncatedTextComponent } from '@osf/shared/components/truncated-text/truncated-text.component';
import { UserPermissions } from '@osf/shared/enums/user-permissions.enum';
import { NodeModel } from '@osf/shared/models/nodes/base-node.model';

@Component({
selector: 'osf-component-card',
imports: [Button, Menu, TranslatePipe, TruncatedTextComponent, IconComponent, ContributorsListComponent],
templateUrl: './component-card.component.html',
styleUrl: './component-card.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ComponentCardComponent {
component = input.required<NodeModel>();
anonymous = input.required<boolean>();
hideDeleteAction = input<boolean>(false);

navigate = output<string>();
menuAction = output<string>();

readonly componentActionItems = computed(() => {
const component = this.component();

const baseItems = [
{
label: 'project.overview.actions.manageContributors',
action: 'manageContributors',
},
{
label: 'project.overview.actions.settings',
action: 'settings',
},
];

if (!this.hideDeleteAction() && component.currentUserPermissions.includes(UserPermissions.Admin)) {
baseItems.push({
label: 'project.overview.actions.delete',
action: 'delete',
});
}

return baseItems;
});

handleNavigate(componentId: string): void {
this.navigate.emit(componentId);
}

handleMenuAction(action: string): void {
this.menuAction.emit(action);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h2>{{ 'project.overview.linkedProjects.title' | translate }}</h2>
<div class="linked-project-wrapper flex flex-column p-3 gap-3">
<div class="flex justify-content-between align-items-center">
<h2 class="flex align-items-center gap-2">
<osf-icon [iconClass]="linkedResource.public ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
<osf-icon [iconClass]="linkedResource.isPublic ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
<a class="linked-project-title" [href]="linkedResource.id">{{ linkedResource.title }}</a>
</h2>
@if (canEdit()) {
Expand All @@ -39,7 +39,7 @@ <h2 class="flex align-items-center gap-2">
<div class="flex flex-wrap gap-1">
<p class="font-bold">{{ 'common.labels.contributors' | translate }}:</p>

<osf-contributors-list [contributors]="linkedResource.contributors"></osf-contributors-list>
<osf-contributors-list [contributors]="linkedResource.bibliographicContributors"></osf-contributors-list>
</div>
@if (linkedResource.description) {
<osf-truncated-text
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,83 @@
import { MockComponents } from 'ng-mocks';
import { MockComponents, MockProvider } from 'ng-mocks';

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

import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component';
import { IconComponent } from '@osf/shared/components/icon/icon.component';
import { TruncatedTextComponent } from '@osf/shared/components/truncated-text/truncated-text.component';
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
import { NodeLinksSelectors } from '@osf/shared/stores/node-links';

import { DeleteNodeLinkDialogComponent } from '../delete-node-link-dialog/delete-node-link-dialog.component';
import { LinkResourceDialogComponent } from '../link-resource-dialog/link-resource-dialog.component';

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

describe.skip('LinkedProjectsComponent', () => {
import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock';
import { OSFTestingModule } from '@testing/osf.testing.module';
import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';

describe('LinkedProjectsComponent', () => {
let component: LinkedResourcesComponent;
let fixture: ComponentFixture<LinkedResourcesComponent>;
let customDialogServiceMock: ReturnType<CustomDialogServiceMockBuilder['build']>;

const mockLinkedResources = [
{ ...MOCK_NODE_WITH_ADMIN, id: 'resource-1', title: 'Linked Resource 1' },
{ ...MOCK_NODE_WITH_ADMIN, id: 'resource-2', title: 'Linked Resource 2' },
{ ...MOCK_NODE_WITH_ADMIN, id: 'resource-3', title: 'Linked Resource 3' },
];

beforeEach(async () => {
customDialogServiceMock = CustomDialogServiceMockBuilder.create().withDefaultOpen().build();

await TestBed.configureTestingModule({
imports: [
LinkedResourcesComponent,
...MockComponents(TruncatedTextComponent, IconComponent, ContributorsListComponent),
OSFTestingModule,
...MockComponents(IconComponent, ContributorsListComponent),
],
providers: [
provideMockStore({
signals: [
{ selector: NodeLinksSelectors.getLinkedResources, value: mockLinkedResources },
{ selector: NodeLinksSelectors.getLinkedResourcesLoading, value: false },
],
}),
MockProvider(CustomDialogService, customDialogServiceMock),
],
}).compileComponents();

fixture = TestBed.createComponent(LinkedResourcesComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('canEdit', true);
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
it('should open LinkResourceDialogComponent with correct config', () => {
component.openLinkProjectModal();

expect(customDialogServiceMock.open).toHaveBeenCalledWith(LinkResourceDialogComponent, {
header: 'project.overview.dialog.linkProject.header',
width: '850px',
});
});

it('should find resource by id and open DeleteNodeLinkDialogComponent with correct config when resource exists', () => {
component.openDeleteResourceModal('resource-2');

expect(customDialogServiceMock.open).toHaveBeenCalledWith(DeleteNodeLinkDialogComponent, {
header: 'project.overview.dialog.deleteNodeLink.header',
width: '650px',
data: { currentLink: mockLinkedResources[1] },
});
});

it('should return early and not open dialog when resource is not found', () => {
customDialogServiceMock.open.mockClear();

component.openDeleteResourceModal('non-existent-id');

expect(customDialogServiceMock.open).not.toHaveBeenCalled();
});
});
Loading