diff --git a/src/app/features/metadata/components/metadata-registry-info/metadata-registry-info.component.spec.ts b/src/app/features/metadata/components/metadata-registry-info/metadata-registry-info.component.spec.ts
index 4a0f25eb2..34f80a903 100644
--- a/src/app/features/metadata/components/metadata-registry-info/metadata-registry-info.component.spec.ts
+++ b/src/app/features/metadata/components/metadata-registry-info/metadata-registry-info.component.spec.ts
@@ -19,6 +19,7 @@ describe('MetadataRegistryInfoComponent', () => {
iri: 'https://example.com/registry',
reviewsWorkflow: 'standard',
allowSubmissions: true,
+ allowUpdates: true,
};
beforeEach(() => {
diff --git a/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.spec.ts b/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.spec.ts
index abdc2a725..8fdba2575 100644
--- a/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.spec.ts
+++ b/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.spec.ts
@@ -37,6 +37,7 @@ describe('RegistryProviderHeroComponent', () => {
iri: '',
reviewsWorkflow: '',
allowSubmissions: false,
+ allowUpdates: true,
};
beforeEach(() => {
diff --git a/src/app/features/registries/pages/justification/justification.component.ts b/src/app/features/registries/pages/justification/justification.component.ts
index 610dfd668..96ddf6e12 100644
--- a/src/app/features/registries/pages/justification/justification.component.ts
+++ b/src/app/features/registries/pages/justification/justification.component.ts
@@ -165,23 +165,20 @@ export class JustificationComponent implements OnDestroy {
private initStepValidation(): void {
effect(() => {
- const currentIndex = this.currentStepIndex();
- const pages = this.pages();
- const revisionData = this.schemaResponseRevisionData();
const stepState = untracked(() => this.stepsState());
- if (currentIndex > 0) {
+ if (this.currentStepIndex() > 0) {
this.actions.updateStepState('0', true, stepState?.[0]?.touched || false);
}
- if (pages.length && currentIndex > 0 && revisionData) {
- for (let i = 1; i < currentIndex; i++) {
- const pageStep = pages[i - 1];
+ if (this.pages().length && this.currentStepIndex() > 0 && this.schemaResponseRevisionData()) {
+ for (let i = 1; i < this.currentStepIndex(); i++) {
+ const pageStep = this.pages()[i - 1];
const isStepInvalid =
pageStep?.questions?.some((question) => {
- const questionData = revisionData[question.responseKey!];
+ const questionData = this.schemaResponseRevisionData()[question.responseKey!];
return question.required && (Array.isArray(questionData) ? !questionData.length : !questionData);
- }) || false;
+ }) ?? false;
this.actions.updateStepState(i.toString(), isStepInvalid, stepState?.[i]?.touched || false);
}
}
diff --git a/src/app/features/registries/pages/registries-provider-search/registries-provider-search.component.spec.ts b/src/app/features/registries/pages/registries-provider-search/registries-provider-search.component.spec.ts
index e30b7d3c0..2e2fe0908 100644
--- a/src/app/features/registries/pages/registries-provider-search/registries-provider-search.component.spec.ts
+++ b/src/app/features/registries/pages/registries-provider-search/registries-provider-search.component.spec.ts
@@ -36,6 +36,7 @@ const MOCK_PROVIDER: RegistryProviderDetails = {
iri: 'http://iri.example.com',
reviewsWorkflow: 'pre-moderation',
allowSubmissions: true,
+ allowUpdates: true,
};
describe('RegistriesProviderSearchComponent', () => {
diff --git a/src/app/features/registry/components/registry-revisions/registry-revisions.component.html b/src/app/features/registry/components/registry-revisions/registry-revisions.component.html
index 794707631..6a50baf07 100644
--- a/src/app/features/registry/components/registry-revisions/registry-revisions.component.html
+++ b/src/app/features/registry/components/registry-revisions/registry-revisions.component.html
@@ -31,7 +31,7 @@
diff --git a/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts b/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts
index 24b0310b9..47d0b876a 100644
--- a/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts
+++ b/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts
@@ -168,6 +168,28 @@ describe('RegistryRevisionsComponent', () => {
expect(spy).toHaveBeenCalledWith(1);
});
+ it('should emit updateRegistration with registry id on startUpdateRegistration', () => {
+ const { component } = setup();
+ const spy = vi.fn();
+ component.updateRegistration.subscribe(spy);
+
+ component.startUpdateRegistration();
+
+ expect(spy).toHaveBeenCalledWith(MOCK_REGISTRY.id);
+ });
+
+ it('should not emit updateRegistration when registry id is missing on startUpdateRegistration', () => {
+ const { fixture, component } = setup();
+ const spy = vi.fn();
+ component.updateRegistration.subscribe(spy);
+ fixture.componentRef.setInput('registry', { ...MOCK_REGISTRY, id: '' });
+ fixture.detectChanges();
+
+ component.startUpdateRegistration();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
it('should emit continueUpdate on continueUpdateHandler', () => {
const { component } = setup();
const spy = vi.fn();
diff --git a/src/app/features/registry/components/registry-revisions/registry-revisions.component.ts b/src/app/features/registry/components/registry-revisions/registry-revisions.component.ts
index 46560bbcf..1a88fab9e 100644
--- a/src/app/features/registry/components/registry-revisions/registry-revisions.component.ts
+++ b/src/app/features/registry/components/registry-revisions/registry-revisions.component.ts
@@ -75,6 +75,16 @@ export class RegistryRevisionsComponent {
});
});
+ startUpdateRegistration() {
+ const registryId = this.registry()?.id;
+
+ if (!registryId) {
+ return;
+ }
+
+ this.updateRegistration.emit(registryId);
+ }
+
emitOpenRevision(index: number) {
this.openRevision.emit(index);
}
diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.html b/src/app/features/registry/pages/registry-overview/registry-overview.component.html
index b7d67b3ca..4712f0918 100644
--- a/src/app/features/registry/pages/registry-overview/registry-overview.component.html
+++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.html
@@ -84,7 +84,7 @@
(continueUpdate)="onContinueUpdateRegistration()"
[isModeration]="isModeration()"
[isSubmitting]="isSchemaResponsesLoading()"
- [canEdit]="hasAdminAccess()"
+ [canEdit]="canUpdate()"
>
diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts
index d4626ee13..d8833dd28 100644
--- a/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts
+++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts
@@ -20,6 +20,7 @@ import { SchemaResponse } from '@osf/shared/models/registration/schema-response.
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
import { ToastService } from '@osf/shared/services/toast.service';
import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service';
+import { RegistrationProviderSelectors } from '@osf/shared/stores/registration-provider';
import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock';
import { createMockSchemaResponse } from '@testing/mocks/schema-response.mock';
@@ -28,7 +29,7 @@ import { CustomDialogServiceMock } from '@testing/providers/custom-dialog-provid
import { LoaderServiceMock, provideLoaderServiceMock } from '@testing/providers/loader-service.mock';
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
import { RouterMockBuilder } from '@testing/providers/router-provider.mock';
-import { provideMockStore } from '@testing/providers/store-provider.mock';
+import { BaseSetupOverrides, mergeSignalOverrides, provideMockStore } from '@testing/providers/store-provider.mock';
import { ToastServiceMock } from '@testing/providers/toast-provider.mock';
import { ViewOnlyLinkHelperMock } from '@testing/providers/view-only-link-helper.mock';
@@ -44,7 +45,7 @@ import { RegistrySelectors } from '../../store/registry';
import { RegistryOverviewComponent } from './registry-overview.component';
-interface SetupOverrides {
+interface SetupOverrides extends BaseSetupOverrides {
registry?: RegistrationOverviewModel | null;
schemaResponses?: SchemaResponse[];
queryParams?: Record;
@@ -67,6 +68,19 @@ function setup(overrides: SetupOverrides = {}) {
const mockLoaderService = new LoaderServiceMock();
const mockToastService = ToastServiceMock.simple();
const mockViewOnlyHelper = ViewOnlyLinkHelperMock.simple(overrides.hasViewOnly);
+ const signalDefaults = [
+ { selector: RegistrySelectors.getRegistry, value: registry },
+ { selector: RegistrySelectors.isRegistryLoading, value: false },
+ { selector: RegistrySelectors.isRegistryAnonymous, value: false },
+ { selector: RegistrySelectors.getSchemaResponses, value: schemaResponses },
+ { selector: RegistrySelectors.isSchemaResponsesLoading, value: false },
+ { selector: RegistrySelectors.getSchemaBlocks, value: [] },
+ { selector: RegistrySelectors.isSchemaBlocksLoading, value: false },
+ { selector: RegistrySelectors.areReviewActionsLoading, value: false },
+ { selector: RegistrySelectors.getSchemaResponse, value: schemaResponses[0] ?? null },
+ { selector: RegistrySelectors.hasAdminAccess, value: false },
+ { selector: RegistrationProviderSelectors.allowUpdates, value: false },
+ ];
TestBed.configureTestingModule({
imports: [
@@ -94,18 +108,7 @@ function setup(overrides: SetupOverrides = {}) {
MockProvider(ToastService, mockToastService),
MockProvider(ViewOnlyLinkHelperService, mockViewOnlyHelper),
provideMockStore({
- signals: [
- { selector: RegistrySelectors.getRegistry, value: registry },
- { selector: RegistrySelectors.isRegistryLoading, value: false },
- { selector: RegistrySelectors.isRegistryAnonymous, value: false },
- { selector: RegistrySelectors.getSchemaResponses, value: schemaResponses },
- { selector: RegistrySelectors.isSchemaResponsesLoading, value: false },
- { selector: RegistrySelectors.getSchemaBlocks, value: [] },
- { selector: RegistrySelectors.isSchemaBlocksLoading, value: false },
- { selector: RegistrySelectors.areReviewActionsLoading, value: false },
- { selector: RegistrySelectors.getSchemaResponse, value: schemaResponses[0] ?? null },
- { selector: RegistrySelectors.hasAdminAccess, value: false },
- ],
+ signals: mergeSignalOverrides(signalDefaults, overrides.selectorOverrides),
}),
],
});
@@ -181,6 +184,30 @@ describe('RegistryOverviewComponent', () => {
expect(component.canMakeDecision()).toBe(false);
});
+ it('should compute canUpdate as true when admin access and provider updates are allowed', () => {
+ const { component } = setup({
+ registry: MOCK_REGISTRATION_OVERVIEW_MODEL,
+ selectorOverrides: [
+ { selector: RegistrySelectors.hasAdminAccess, value: true },
+ { selector: RegistrationProviderSelectors.allowUpdates, value: true },
+ ],
+ });
+
+ expect(component.canUpdate()).toBe(true);
+ });
+
+ it('should compute canUpdate as false when provider updates are not allowed', () => {
+ const { component } = setup({
+ registry: MOCK_REGISTRATION_OVERVIEW_MODEL,
+ selectorOverrides: [
+ { selector: RegistrySelectors.hasAdminAccess, value: true },
+ { selector: RegistrationProviderSelectors.allowUpdates, value: false },
+ ],
+ });
+
+ expect(component.canUpdate()).toBe(false);
+ });
+
it('should compute isInitialState from reviewsState', () => {
const { component } = setup({
registry: { ...MOCK_REGISTRATION_OVERVIEW_MODEL, reviewsState: RegistrationReviewStates.Initial },
diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts
index 927fc222a..fcf3bbac3 100644
--- a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts
+++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts
@@ -37,6 +37,7 @@ import { ToastService } from '@osf/shared/services/toast.service';
import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service';
import { GetBookmarksCollectionId } from '@osf/shared/stores/bookmarks';
import { GetBibliographicContributors } from '@osf/shared/stores/contributors';
+import { RegistrationProviderSelectors } from '@osf/shared/stores/registration-provider';
import { ArchivingMessageComponent } from '../../components/archiving-message/archiving-message.component';
import { RegistrationOverviewToolbarComponent } from '../../components/registration-overview-toolbar/registration-overview-toolbar.component';
@@ -98,6 +99,7 @@ export class RegistryOverviewComponent implements OnInit, OnDestroy {
readonly areReviewActionsLoading = select(RegistrySelectors.areReviewActionsLoading);
readonly currentRevision = select(RegistrySelectors.getSchemaResponse);
readonly hasAdminAccess = select(RegistrySelectors.hasAdminAccess);
+ readonly allowUpdates = select(RegistrationProviderSelectors.allowUpdates);
readonly selectedRevisionIndex = signal(0);
@@ -112,6 +114,8 @@ export class RegistryOverviewComponent implements OnInit, OnDestroy {
() => !this.registry()?.archiving && !this.registry()?.withdrawn && this.isModeration()
);
+ readonly canUpdate = computed(() => this.hasAdminAccess() && this.allowUpdates());
+
isRootRegistration = computed(() => {
const rootId = this.registry()?.rootParentId;
return !rootId || rootId === this.registry()?.id;
diff --git a/src/app/shared/mappers/registration-provider.mapper.ts b/src/app/shared/mappers/registration-provider.mapper.ts
index d97c2ebf8..8feb41303 100644
--- a/src/app/shared/mappers/registration-provider.mapper.ts
+++ b/src/app/shared/mappers/registration-provider.mapper.ts
@@ -35,6 +35,7 @@ export class RegistrationProviderMapper {
iri: response.links.iri,
reviewsWorkflow: response.attributes.reviews_workflow,
allowSubmissions: response.attributes.allow_submissions,
+ allowUpdates: response.attributes.allow_updates,
};
}
}
diff --git a/src/app/shared/models/provider/registry-provider.model.ts b/src/app/shared/models/provider/registry-provider.model.ts
index 1c914acd9..9ccec98f6 100644
--- a/src/app/shared/models/provider/registry-provider.model.ts
+++ b/src/app/shared/models/provider/registry-provider.model.ts
@@ -11,4 +11,5 @@ export interface RegistryProviderDetails {
iri: string;
reviewsWorkflow: string;
allowSubmissions: boolean;
+ allowUpdates: boolean;
}
diff --git a/src/app/shared/stores/registration-provider/registration-provider.selectors.ts b/src/app/shared/stores/registration-provider/registration-provider.selectors.ts
index 61010f5cb..d960ebd4e 100644
--- a/src/app/shared/stores/registration-provider/registration-provider.selectors.ts
+++ b/src/app/shared/stores/registration-provider/registration-provider.selectors.ts
@@ -13,4 +13,9 @@ export class RegistrationProviderSelectors {
static isBrandedProviderLoading(state: RegistrationProviderStateModel) {
return state.currentBrandedProvider.isLoading;
}
+
+ @Selector([RegistrationProviderState])
+ static allowUpdates(state: RegistrationProviderStateModel) {
+ return state.currentBrandedProvider.data?.allowUpdates ?? false;
+ }
}