Skip to content

Commit 1325bc9

Browse files
authored
[ENG-10529] Users can still submit to registries that are closed to new submissions (#903)
- Ticket: https://openscience.atlassian.net/browse/ENG-10529 - Feature flag: n/a ## Purpose In admin, the Product Team can determine when a registry (and other services) are open for new submissions. When closed, a user would see no “Add registration” button when on the registration pages, and direct submission links (ie https://osf.io/registries/dataarchive/new) will throw a not allowed error and not allow submissions. However, registries closed to submissions can be submitted to freely. This is a misleading status, and confusing for users and members. For example, the Character Lab Registry has been closed to submission for several years but is now available for submissions: https://osf.io/registries/characterlabregistry ## Summary of Changes Not render `Add Registration` button if `allow_submissions` is set to false in admin redirect and show error message if on link /new access `allow_submissions` is set to false
1 parent 4a85dd3 commit 1325bc9

7 files changed

Lines changed: 183 additions & 98 deletions

File tree

Lines changed: 77 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,92 @@
1-
<section class="h-full" data-test-new-registration-form>
2-
<osf-sub-header [title]="'registries.new.addNewRegistry' | translate" />
1+
@if (canShowForm()) {
2+
<section class="h-full" data-test-new-registration-form>
3+
<osf-sub-header [title]="'registries.new.addNewRegistry' | translate" />
34

4-
<section class="flex flex-column lg:flex-row flex-1 p-5 gap-4 bg-white w-full">
5-
<p>
6-
{{ 'registries.new.infoText1' | translate }}
7-
<a class="font-bold" href="https://help.osf.io/"> {{ 'common.links.clickHere' | translate }}</a>
8-
{{ 'registries.new.infoText2' | translate }}
9-
</p>
10-
</section>
5+
<section class="flex flex-column lg:flex-row flex-1 p-5 gap-4 bg-white w-full">
6+
<p>
7+
{{ 'registries.new.infoText1' | translate }}
8+
<a class="font-bold" href="https://help.osf.io/"> {{ 'common.links.clickHere' | translate }}</a>
9+
{{ 'registries.new.infoText2' | translate }}
10+
</p>
11+
</section>
12+
13+
<section class="flex flex-column flex-1 p-5 gap-4 w-full bg-white">
14+
<p-card class="w-full">
15+
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} 1</h2>
16+
<p class="mb-4 text-lg font-bold">{{ 'registries.new.steps.step1' | translate }}</p>
17+
<div class="flex gap-2">
18+
<p-button
19+
class="btn-full-width w-2 font-bold"
20+
[class]="{ 'pointer-events-none': fromProject() }"
21+
severity="info"
22+
[label]="'common.buttons.yes' | translate"
23+
[raised]="fromProject()"
24+
(onClick)="toggleFromProject()"
25+
/>
26+
<p-button
27+
class="btn-full-width w-2"
28+
[class]="{ 'pointer-events-none': !fromProject() }"
29+
severity="info"
30+
[label]="'common.buttons.no' | translate"
31+
[raised]="!fromProject()"
32+
(onClick)="toggleFromProject()"
33+
/>
34+
</div>
35+
</p-card>
1136

12-
<section class="flex flex-column flex-1 p-5 gap-4 w-full bg-white">
13-
<p-card class="w-full">
14-
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} 1</h2>
15-
<p class="mb-4 text-lg font-bold">{{ 'registries.new.steps.step1' | translate }}</p>
16-
<div class="flex gap-2">
17-
<p-button
18-
class="btn-full-width w-2 font-bold"
19-
[class]="{ 'pointer-events-none': fromProject() }"
20-
severity="info"
21-
[label]="'common.buttons.yes' | translate"
22-
[raised]="fromProject()"
23-
(onClick)="toggleFromProject()"
24-
/>
25-
<p-button
26-
class="btn-full-width w-2"
27-
[class]="{ 'pointer-events-none': !fromProject() }"
28-
severity="info"
29-
[label]="'common.buttons.no' | translate"
30-
[raised]="!fromProject()"
31-
(onClick)="toggleFromProject()"
32-
/>
33-
</div>
34-
</p-card>
37+
<form [formGroup]="draftForm" (ngSubmit)="createDraft()" class="flex flex-column gap-4">
38+
@if (fromProject()) {
39+
<p-card class="w-full">
40+
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} 2</h2>
41+
<p class="mb-3 text-lg font-bold">{{ 'registries.new.steps.step2' | translate }}</p>
42+
<p class="mb-4">{{ 'registries.new.steps.step2InfoText' | translate }}</p>
43+
<div class="flex">
44+
<p-select
45+
data-test-project-select
46+
formControlName="project"
47+
[options]="projects()"
48+
[placeholder]="'registries.new.selectProject' | translate"
49+
optionLabel="title"
50+
optionValue="id"
51+
filter="true"
52+
[loading]="isProjectsLoading()"
53+
(onFilter)="onProjectFilter($event.filter)"
54+
class="w-6"
55+
/>
56+
</div>
57+
</p-card>
58+
}
3559

