Skip to content
Open
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
@@ -0,0 +1,17 @@
import { ResourceVisibilityFilter } from '@osf/shared/enums/resource-visibility-filter.enum';
import { TabOption } from '@osf/shared/models/tab-option.model';

export const VISIBILITY_FILTER_OPTIONS: TabOption[] = [
{
value: ResourceVisibilityFilter.All,
label: 'myProjects.visibilityFilterOptions.all',
},
{
value: ResourceVisibilityFilter.Public,
label: 'myProjects.visibilityFilterOptions.public',
},
{
value: ResourceVisibilityFilter.Private,
label: 'myProjects.visibilityFilterOptions.private',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ResourceSearchMode } from '@osf/shared/enums/resource-search-mode.enum';
import { ResourceVisibilityFilter } from '@osf/shared/enums/resource-visibility-filter.enum';

import { getMyProjectsEmptyMessageKey } from './get-my-projects-empty-message-key.util';

describe('getMyProjectsEmptyMessageKey', () => {
it('should return all visibility messages for User search mode', () => {
expect(getMyProjectsEmptyMessageKey(ResourceSearchMode.User, ResourceVisibilityFilter.All)).toBe(
'myProjects.table.emptyState.all.both'
);
expect(getMyProjectsEmptyMessageKey(ResourceSearchMode.User, ResourceVisibilityFilter.Public)).toBe(
'myProjects.table.emptyState.all.public'
);
expect(getMyProjectsEmptyMessageKey(ResourceSearchMode.User, ResourceVisibilityFilter.Private)).toBe(
'myProjects.table.emptyState.all.private'
);
});

it('should return projects visibility messages for Root search mode', () => {
expect(getMyProjectsEmptyMessageKey(ResourceSearchMode.Root, ResourceVisibilityFilter.All)).toBe(
'myProjects.table.emptyState.projects.both'
);
expect(getMyProjectsEmptyMessageKey(ResourceSearchMode.Root, ResourceVisibilityFilter.Public)).toBe(
'myProjects.table.emptyState.projects.public'
);
expect(getMyProjectsEmptyMessageKey(ResourceSearchMode.Root, ResourceVisibilityFilter.Private)).toBe(
'myProjects.table.emptyState.projects.private'
);
});

it('should return components visibility messages for Component search mode', () => {
expect(getMyProjectsEmptyMessageKey(ResourceSearchMode.Component, ResourceVisibilityFilter.All)).toBe(
'myProjects.table.emptyState.components.both'
);
expect(getMyProjectsEmptyMessageKey(ResourceSearchMode.Component, ResourceVisibilityFilter.Public)).toBe(
'myProjects.table.emptyState.components.public'
);
expect(getMyProjectsEmptyMessageKey(ResourceSearchMode.Component, ResourceVisibilityFilter.Private)).toBe(
'myProjects.table.emptyState.components.private'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ResourceSearchMode } from '@osf/shared/enums/resource-search-mode.enum';
import { ResourceVisibilityFilter } from '@osf/shared/enums/resource-visibility-filter.enum';

const EMPTY_STATE_KEYS: Record<ResourceSearchMode, Record<ResourceVisibilityFilter, string>> = {
[ResourceSearchMode.All]: {
[ResourceVisibilityFilter.All]: 'myProjects.table.emptyState.all.both',
[ResourceVisibilityFilter.Public]: 'myProjects.table.emptyState.all.public',
[ResourceVisibilityFilter.Private]: 'myProjects.table.emptyState.all.private',
},
[ResourceSearchMode.Root]: {
[ResourceVisibilityFilter.All]: 'myProjects.table.emptyState.projects.both',
[ResourceVisibilityFilter.Public]: 'myProjects.table.emptyState.projects.public',
[ResourceVisibilityFilter.Private]: 'myProjects.table.emptyState.projects.private',
},
[ResourceSearchMode.Component]: {
[ResourceVisibilityFilter.All]: 'myProjects.table.emptyState.components.both',
[ResourceVisibilityFilter.Public]: 'myProjects.table.emptyState.components.public',
[ResourceVisibilityFilter.Private]: 'myProjects.table.emptyState.components.private',
},
[ResourceSearchMode.User]: {
[ResourceVisibilityFilter.All]: 'myProjects.table.emptyState.all.both',
[ResourceVisibilityFilter.Public]: 'myProjects.table.emptyState.all.public',
[ResourceVisibilityFilter.Private]: 'myProjects.table.emptyState.all.private',
},
};

export function getMyProjectsEmptyMessageKey(
searchMode: ResourceSearchMode,
visibilityFilter: ResourceVisibilityFilter
): string {
return EMPTY_STATE_KEYS[searchMode][visibilityFilter];
}
9 changes: 9 additions & 0 deletions src/app/features/my-projects/my-projects.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
[fullWidth]="true"
(changeValue)="onProjectFilterChange()"
></osf-select>

<osf-select
class="flex-1"
[options]="visibilityFilterOption"
[(selectedValue)]="selectedVisibilityFilterOption"
[fullWidth]="true"
(changeValue)="onVisibilityFilterChange()"
></osf-select>
</div>

<osf-my-projects-table
Expand All @@ -51,6 +59,7 @@
[sortColumn]="sortColumn()"
[sortOrder]="sortOrder()"
[isLoading]="isLoading()"
[emptyMessageKey]="projectsEmptyMessageKey()"
(pageChange)="onPageChange($event)"
(sort)="onSort($event)"
(itemClick)="navigateToProject($event)"
Expand Down
117 changes: 114 additions & 3 deletions src/app/features/my-projects/my-projects.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-pro
import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock';

import { PROJECT_FILTER_OPTIONS } from './constants/project-filter-options.const';
import { VISIBILITY_FILTER_OPTIONS } from './constants/visibility-filter-options.const';
import { MyProjectsQueryService } from './services/my-projects-query.service';
import { MyProjectsTableParamsService } from './services/my-projects-table-params.service';
import { CreateProjectDialogComponent } from './components';
Expand Down Expand Up @@ -77,7 +78,11 @@ describe('MyProjectsComponent', () => {
{ selector: BookmarksSelectors.getBookmarksTotalCount, value: 0 },
];

function setup(selectorOverrides?: SignalOverride[]) {
function setup(
selectorOverrides?: SignalOverride[],
queryModelOverrides?: { search?: string },
routeQueryParams: Record<string, string> = { tab: '1', page: '1', size: '10' }
) {
routerMock = RouterMockBuilder.create().build();
customDialogService = CustomDialogServiceMock.simple();
projectRedirectDialogService = ProjectRedirectDialogServiceMock.simple();
Expand All @@ -86,14 +91,14 @@ describe('MyProjectsComponent', () => {
.withQueryModel({
page: 1,
size: 10,
search: '',
search: queryModelOverrides?.search ?? '',
sortColumn: '',
sortOrder: SortOrder.Asc,
})
.withSelectedTab(MyProjectsTab.Projects)
.build();
tableParamsServiceMock = MyProjectsTableParamsServiceMock.simple();
const routeMock = ActivatedRouteMockBuilder.create().withQueryParams({ tab: '1', page: '1', size: '10' }).build();
const routeMock = ActivatedRouteMockBuilder.create().withQueryParams(routeQueryParams).build();

TestBed.configureTestingModule({
imports: [
Expand Down Expand Up @@ -170,6 +175,7 @@ describe('MyProjectsComponent', () => {
expect(store.dispatch).toHaveBeenCalledWith(new ClearMyResources());
expect(component.selectedTab()).toBe(MyProjectsTab.Registrations);
expect(component.selectedProjectFilterOption()).toBe(PROJECT_FILTER_OPTIONS[0].value);
expect(component.selectedVisibilityFilterOption()).toBe(VISIBILITY_FILTER_OPTIONS[0].value);
expect(queryServiceMock.handleTabSwitch).toHaveBeenCalledWith(
{ tab: '1', page: '1', size: '10' },
MyProjectsTab.Registrations
Expand Down Expand Up @@ -210,6 +216,111 @@ describe('MyProjectsComponent', () => {
expect(routerMock.navigate).toHaveBeenCalledWith([projectItem.id]);
});

it('should navigate to registry and set active project', () => {
setup();

component.navigateToRegistry(projectItem);

expect(component.activeProject()).toEqual(projectItem);
expect(routerMock.navigate).toHaveBeenCalledWith([projectItem.id]);
});

it('should fetch projects on project filter change', () => {
setup();
const getMyProjectsSpy = vi.spyOn(component.actions, 'getMyProjects').mockReturnValue(of(void 0));

component.onProjectFilterChange();

expect(getMyProjectsSpy).toHaveBeenCalledWith(
1,
10,
{
searchValue: '',
searchFields: ['title', 'tags', 'description'],
sortColumn: '',
sortOrder: SortOrder.Asc,
},
PROJECT_FILTER_OPTIONS[0].value,
undefined,
VISIBILITY_FILTER_OPTIONS[0].value
);
expect(component.isLoading()).toBe(false);
});

it('should fetch projects on visibility filter change', () => {
setup();
const getMyProjectsSpy = vi.spyOn(component.actions, 'getMyProjects').mockReturnValue(of(void 0));

component.onVisibilityFilterChange();

expect(getMyProjectsSpy).toHaveBeenCalled();
expect(component.isLoading()).toBe(false);
});

it('should fetch preprints with preprint search fields', () => {
setup();
component.selectedTab.set(MyProjectsTab.Preprints);
const getMyPreprintsSpy = vi.spyOn(component.actions, 'getMyPreprints').mockReturnValue(of(void 0));

component.onProjectFilterChange();

expect(getMyPreprintsSpy).toHaveBeenCalledWith(1, 10, {
searchValue: '',
searchFields: ['title', 'tags'],
sortColumn: '',
sortOrder: SortOrder.Asc,
});
});

it('should fetch bookmarks when collection id exists', () => {
setup();
component.selectedTab.set(MyProjectsTab.Bookmarks);
const getBookmarksSpy = vi.spyOn(component.actions, 'getMyBookmarks').mockReturnValue(of(void 0));

component.onProjectFilterChange();

expect(getBookmarksSpy).toHaveBeenCalledWith('bookmark-collection-id', {
searchValue: '',
searchFields: ['title', 'tags', 'description'],
sortColumn: '',
sortOrder: SortOrder.Asc,
});
});

it('should not fetch bookmarks when collection id is missing', () => {
setup([{ selector: BookmarksSelectors.getBookmarksCollectionId, value: null }]);
component.selectedTab.set(MyProjectsTab.Bookmarks);
const getBookmarksSpy = vi.spyOn(component.actions, 'getMyBookmarks').mockReturnValue(of(void 0));

component.onProjectFilterChange();

expect(getBookmarksSpy).not.toHaveBeenCalled();
expect(component.isLoading()).toBe(true);
});

it('should compute no results message key when search is applied', () => {
setup(undefined, { search: 'alpha' }, { tab: '1', page: '1', size: '10', search: 'alpha' });

expect(component.projectsEmptyMessageKey()).toBe('common.search.noResultsFound');
});

it('should compute empty state message key when search is empty', () => {
setup();

expect(component.projectsEmptyMessageKey()).toBe('myProjects.table.emptyState.all.both');
});

it('should not redirect when create project dialog closes without project id', () => {
setup();
const onClose$ = new Subject<{ project?: { id?: string } }>();
customDialogService.open.mockReturnValue(CustomDialogServiceMock.dialogRefWithClose(onClose$.asObservable()));

component.createProject();
onClose$.next({ project: {} });

expect(projectRedirectDialogService.showProjectRedirectDialog).not.toHaveBeenCalled();
});

it('should delegate search handling after debounce', () => {
vi.useFakeTimers();
setup();
Expand Down
36 changes: 35 additions & 1 deletion src/app/features/my-projects/my-projects.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import { QueryParams } from '@shared/models/query-params.model';
import { TableParameters } from '@shared/models/table-parameters.model';

import { PROJECT_FILTER_OPTIONS } from './constants/project-filter-options.const';
import { VISIBILITY_FILTER_OPTIONS } from './constants/visibility-filter-options.const';
import { getMyProjectsEmptyMessageKey } from './helpers/get-my-projects-empty-message-key.util';
import { MyProjectsQueryService } from './services/my-projects-query.service';
import { MyProjectsTableParamsService } from './services/my-projects-table-params.service';
import { CreateProjectDialogComponent } from './components';
Expand Down Expand Up @@ -89,10 +91,25 @@ export class MyProjectsComponent implements OnInit {
readonly tabOptions = MY_PROJECTS_TABS;
readonly tabOption = MyProjectsTab;
readonly projectFilterOption = PROJECT_FILTER_OPTIONS;
readonly visibilityFilterOption = VISIBILITY_FILTER_OPTIONS;
readonly selectedProjectFilterOption = signal(PROJECT_FILTER_OPTIONS[0].value);
readonly selectedVisibilityFilterOption = signal(VISIBILITY_FILTER_OPTIONS[0].value);

readonly searchControl = new FormControl<string>('');

readonly appliedSearch = computed(() => {
const params = this.queryParams();
return params ? this.queryService.toQueryModel(params).search || '' : '';
});

readonly projectsEmptyMessageKey = computed(() => {
if (this.appliedSearch()) {
return 'common.search.noResultsFound';
}

return getMyProjectsEmptyMessageKey(this.selectedProjectFilterOption(), this.selectedVisibilityFilterOption());
});

readonly queryParams = toSignal(this.route.queryParams);
readonly currentPage = signal(1);
readonly currentPageSize = signal(DEFAULT_TABLE_PARAMS.rows);
Expand Down Expand Up @@ -154,6 +171,7 @@ export class MyProjectsComponent implements OnInit {
this.actions.clearMyProjects();
this.selectedTab.set(value);
this.selectedProjectFilterOption.set(PROJECT_FILTER_OPTIONS[0].value);
this.selectedVisibilityFilterOption.set(VISIBILITY_FILTER_OPTIONS[0].value);
const current = this.queryService.getRawParams();
this.queryService.handleTabSwitch(current, this.selectedTab());
}
Expand All @@ -168,6 +186,15 @@ export class MyProjectsComponent implements OnInit {
}
}

onVisibilityFilterChange(): void {
const params = this.queryParams();

if (params) {
const queryParams = this.queryService.toQueryModel(params);
this.fetchDataForCurrentTab(queryParams);
}
}

createProject(): void {
this.customDialogService
.open(CreateProjectDialogComponent, {
Expand Down Expand Up @@ -312,7 +339,14 @@ export class MyProjectsComponent implements OnInit {
let action$;
switch (this.selectedTab()) {
case MyProjectsTab.Projects:
action$ = this.actions.getMyProjects(pageNumber, pageSize, filters, this.selectedProjectFilterOption());
action$ = this.actions.getMyProjects(
pageNumber,
pageSize,
filters,
this.selectedProjectFilterOption(),
undefined,
this.selectedVisibilityFilterOption()
);
break;
case MyProjectsTab.Registrations:
action$ = this.actions.getMyRegistrations(pageNumber, pageSize, filters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@

<ng-template pTemplate="emptymessage">
<tr>
<td colspan="3" class="text-center">{{ 'common.search.noResultsFound' | translate }}</td>
<td colspan="3" class="text-center">{{ emptyMessageKey() | translate }}</td>
</tr>
</ng-template>
</p-table>
Loading
Loading