-
{{ 'home.loggedIn.dashboard.noCreatedProject' | translate }}
+
{{ noProjectsMessage() | translate }}
{{ 'home.loggedIn.dashboard.watchVideoBelow' | translate }}
diff --git a/src/app/features/home/pages/dashboard/dashboard.component.spec.ts b/src/app/features/home/pages/dashboard/dashboard.component.spec.ts
index ba3b7cf3c..c4ff71def 100644
--- a/src/app/features/home/pages/dashboard/dashboard.component.spec.ts
+++ b/src/app/features/home/pages/dashboard/dashboard.component.spec.ts
@@ -13,6 +13,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { ScheduledBannerComponent } from '@core/components/osf-banners/scheduled-banner/scheduled-banner.component';
+import { UserSelectors } from '@osf/core/store/user';
import { CreateProjectDialogComponent } from '@osf/features/my-projects/components';
import { IconComponent } from '@osf/shared/components/icon/icon.component';
import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component';
@@ -49,6 +50,7 @@ describe('DashboardComponent', () => {
{ selector: MyResourcesSelectors.getProjects, value: [] },
{ selector: MyResourcesSelectors.getTotalProjects, value: 0 },
{ selector: MyResourcesSelectors.getProjectsLoading, value: false },
+ { selector: UserSelectors.getActiveFlags, value: [] },
];
interface SetupOverrides extends BaseSetupOverrides {
@@ -98,6 +100,15 @@ describe('DashboardComponent', () => {
expect(component).toBeTruthy();
});
+ it('should disable project creation and show tooltip when prevent_project_creation flag is active', () => {
+ setup({
+ selectorOverrides: [{ selector: UserSelectors.getActiveFlags, value: ['prevent_project_creation'] }],
+ });
+
+ expect(component.projectCreationDisabled()).toBe(true);
+ expect(component.buttonTooltip()).toBe('myProjects.header.createProjectDisabledTooltip');
+ });
+
it('should read query params and fetch projects on init', () => {
setup({
routeQueryParams: {
diff --git a/src/app/features/home/pages/dashboard/dashboard.component.ts b/src/app/features/home/pages/dashboard/dashboard.component.ts
index f9fa9eb5c..7e89a8625 100644
--- a/src/app/features/home/pages/dashboard/dashboard.component.ts
+++ b/src/app/features/home/pages/dashboard/dashboard.component.ts
@@ -15,6 +15,7 @@ import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ScheduledBannerComponent } from '@core/components/osf-banners/scheduled-banner/scheduled-banner.component';
+import { UserSelectors } from '@osf/core/store/user';
import { CreateProjectDialogComponent } from '@osf/features/my-projects/components';
import { IconComponent } from '@osf/shared/components/icon/icon.component';
import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component';
@@ -29,7 +30,6 @@ import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'
import { ProjectRedirectDialogService } from '@osf/shared/services/project-redirect-dialog.service';
import { ClearMyResources, GetMyProjects, MyResourcesSelectors } from '@osf/shared/stores/my-resources';
import { TableParameters } from '@shared/models/table-parameters.model';
-
@Component({
selector: 'osf-dashboard',
imports: [
@@ -54,6 +54,7 @@ export class DashboardComponent implements OnInit {
private readonly projectRedirectDialogService = inject(ProjectRedirectDialogService);
private readonly platformId = inject(PLATFORM_ID);
private readonly isBrowser = isPlatformBrowser(this.platformId);
+ private readonly activeFlags = select(UserSelectors.getActiveFlags);
readonly searchControl = new FormControl
('');
readonly activeProject = signal(null);
@@ -72,7 +73,18 @@ export class DashboardComponent implements OnInit {
return this.projects().filter((project) => project.title.toLowerCase().includes(search));
});
+ readonly projectCreationDisabled = computed(() => this.activeFlags().includes('prevent_project_creation'));
+ readonly buttonTooltip = computed(() =>
+ this.projectCreationDisabled() ? 'myProjects.header.createProjectDisabledTooltip' : ''
+ );
+
readonly existsProjects = computed(() => this.projects().length || !!this.searchControl.value?.length);
+ readonly noProjectsMessage = computed(() => {
+ if (this.projectCreationDisabled()) {
+ return 'home.loggedIn.dashboard.noCreatedProjectAndCreateProjectDisabled';
+ }
+ return 'home.loggedIn.dashboard.noCreatedProject';
+ });
constructor() {
this.setupSearchSubscription();
diff --git a/src/app/features/my-projects/my-projects.component.html b/src/app/features/my-projects/my-projects.component.html
index e3a806661..a458d1f48 100644
--- a/src/app/features/my-projects/my-projects.component.html
+++ b/src/app/features/my-projects/my-projects.component.html
@@ -3,6 +3,8 @@
[showButton]="true"
[buttonLabel]="'myProjects.header.createProject' | translate"
[title]="'myProjects.header.title' | translate"
+ [isButtonDisabled]="projectCreationDisabled()"
+ [buttonTooltip]="buttonTooltip() | translate"
[icon]="'custom-icon-projects-dark'"
(buttonClick)="createProject()"
/>
diff --git a/src/app/features/my-projects/my-projects.component.spec.ts b/src/app/features/my-projects/my-projects.component.spec.ts
index 016753ab2..a148eea7e 100644
--- a/src/app/features/my-projects/my-projects.component.spec.ts
+++ b/src/app/features/my-projects/my-projects.component.spec.ts
@@ -11,6 +11,7 @@ import { Mock } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router';
+import { UserSelectors } from '@osf/core/store/user/user.selectors';
import { MyProjectsTableComponent } from '@osf/shared/components/my-projects-table/my-projects-table.component';
import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component';
import { SelectComponent } from '@osf/shared/components/select/select.component';
@@ -77,6 +78,7 @@ describe('MyProjectsComponent', () => {
{ selector: BookmarksSelectors.getBookmarks, value: [] },
{ selector: BookmarksSelectors.getBookmarksCollectionId, value: 'bookmark-collection-id' },
{ selector: BookmarksSelectors.getBookmarksTotalCount, value: 0 },
+ { selector: UserSelectors.getActiveFlags, value: [] },
];
function setup(selectorOverrides?: SignalOverride[]) {
@@ -132,6 +134,13 @@ describe('MyProjectsComponent', () => {
expect(component).toBeTruthy();
});
+ it('should disable project creation and show tooltip when prevent_project_creation flag is active', () => {
+ setup([{ selector: UserSelectors.getActiveFlags, value: ['prevent_project_creation'] }]);
+
+ expect(component.projectCreationDisabled()).toBe(true);
+ expect(component.buttonTooltip()).toBe('myProjects.header.createProjectDisabledTooltip');
+ });
+
it('should dispatch get bookmarks collection id on init', () => {
setup();
expect(store.dispatch).toHaveBeenCalledWith(new GetBookmarksCollectionId());
diff --git a/src/app/features/my-projects/my-projects.component.ts b/src/app/features/my-projects/my-projects.component.ts
index eb801ea78..15344bcea 100644
--- a/src/app/features/my-projects/my-projects.component.ts
+++ b/src/app/features/my-projects/my-projects.component.ts
@@ -25,6 +25,7 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
+import { UserSelectors } from '@osf/core/store/user';
import { MyProjectsTableComponent } from '@osf/shared/components/my-projects-table/my-projects-table.component';
import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component';
import { SelectComponent } from '@osf/shared/components/select/select.component';
@@ -83,6 +84,7 @@ export class MyProjectsComponent implements OnInit {
readonly tableParamsService = inject(MyProjectsTableParamsService);
readonly platformId = inject(PLATFORM_ID);
readonly isBrowser = isPlatformBrowser(this.platformId);
+ readonly activeFlags = select(UserSelectors.getActiveFlags);
readonly isLoading = signal(false);
readonly isMedium = toSignal(inject(IS_MEDIUM));
@@ -113,6 +115,10 @@ export class MyProjectsComponent implements OnInit {
readonly bookmarksCollectionId = select(BookmarksSelectors.getBookmarksCollectionId);
readonly totalBookmarksCount = select(BookmarksSelectors.getBookmarksTotalCount);
readonly isBookmarks = computed(() => this.selectedTab() === MyProjectsTab.Bookmarks);
+ readonly projectCreationDisabled = computed(() => this.activeFlags().includes('prevent_project_creation'));
+ readonly buttonTooltip = computed(() =>
+ this.projectCreationDisabled() ? 'myProjects.header.createProjectDisabledTooltip' : ''
+ );
readonly actions = createDispatchMap({
getBookmarksCollectionId: GetBookmarksCollectionId,
diff --git a/src/app/shared/components/sub-header/sub-header.component.html b/src/app/shared/components/sub-header/sub-header.component.html
index 13eee31c5..c198c9fb0 100644
--- a/src/app/shared/components/sub-header/sub-header.component.html
+++ b/src/app/shared/components/sub-header/sub-header.component.html
@@ -32,6 +32,7 @@
[loading]="isSubmitting()"
[disabled]="isButtonDisabled()"
data-test-sub-header-button
+ [pTooltip]="buttonTooltip()"
>
}
diff --git a/src/app/shared/components/sub-header/sub-header.component.spec.ts b/src/app/shared/components/sub-header/sub-header.component.spec.ts
index 74c875fea..a479eb79e 100644
--- a/src/app/shared/components/sub-header/sub-header.component.spec.ts
+++ b/src/app/shared/components/sub-header/sub-header.component.spec.ts
@@ -128,6 +128,11 @@ describe('SubHeaderComponent', () => {
expect(component.isButtonDisabled()).toBe(true);
});
+ it('should set buttonTooltip input correctly', () => {
+ fixture.componentRef.setInput('buttonTooltip', 'Test button tooltip');
+ expect(component.buttonTooltip()).toBe('Test button tooltip');
+ });
+
it('should emit buttonClick event', () => {
const emitSpy = vi.spyOn(component.buttonClick, 'emit');
@@ -155,12 +160,14 @@ describe('SubHeaderComponent', () => {
fixture.componentRef.setInput('description', 'Description with special chars: <>&"\'');
fixture.componentRef.setInput('buttonLabel', 'Button with special chars: !@#$%');
fixture.componentRef.setInput('tooltip', 'Tooltip with special chars: [{}]|\\');
+ fixture.componentRef.setInput('buttonTooltip', 'Button tooltip with special chars: @#$%()<>');
fixture.componentRef.setInput('icon', 'pi-icon-with-special-chars');
expect(component.title()).toBe('Title with special chars: @#$%^&*()');
expect(component.description()).toBe('Description with special chars: <>&"\'');
expect(component.buttonLabel()).toBe('Button with special chars: !@#$%');
expect(component.tooltip()).toBe('Tooltip with special chars: [{}]|\\');
+ expect(component.buttonTooltip()).toBe('Button tooltip with special chars: @#$%()<>');
expect(component.icon()).toBe('pi-icon-with-special-chars');
});
@@ -169,12 +176,14 @@ describe('SubHeaderComponent', () => {
fixture.componentRef.setInput('description', '');
fixture.componentRef.setInput('buttonLabel', '');
fixture.componentRef.setInput('tooltip', '');
+ fixture.componentRef.setInput('buttonTooltip', '');
fixture.componentRef.setInput('icon', '');
expect(component.title()).toBe('');
expect(component.description()).toBe('');
expect(component.buttonLabel()).toBe('');
expect(component.tooltip()).toBe('');
+ expect(component.buttonTooltip()).toBe('');
expect(component.icon()).toBe('');
});
@@ -193,9 +202,11 @@ describe('SubHeaderComponent', () => {
fixture.componentRef.setInput('showButton', true);
fixture.componentRef.setInput('isButtonDisabled', true);
fixture.componentRef.setInput('buttonLabel', 'Disabled Button');
+ fixture.componentRef.setInput('buttonTooltip', 'Disabled Button Tooltip');
expect(component.showButton()).toBe(true);
expect(component.isButtonDisabled()).toBe(true);
expect(component.buttonLabel()).toBe('Disabled Button');
+ expect(component.buttonTooltip()).toBe('Disabled Button Tooltip');
});
});
diff --git a/src/app/shared/components/sub-header/sub-header.component.ts b/src/app/shared/components/sub-header/sub-header.component.ts
index e0150cc8f..559f76924 100644
--- a/src/app/shared/components/sub-header/sub-header.component.ts
+++ b/src/app/shared/components/sub-header/sub-header.component.ts
@@ -25,5 +25,6 @@ export class SubHeaderComponent {
isLoading = input