36-
<form [formGroup]="draftForm" (ngSubmit)="createDraft()" class="flex flex-column gap-4">
37-
@if (fromProject()) {
3860
<p-card class="w-full">
39-
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} 2</h2>
40-
<p class="mb-3 text-lg font-bold">{{ 'registries.new.steps.step2' | translate }}</p>
41-
<p class="mb-4">{{ 'registries.new.steps.step2InfoText' | translate }}</p>
61+
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} {{ fromProject() ? '3' : '2' }}</h2>
62+
<p class="mb-4 text-lg font-bold">{{ 'registries.new.steps.step3' | translate }}</p>
4263
<div class="flex">
4364
<p-select
44-
data-test-project-select
45-
formControlName="project"
46-
[options]="projects()"
47-
[placeholder]="'registries.new.selectProject' | translate"
48-
optionLabel="title"
65+
data-test-schema-select
66+
formControlName="providerSchema"
67+
[options]="providerSchemas()"
68+
optionLabel="name"
4969
optionValue="id"
50-
filter="true"
51-
[loading]="isProjectsLoading()"
52-
(onFilter)="onProjectFilter($event.filter)"
70+
[loading]="isProvidersLoading()"
5371
class="w-6"
5472
/>
5573
</div>
5674
</p-card>
57-
}
5875

59-
<p-card class="w-full">
60-
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} {{ fromProject() ? '3' : '2' }}</h2>
61-
<p class="mb-4 text-lg font-bold">{{ 'registries.new.steps.step3' | translate }}</p>
62-
<div class="flex">
63-
<p-select
64-
data-test-schema-select
65-
formControlName="providerSchema"
66-
[options]="providerSchemas()"
67-
optionLabel="name"
68-
optionValue="id"
69-
[loading]="isProvidersLoading()"
70-
class="w-6"
76+
<div class="flex justify-content-end">
77+
<p-button
78+
data-test-start-registration-button
79+
[label]="'registries.new.createDraft' | translate"
80+
[disabled]="draftForm.invalid"
81+
type="submit"
82+
[loading]="isDraftSubmitting()"
7183
/>
7284
</div>
73-
</p-card>
74-
75-
<div class="flex justify-content-end">
76-
<p-button
77-
data-test-start-registration-button
78-
[label]="'registries.new.createDraft' | translate"
79-
[disabled]="draftForm.invalid"
80-
type="submit"
81-
[loading]="isDraftSubmitting()"
82-
/>
83-
</div>
84-
</form>
85+
</form>
86+
</section>
8587
</section>
86-
</section>
88+
} @else {
89+
<div class="flex-1">
90+
<osf-loading-spinner />
91+
</div>
92+
}

src/app/features/registries/components/new-registration/new-registration.component.spec.ts

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,61 @@ import { UserSelectors } from '@core/store/user';
99
import { CreateDraft, GetProjects, GetProviderSchemas, RegistriesSelectors } from '@osf/features/registries/store';
1010
import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component';
1111
import { ToastService } from '@osf/shared/services/toast.service';
12-
import { GetRegistryProvider } from '@shared/stores/registration-provider';
12+
import { GetRegistryProvider, RegistrationProviderSelectors } from '@shared/stores/registration-provider';
1313

1414
import { NewRegistrationComponent } from './new-registration.component';
1515

1616
import { MOCK_PROVIDER_SCHEMAS } from '@testing/mocks/registries.mock';
1717
import { provideOSFCore } from '@testing/osf.testing.provider';
1818
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
1919
import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock';
20-
import { provideMockStore } from '@testing/providers/store-provider.mock';
20+
import {
21+
BaseSetupOverrides,
22+
mergeSignalOverrides,
23+
provideMockStore,
24+
SignalOverride,
25+
} from '@testing/providers/store-provider.mock';
26+
import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock';
2127

