From 86e0a13820b308886f8037f7d6b86ce160e6b316 Mon Sep 17 00:00:00 2001 From: futa-ikeda Date: Fri, 15 May 2026 15:17:33 -0400 Subject: [PATCH 1/2] feat(dashboard): Prevent project creation --- .../home/pages/dashboard/dashboard.component.html | 6 +++++- .../pages/dashboard/dashboard.component.spec.ts | 11 +++++++++++ .../home/pages/dashboard/dashboard.component.ts | 14 +++++++++++++- .../sub-header/sub-header.component.html | 1 + .../sub-header/sub-header.component.spec.ts | 11 +++++++++++ .../components/sub-header/sub-header.component.ts | 1 + src/assets/i18n/en.json | 3 +++ 7 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/app/features/home/pages/dashboard/dashboard.component.html b/src/app/features/home/pages/dashboard/dashboard.component.html index 059bf4be3..7b29997a2 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.html +++ b/src/app/features/home/pages/dashboard/dashboard.component.html @@ -8,6 +8,8 @@ [title]="'home.loggedIn.dashboard.title' | translate" [icon]="'fas fa-home'" [buttonLabel]="'home.loggedIn.dashboard.createProject' | translate" + [isButtonDisabled]="projectCreationDisabled()" + [buttonTooltip]="buttonTooltip() | translate" (buttonClick)="createProject()" /> @@ -69,11 +71,13 @@

{{ 'home.loggedIn.latestResearch.title' | translate }}

[title]="'home.loggedIn.dashboard.welcome' | translate" [icon]="'home'" [buttonLabel]="'home.loggedIn.dashboard.createProject' | translate" + [isButtonDisabled]="projectCreationDisabled()" + [buttonTooltip]="buttonTooltip() | translate" (buttonClick)="createProject()" />
-

{{ '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/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(false); isSubmitting = input(false); isButtonDisabled = input(false); + buttonTooltip = input(''); buttonClick = output(); } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 2887a045d..2599fb079 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -796,6 +796,8 @@ "loggedIn": { "dashboard": { "createProject": "Create New Project", + "createProjectDisabledTooltip": "Projects can no longer be created.", + "getStartedHelp": "Visit Get Started Help Guides", "images": { "osfCollectionsImageAltText": "OSF Collections", @@ -804,6 +806,7 @@ "osfRegistriesImageAltTest": "OSF Registries" }, "noCreatedProject": "You haven’t created a project yet. Click the \"Create New Project\" button above to get started.", + "noCreatedProjectAndCreateProjectDisabled": "You haven’t created a project yet.", "quickSearch": { "goTo": "Go to", "myProjects": "My Projects", From 8b197a3b44fab7850b8cde50bc2d7af6449c0db4 Mon Sep 17 00:00:00 2001 From: futa-ikeda Date: Fri, 15 May 2026 15:18:41 -0400 Subject: [PATCH 2/2] feat(my-projects): Prevent project creation --- src/app/features/my-projects/my-projects.component.html | 2 ++ .../features/my-projects/my-projects.component.spec.ts | 9 +++++++++ src/app/features/my-projects/my-projects.component.ts | 6 ++++++ src/assets/i18n/en.json | 1 + 4 files changed, 18 insertions(+) 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/assets/i18n/en.json b/src/assets/i18n/en.json index 2599fb079..2a11b6457 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1098,6 +1098,7 @@ }, "header": { "createProject": "Create Project", + "createProjectDisabledTooltip": "Projects can no longer be created.", "title": "My Projects" }, "redirectDialog": {