2228
describe('NewRegistrationComponent', () => {
2329
let component: NewRegistrationComponent;
2430
let fixture: ComponentFixture<NewRegistrationComponent>;
2531
let store: Store;
2632
let mockRouter: RouterMockType;
27-
28-
beforeEach(() => {
33+
let toastService: ToastServiceMockType;
34+
35+
interface SetupOverrides extends BaseSetupOverrides {
36+
selectorOverrides?: SignalOverride[];
37+
}
38+
39+
const defaultSignals: SignalOverride[] = [
40+
{ selector: RegistriesSelectors.getProjects, value: [{ id: 'p1', title: 'P1' }] },
41+
{ selector: RegistriesSelectors.getProviderSchemas, value: MOCK_PROVIDER_SCHEMAS },
42+
{ selector: RegistriesSelectors.isDraftSubmitting, value: false },
43+
{ selector: RegistriesSelectors.getDraftRegistration, value: { id: 'draft-1' } },
44+
{ selector: RegistriesSelectors.isProvidersLoading, value: false },
45+
{ selector: RegistriesSelectors.isProjectsLoading, value: false },
46+
{ selector: UserSelectors.getCurrentUser, value: { id: 'user-1' } },
47+
{ selector: RegistrationProviderSelectors.getBrandedProvider, value: { id: 'prov-1', allowSubmissions: true } },
48+
];
49+
50+
const setup = (overrides?: SetupOverrides) => {
2951
const mockActivatedRoute = ActivatedRouteMockBuilder.create()
30-
.withParams({ providerId: 'prov-1' })
52+
.withParams(overrides?.routeParams || { providerId: 'prov-1' })
3153
.withQueryParams({ projectId: 'proj-1' })
3254
.build();
3355
mockRouter = RouterMockBuilder.create().withUrl('/x').build();
56+
toastService = ToastServiceMock.simple();
3457

3558
TestBed.configureTestingModule({
3659
imports: [NewRegistrationComponent, MockComponent(SubHeaderComponent)],
3760
providers: [
3861
provideOSFCore(),
3962
MockProvider(ActivatedRoute, mockActivatedRoute),
40-
MockProvider(ToastService),
63+
MockProvider(ToastService, toastService),
4164
MockProvider(Router, mockRouter),
4265
provideMockStore({
43-
signals: [
44-
{ selector: RegistriesSelectors.getProjects, value: [{ id: 'p1', title: 'P1' }] },
45-
{ selector: RegistriesSelectors.getProviderSchemas, value: MOCK_PROVIDER_SCHEMAS },
46-
{ selector: RegistriesSelectors.isDraftSubmitting, value: false },
47-
{ selector: RegistriesSelectors.getDraftRegistration, value: { id: 'draft-1' } },
48-
{ selector: RegistriesSelectors.isProvidersLoading, value: false },
49-
{ selector: RegistriesSelectors.isProjectsLoading, value: false },
50-
{ selector: UserSelectors.getCurrentUser, value: { id: 'user-1' } },
51-
],
66+
signals: mergeSignalOverrides(defaultSignals, overrides?.selectorOverrides),
5267
}),
5368
],
5469
});
@@ -57,31 +72,69 @@ describe('NewRegistrationComponent', () => {
5772
fixture = TestBed.createComponent(NewRegistrationComponent);
5873
component = fixture.componentInstance;
5974
fixture.detectChanges();
60-
});
75+
};
6176

6277
it('should create', () => {
78+
setup();
6379
expect(component).toBeTruthy();
6480
});
6581

82+
it('should allow submissions when provider allows it', () => {
83+
setup();
84+
expect(component.canShowForm()).toBe(true);
85+
expect(toastService.showError).not.toHaveBeenCalled();
86+
expect(mockRouter.navigate).not.toHaveBeenCalled();
87+
});
88+
89+
it('should redirect and show error when submissions are not allowed', () => {
90+
setup({
91+
selectorOverrides: [
92+
{
93+
selector: RegistrationProviderSelectors.getBrandedProvider,
94+
value: { id: 'prov-1', allowSubmissions: false },
95+
},
96+
],
97+
});
98+
99+
expect(component.canShowForm()).toBe(false);
100+
expect(toastService.showError).toHaveBeenCalledWith('registries.new.registryClosedForSubmissions');
101+
expect(mockRouter.navigate).toHaveBeenCalledWith(['/registries', 'prov-1']);
102+
});
103+
104+
it('should redirect and show error when allowSubmissions is undefined', () => {
105+
setup({
106+
selectorOverrides: [{ selector: RegistrationProviderSelectors.getBrandedProvider, value: { id: 'prov-1' } }],
107+
});
108+
109+
expect(component.canShowForm()).toBe(false);
110+
expect(toastService.showError).toHaveBeenCalledWith('registries.new.registryClosedForSubmissions');
111+
expect(mockRouter.navigate).toHaveBeenCalledWith(['/registries', 'prov-1']);
112+
});
113+
66114
it('should dispatch initial data fetching on init', () => {
115+
setup();
67116
expect(store.dispatch).toHaveBeenCalledWith(new GetProjects('user-1', ''));
68117
expect(store.dispatch).toHaveBeenCalledWith(new GetRegistryProvider('prov-1'));
69118
expect(store.dispatch).toHaveBeenCalledWith(new GetProviderSchemas('prov-1'));
70119
});
71120

72121
it('should init fromProject as true when projectId is present', () => {
122+
setup();
73123
expect(component.fromProject()).toBe(true);
74124
});
75125

76126
it('should init form with project id from route', () => {
127+
setup();
77128
expect(component.draftForm.get('project')?.value).toBe('proj-1');
78129
});
79130

80131
it('should default providerSchema when schemas are available', () => {
132+
setup();
81133
expect(component.draftForm.get('providerSchema')?.value).toBe('schema-1');
82134
});
83135

84136
it('should toggle fromProject and add/remove validator', () => {
137+
setup();
85138
component.fromProject.set(false);
86139
component.toggleFromProject();
87140
expect(component.fromProject()).toBe(true);
@@ -93,6 +146,7 @@ describe('NewRegistrationComponent', () => {
93146
});
94147

95148
it('should dispatch createDraft and navigate when form is valid', () => {
149+
setup();
96150
component.draftForm.patchValue({ providerSchema: 'schema-1', project: 'proj-1' });
97151
component.fromProject.set(true);
98152
(store.dispatch as jest.Mock).mockClear();
@@ -106,6 +160,7 @@ describe('NewRegistrationComponent', () => {
106160
});
107161

108162
it('should not dispatch createDraft when form is invalid', () => {
163+
setup();
109164
component.draftForm.patchValue({ providerSchema: '' });
110165
(store.dispatch as jest.Mock).mockClear();
111166

@@ -115,6 +170,7 @@ describe('NewRegistrationComponent', () => {
115170
});
116171

117172
it('should dispatch getProjects after debounced filter', fakeAsync(() => {
173+
setup();
118174
(store.dispatch as jest.Mock).mockClear();
119175

120176
component.onProjectFilter('abc');
@@ -124,6 +180,7 @@ describe('NewRegistrationComponent', () => {
124180
}));
125181

126182
it('should not dispatch duplicate getProjects for same filter value', fakeAsync(() => {
183+
setup();
127184
(store.dispatch as jest.Mock).mockClear();
128185

129186
component.onProjectFilter('abc');
@@ -132,12 +189,13 @@ describe('NewRegistrationComponent', () => {
132189
tick(300);
133190

134191
const getProjectsCalls = (store.dispatch as jest.Mock).mock.calls.filter(
135-
([action]: [any]) => action instanceof GetProjects
192+
([action]: [unknown]) => action instanceof GetProjects
136193
);
137194
expect(getProjectsCalls.length).toBe(1);
138195
}));
139196

140197
it('should debounce rapid filter calls and dispatch only the last value', fakeAsync(() => {
198+
setup();
141199
(store.dispatch as jest.Mock).mockClear();
142200

143201
component.onProjectFilter('a');
@@ -146,7 +204,7 @@ describe('NewRegistrationComponent', () => {
146204
tick(300);
147205

148206
const getProjectsCalls = (store.dispatch as jest.Mock).mock.calls.filter(
149-
([action]: [any]) => action instanceof GetProjects
207+
([action]: [unknown]) => action instanceof GetProjects
150208
);
151209
expect(getProjectsCalls.length).toBe(1);
152210
expect(getProjectsCalls[0][0]).toEqual(new GetProjects('user-1', 'abc'));

0 commit comments

Comments
 (0)