diff --git a/.eslintrc.json b/.eslintrc.json index af1b97849b..6920cc4712 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -231,10 +231,13 @@ "*.json5" ], "extends": [ - "plugin:jsonc/recommended-with-jsonc" + "plugin:jsonc/recommended-with-json5" ], "rules": { - "no-irregular-whitespace": "error", + // The ESLint core no-irregular-whitespace rule doesn't work well in JSON + // See: https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-irregular-whitespace.html + "no-irregular-whitespace": "off", + "jsonc/no-irregular-whitespace": "error", "no-trailing-spaces": "error", "jsonc/comma-dangle": [ "error", diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb641bea1e..a72d0d6c18 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -184,12 +184,115 @@ jobs: # Get homepage and verify that the tag includes "DSpace". # If it does, then SSR is working, as this tag is created by our MetadataService. # This step also prints entire HTML of homepage for easier debugging if grep fails. - - name: Verify SSR (server-side rendering) + - name: Verify SSR (server-side rendering) on Homepage run: | result=$(wget -O- -q http://127.0.0.1:4000/home) echo "$result" echo "$result" | grep -oE "]*>" | grep DSpace + # Get a specific community in our test data and verify that the "

" tag includes "Publications" (the community name). + # If it does, then SSR is working. + - name: Verify SSR on a Community page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/communities/0958c910-2037-42a9-81c7-dca80e3892b4) + echo "$result" + echo "$result" | grep -oE "

]*>[^><]*

" | grep Publications + + # Get a specific collection in our test data and verify that the "

" tag includes "Articles" (the collection name). + # If it does, then SSR is working. + - name: Verify SSR on a Collection page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/collections/282164f5-d325-4740-8dd1-fa4d6d3e7200) + echo "$result" + echo "$result" | grep -oE "

]*>[^><]*

" | grep Articles + + # Get a specific publication in our test data and verify that the tag includes + # the title of this publication. If it does, then SSR is working. + - name: Verify SSR on a Publication page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/publication/6160810f-1e53-40db-81ef-f6621a727398) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "An Economic Model of Mortality Salience" + + # Get a specific person in our test data and verify that the tag includes + # the name of the person. If it does, then SSR is working. + - name: Verify SSR on a Person page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/person/b1b2c768-bda1-448a-a073-fc541e8b24d9) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "Simmons, Cameron" + + # Get a specific project in our test data and verify that the tag includes + # the name of the project. If it does, then SSR is working. + - name: Verify SSR on a Project page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/project/46ccb608-a74c-4bf6-bc7a-e29cc7defea9) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "University Research Fellowship" + + # Get a specific orgunit in our test data and verify that the tag includes + # the name of the orgunit. If it does, then SSR is working. + - name: Verify SSR on an OrgUnit page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/orgunit/9851674d-bd9a-467b-8d84-068deb568ccf) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "Law and Development" + + # Get a specific journal in our test data and verify that the tag includes + # the name of the journal. If it does, then SSR is working. + - name: Verify SSR on a Journal page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/journal/d4af6c3e-53d0-4757-81eb-566f3b45d63a) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "Environmental & Architectural Phenomenology" + + # Get a specific journal volume in our test data and verify that the tag includes + # the name of the volume. If it does, then SSR is working. + - name: Verify SSR on a Journal Volume page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/journalvolume/07c6249f-4bf7-494d-9ce3-6ffdb2aed538) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "Environmental & Architectural Phenomenology Volume 28 (2017)" + + # Get a specific journal issue in our test data and verify that the tag includes + # the name of the issue. If it does, then SSR is working. + - name: Verify SSR on a Journal Issue page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/journalissue/44c29473-5de2-48fa-b005-e5029aa1a50b) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "Environmental & Architectural Phenomenology Vol. 28, No. 1" + + # Verify 301 Handle redirect behavior + # Note: /handle/123456789/260 is the same test Publication used by our e2e tests + - name: Verify 301 redirect from '/handle' URLs + run: | + result=$(wget --server-response --quiet http://127.0.0.1:4000/handle/123456789/260 2>&1 | head -1 | awk '{print $2}') + echo "$result" + [[ "$result" -eq "301" ]] + + # Verify 403 error code behavior + - name: Verify 403 error code from '/403' + run: | + result=$(wget --server-response --quiet http://127.0.0.1:4000/403 2>&1 | head -1 | awk '{print $2}') + echo "$result" + [[ "$result" -eq "403" ]] + + # Verify 404 error code behavior + - name: Verify 404 error code from '/404' and on invalid pages + run: | + result=$(wget --server-response --quiet http://127.0.0.1:4000/404 2>&1 | head -1 | awk '{print $2}') + echo "$result" + result2=$(wget --server-response --quiet http://127.0.0.1:4000/invalidurl 2>&1 | head -1 | awk '{print $2}') + echo "$result2" + [[ "$result" -eq "404" && "$result2" -eq "404" ]] + + # Verify 500 error code behavior + - name: Verify 500 error code from '/500' + run: | + result=$(wget --server-response --quiet http://127.0.0.1:4000/500 2>&1 | head -1 | awk '{print $2}') + echo "$result" + [[ "$result" -eq "500" ]] + - name: Stop running app run: kill -9 $(lsof -t -i:4000) diff --git a/config/config.example.yml b/config/config.example.yml index a1b2f3f579..c82df9e3b2 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -23,10 +23,24 @@ universal: # Determining which styles are critical is a relatively expensive operation; this option is # disabled (false) by default to boost server performance at the expense of loading smoothness. inlineCriticalCss: false - # Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects. - # NOTE: The "/handle/" path ensures Handle redirects work via SSR. The "/reload/" path ensures - # hard refreshes (e.g. after login) trigger SSR while fully reloading the page. - paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ] + # Patterns to be run as regexes against the path of the page to check if SSR is allowed. + # If the path match any of the regexes it will be served directly in CSR. + # By default, excludes community and collection browse, global browse, global search, community list, statistics and various administrative tools. + excludePathPatterns: + - pattern: "^/communities/[a-f0-9-]{36}/browse(/.*)?$" + flag: "i" + - pattern: "^/collections/[a-f0-9-]{36}/browse(/.*)?$" + flag: "i" + - pattern: "^/browse/" + - pattern: "^/search$" + - pattern: "^/community-list$" + - pattern: "^/admin/" + - pattern: "^/processes/?" + - pattern: "^/notifications/" + - pattern: "^/statistics/?" + - pattern: "^/access-control/" + - pattern: "^/health$" + # Whether to enable rendering of Search component on SSR. # If set to true the component will be included in the HTML returned from the server side rendering. # If set to false the component will not be included in the HTML returned from the server side rendering. diff --git a/package.json b/package.json index 1ab4e54262..1a625edf21 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@angular/platform-browser-dynamic": "^15.2.10", "@angular/platform-server": "^15.2.10", "@angular/router": "^15.2.10", - "@babel/runtime": "7.26.7", + "@babel/runtime": "7.27.6", "@kolkov/ngx-gallery": "^2.0.1", "@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-dynamic-forms/core": "^15.0.0", @@ -73,14 +73,14 @@ "@nicky-lenaers/ngx-scroll-to": "^14.0.0", "angular-idle-preload": "3.0.0", "angulartics2": "^12.2.1", - "axios": "^1.7.9", + "axios": "^1.10.0", "bootstrap": "^4.6.1", "cerialize": "0.1.18", "cli-progress": "^3.12.0", "colors": "^1.4.0", - "compression": "^1.7.5", + "compression": "^1.8.0", "cookie-parser": "1.4.7", - "core-js": "^3.40.0", + "core-js": "^3.42.0", "date-fns": "^2.30.0", "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", @@ -89,9 +89,9 @@ "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.1.1", "filesize": "^6.1.0", - "http-proxy-middleware": "^2.0.7", + "http-proxy-middleware": "^2.0.9", "http-terminator": "^3.2.0", - "isbot": "^5.1.22", + "isbot": "^5.1.28", "js-cookie": "2.2.1", "js-yaml": "^4.1.0", "json5": "^2.2.3", @@ -116,8 +116,8 @@ "nouislider": "^15.8.1", "pem": "1.14.8", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.0", - "sanitize-html": "^2.14.0", + "rxjs": "^7.8.2", + "sanitize-html": "^2.17.0", "sortablejs": "1.15.6", "uuid": "^8.3.2", "zone.js": "~0.13.3" @@ -146,12 +146,12 @@ "@types/grecaptcha": "^3.0.9", "@types/jasmine": "~3.6.0", "@types/js-cookie": "2.2.6", - "@types/lodash": "^4.17.15", + "@types/lodash": "^4.17.17", "@types/node": "^14.18.63", - "@types/sanitize-html": "^2.13.0", + "@types/sanitize-html": "^2.16.0", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", - "axe-core": "^4.10.2", + "axe-core": "^4.10.3", "compression-webpack-plugin": "^9.2.0", "copy-webpack-plugin": "^6.4.1", "cross-env": "^7.0.3", @@ -163,7 +163,7 @@ "eslint-plugin-deprecation": "^1.5.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^45.0.0", - "eslint-plugin-jsonc": "^2.19.1", + "eslint-plugin-jsonc": "^2.20.1", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-unused-imports": "^2.0.0", "express-static-gzip": "^2.2.0", @@ -175,7 +175,7 @@ "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "karma-mocha-reporter": "2.2.5", - "ng-mocks": "^14.13.2", + "ng-mocks": "^14.13.5", "ngx-mask": "^13.1.7", "nodemon": "^2.0.22", "postcss": "^8.5", @@ -187,7 +187,7 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "^16.14.0", "rimraf": "^3.0.2", - "sass": "~1.84.0", + "sass": "~1.89.1", "sass-loader": "^12.6.0", "sass-resources-loader": "^2.2.5", "ts-node": "^8.10.2", diff --git a/server.ts b/server.ts index cfab230ef5..1aee5dc657 100644 --- a/server.ts +++ b/server.ts @@ -55,6 +55,7 @@ import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { logStartupMessage } from './startup-message'; import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model'; +import { SsrExcludePatterns } from './src/config/universal-config.interface'; /* @@ -241,7 +242,7 @@ export function app() { * The callback function to serve server side angular */ function ngApp(req, res) { - if (environment.universal.preboot && req.method === 'GET' && (req.path === '/' || environment.universal.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) { + if (environment.universal.preboot && req.method === 'GET' && (req.path === '/' || !isExcludedFromSsr(req.path, environment.universal.excludePathPatterns))) { // Render the page to user via SSR (server side rendering) serverSideRender(req, res); } else { @@ -625,6 +626,21 @@ function start() { } } +/** + * Check if SSR should be skipped for path + * + * @param path + * @param excludePathPattern + */ +function isExcludedFromSsr(path: string, excludePathPattern: SsrExcludePatterns[]): boolean { + const patterns = excludePathPattern.map(p => + new RegExp(p.pattern, p.flag || '') + ); + return patterns.some((regex) => { + return regex.test(path) + }); +} + /* * The callback function to serve health check requests */ diff --git a/src/app/access-control/bulk-access/bulk-access.component.spec.ts b/src/app/access-control/bulk-access/bulk-access.component.spec.ts index e9b253147d..4a545dc3dd 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.spec.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NO_ERRORS_SCHEMA, Component } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; @@ -57,10 +57,15 @@ describe('BulkAccessComponent', () => { 'file': { } }; - const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { - getValue: jasmine.createSpy('getValue'), - reset: jasmine.createSpy('reset') - }); + @Component({ + selector: 'ds-bulk-access-settings', + template: '' + }) + class MockBulkAccessSettingsComponent { + isFormValid = jasmine.createSpy('isFormValid').and.returnValue(false); + getValue = jasmine.createSpy('getValue'); + reset = jasmine.createSpy('reset'); + } const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }]; const selectableListState: SelectableListState = { id: 'test', selection }; const expectedIdList = ['1234', '5678']; @@ -73,7 +78,10 @@ describe('BulkAccessComponent', () => { RouterTestingModule, TranslateModule.forRoot() ], - declarations: [ BulkAccessComponent ], + declarations: [ + BulkAccessComponent, + MockBulkAccessSettingsComponent, + ], providers: [ { provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock }, { provide: NotificationsService, useValue: NotificationsServiceStub }, @@ -102,7 +110,6 @@ describe('BulkAccessComponent', () => { (component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListStateEmpty)); fixture.detectChanges(); - component.settings = mockSettings; }); it('should create', () => { @@ -119,13 +126,12 @@ describe('BulkAccessComponent', () => { }); - describe('when there are elements selected', () => { + describe('when there are elements selected and step two form is invalid', () => { beforeEach(() => { (component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState)); fixture.detectChanges(); - component.settings = mockSettings; }); it('should create', () => { @@ -136,9 +142,9 @@ describe('BulkAccessComponent', () => { expect(component.objectsSelected$.value).toEqual(expectedIdList); }); - it('should enable the execute button when there are objects selected', () => { + it('should not enable the execute button when there are objects selected and step two form is invalid', () => { component.objectsSelected$.next(['1234']); - expect(component.canExport()).toBe(true); + expect(component.canExport()).toBe(false); }); it('should call the settings reset method when reset is called', () => { @@ -146,6 +152,23 @@ describe('BulkAccessComponent', () => { expect(component.settings.reset).toHaveBeenCalled(); }); + + }); + + describe('when there are elements selectedted and the step two form is valid', () => { + + beforeEach(() => { + + (component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState)); + fixture.detectChanges(); + (component as any).settings.isFormValid.and.returnValue(true); + }); + + it('should enable the execute button when there are objects selected and step two form is valid', () => { + component.objectsSelected$.next(['1234']); + expect(component.canExport()).toBe(true); + }); + it('should call the bulkAccessControlService executeScript method when submit is called', () => { (component.settings as any).getValue.and.returnValue(mockFormState); bulkAccessControlService.createPayloadFile.and.returnValue(mockFile); diff --git a/src/app/access-control/bulk-access/bulk-access.component.ts b/src/app/access-control/bulk-access/bulk-access.component.ts index 04724614cb..bdea3d5cbe 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.ts @@ -37,7 +37,7 @@ export class BulkAccessComponent implements OnInit { constructor( private bulkAccessControlService: BulkAccessControlService, - private selectableListService: SelectableListService + private selectableListService: SelectableListService, ) { } @@ -51,7 +51,7 @@ export class BulkAccessComponent implements OnInit { } canExport(): boolean { - return this.objectsSelected$.value?.length > 0; + return this.objectsSelected$.value?.length > 0 && this.settings?.isFormValid(); } /** diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts index eecc016245..5d1070893c 100644 --- a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts @@ -31,4 +31,8 @@ export class BulkAccessSettingsComponent { this.controlForm.reset(); } + isFormValid() { + return this.controlForm.isValid(); + } + } diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.html b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.html index 44b6bfb697..a9fb368ef4 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.html +++ b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.html @@ -1,4 +1,4 @@ -
+

{{messagePrefix + '.create' | translate}}

diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts index f04324bdc5..c072d183aa 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts @@ -11,7 +11,7 @@ import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { take } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; -import { combineLatest } from 'rxjs'; +import { Observable } from 'rxjs'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; import { MetadataField } from '../../../../core/metadata/metadata-field.model'; @@ -90,6 +90,8 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy { */ @Output() submitForm: EventEmitter = new EventEmitter(); + activeMetadataField$: Observable; + constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) { @@ -99,70 +101,64 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy { * Initialize the component, setting up the necessary Models for the dynamic form */ ngOnInit() { - combineLatest([ - this.translateService.get(`${this.messagePrefix}.element`), - this.translateService.get(`${this.messagePrefix}.qualifier`), - this.translateService.get(`${this.messagePrefix}.scopenote`) - ]).subscribe(([element, qualifier, scopenote]) => { - this.element = new DynamicInputModel({ - id: 'element', - label: element, - name: 'element', - validators: { - required: null, - pattern: '^[^. ,]*$', - maxLength: 64, - }, - required: true, - errorMessages: { - pattern: 'error.validation.metadata.element.invalid-pattern', - maxLength: 'error.validation.metadata.element.max-length', - }, - }); - this.qualifier = new DynamicInputModel({ - id: 'qualifier', - label: qualifier, - name: 'qualifier', - validators: { - pattern: '^[^. ,]*$', - maxLength: 64, - }, - required: false, - errorMessages: { - pattern: 'error.validation.metadata.qualifier.invalid-pattern', - maxLength: 'error.validation.metadata.qualifier.max-length', - }, - }); - this.scopeNote = new DynamicTextAreaModel({ - id: 'scopeNote', - label: scopenote, - name: 'scopeNote', - required: false, - rows: 5, - }); - this.formModel = [ - new DynamicFormGroupModel( - { - id: 'metadatadatafieldgroup', - group:[this.element, this.qualifier, this.scopeNote] - }) - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - this.registryService.getActiveMetadataField().subscribe((field: MetadataField): void => { - if (field == null) { - this.clearFields(); - } else { - this.formGroup.patchValue({ - metadatadatafieldgroup: { - element: field.element, - qualifier: field.qualifier, - scopeNote: field.scopeNote, - }, - }); - this.element.disabled = true; - this.qualifier.disabled = true; - } - }); + this.element = new DynamicInputModel({ + id: 'element', + label: this.translateService.instant(`${this.messagePrefix}.element`), + name: 'element', + validators: { + required: null, + pattern: '^[^. ,]*$', + maxLength: 64, + }, + required: true, + errorMessages: { + pattern: 'error.validation.metadata.element.invalid-pattern', + maxLength: 'error.validation.metadata.element.max-length', + }, + }); + this.qualifier = new DynamicInputModel({ + id: 'qualifier', + label: this.translateService.instant(`${this.messagePrefix}.qualifier`), + name: 'qualifier', + validators: { + pattern: '^[^. ,]*$', + maxLength: 64, + }, + required: false, + errorMessages: { + pattern: 'error.validation.metadata.qualifier.invalid-pattern', + maxLength: 'error.validation.metadata.qualifier.max-length', + }, + }); + this.scopeNote = new DynamicTextAreaModel({ + id: 'scopeNote', + label: this.translateService.instant(`${this.messagePrefix}.scopenote`), + name: 'scopeNote', + required: false, + rows: 5, + }); + this.formModel = [ + new DynamicFormGroupModel( + { + id: 'metadatadatafieldgroup', + group:[this.element, this.qualifier, this.scopeNote] + }) + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.registryService.getActiveMetadataField().subscribe((field: MetadataField): void => { + if (field == null) { + this.clearFields(); + } else { + this.formGroup.patchValue({ + metadatadatafieldgroup: { + element: field.element, + qualifier: field.qualifier, + scopeNote: field.scopeNote, + }, + }); + this.element.disabled = true; + this.qualifier.disabled = true; + } }); } diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts index 8e4f13b164..df938fb6c9 100644 --- a/src/app/admin/admin-routing.module.ts +++ b/src/app/admin/admin-routing.module.ts @@ -1,13 +1,13 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component'; -import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; +import { ThemedAdminSearchPageComponent } from './admin-search-page/themed-admin-search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; import { REGISTRIES_MODULE_PATH } from './admin-routing-paths'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; +import { ThemedAdminWorkflowPageComponent } from './admin-workflow-page/themed-admin-workflow-page.component'; @NgModule({ imports: [ @@ -20,13 +20,13 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import { path: 'search', resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: AdminSearchPageComponent, + component: ThemedAdminSearchPageComponent, data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' } }, { path: 'workflow', resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: AdminWorkflowPageComponent, + component: ThemedAdminWorkflowPageComponent, data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' } }, { diff --git a/src/app/admin/admin-search-page/admin-search.module.ts b/src/app/admin/admin-search-page/admin-search.module.ts index 353d6dd498..b45eca15c4 100644 --- a/src/app/admin/admin-search-page/admin-search.module.ts +++ b/src/app/admin/admin-search-page/admin-search.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '../../shared/shared.module'; +import { ThemedAdminSearchPageComponent } from './themed-admin-search-page.component'; import { AdminSearchPageComponent } from './admin-search-page.component'; import { ItemAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component'; import { CommunityAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component'; @@ -31,6 +32,7 @@ const ENTRY_COMPONENTS = [ ResearchEntitiesModule.withEntryComponents() ], declarations: [ + ThemedAdminSearchPageComponent, AdminSearchPageComponent, ...ENTRY_COMPONENTS ] diff --git a/src/app/admin/admin-search-page/themed-admin-search-page.component.ts b/src/app/admin/admin-search-page/themed-admin-search-page.component.ts new file mode 100644 index 0000000000..741a3b04f9 --- /dev/null +++ b/src/app/admin/admin-search-page/themed-admin-search-page.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { AdminSearchPageComponent } from './admin-search-page.component'; + +/** + * Themed wrapper for {@link AdminSearchPageComponent} + */ +@Component({ + selector: 'ds-themed-admin-search-page', + templateUrl: '../../shared/theme-support/themed.component.html', +}) +export class ThemedAdminSearchPageComponent extends ThemedComponent { + + protected getComponentName(): string { + return 'AdminSearchPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/admin/admin-search-page/admin-search-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./admin-search-page.component'); + } + +} diff --git a/src/app/admin/admin-workflow-page/admin-workflow.module.ts b/src/app/admin/admin-workflow-page/admin-workflow.module.ts index 21990c1ea9..1de73dee53 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow.module.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow.module.ts @@ -27,6 +27,7 @@ import { import { SupervisionOrderStatusComponent } from './admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component'; +import { ThemedAdminWorkflowPageComponent } from './themed-admin-workflow-page.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -42,6 +43,7 @@ const ENTRY_COMPONENTS = [ SharedModule.withEntryComponents() ], declarations: [ + ThemedAdminWorkflowPageComponent, AdminWorkflowPageComponent, SupervisionOrderGroupSelectorComponent, SupervisionOrderStatusComponent, diff --git a/src/app/admin/admin-workflow-page/themed-admin-workflow-page.component.ts b/src/app/admin/admin-workflow-page/themed-admin-workflow-page.component.ts new file mode 100644 index 0000000000..fe84c44d0e --- /dev/null +++ b/src/app/admin/admin-workflow-page/themed-admin-workflow-page.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { AdminWorkflowPageComponent } from './admin-workflow-page.component'; + +/** + * Themed wrapper for {@link AdminWorkflowPageComponent} + */ +@Component({ + selector: 'ds-themed-admin-workflow-page', + templateUrl: '../../shared/theme-support/themed.component.html', +}) +export class ThemedAdminWorkflowPageComponent extends ThemedComponent { + + protected getComponentName(): string { + return 'AdminWorkflowPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/admin/admin-workflow-page/admin-workflow-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./admin-workflow-page.component'); + } + +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index deb68f1ea9..b81201dd01 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -40,6 +40,8 @@ import { import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { MenuResolver } from './menu.resolver'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; +import { HomePageResolver } from './home-page/home-page.resolver'; +import { ViewTrackerResolverService } from './statistics/angulartics/dspace/view-tracker-resolver.service'; @NgModule({ imports: [ @@ -63,7 +65,15 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone path: 'home', loadChildren: () => import('./home-page/home-page.module') .then((m) => m.HomePageModule), - data: { showBreadcrumbs: false }, + data: { + showBreadcrumbs: false, + dsoPath: 'site' + }, + resolve: { + site: HomePageResolver, + tracking: ViewTrackerResolverService, + }, + canActivate: [EndUserAgreementCurrentUserGuard] }, { @@ -251,6 +261,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone }) ], exports: [RouterModule], + providers: [HomePageResolver, ViewTrackerResolverService], }) export class AppRoutingModule { diff --git a/src/app/breadcrumbs/breadcrumbs.component.html b/src/app/breadcrumbs/breadcrumbs.component.html index 3bba89622f..1d95c39cc0 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.html +++ b/src/app/breadcrumbs/breadcrumbs.component.html @@ -10,7 +10,7 @@ - + diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html index c24ca93403..6a46de108c 100644 --- a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html +++ b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html @@ -10,6 +10,8 @@ + [queryParamsHandling]="'merge'" + role="link" + tabindex="0"> {{ 'browse.taxonomy.button' | translate }} diff --git a/src/app/collection-page/collection-page-routing.module.ts b/src/app/collection-page/collection-page-routing.module.ts index 56305d86c0..7d657e52db 100644 --- a/src/app/collection-page/collection-page-routing.module.ts +++ b/src/app/collection-page/collection-page-routing.module.ts @@ -23,6 +23,7 @@ import { ThemedCollectionPageComponent } from './themed-collection-page.componen import { MenuItemType } from '../shared/menu/menu-item-type.model'; import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; +import { ViewTrackerResolverService } from '../statistics/angulartics/dspace/view-tracker-resolver.service'; @NgModule({ imports: [ @@ -86,6 +87,7 @@ import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-bread pathMatch: 'full', resolve: { menu: DSOEditMenuResolver, + tracking: ViewTrackerResolverService, }, } ], @@ -116,6 +118,7 @@ import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-bread CreateCollectionPageGuard, CollectionPageAdministratorGuard, CommunityBreadcrumbResolver, + ViewTrackerResolverService, ] }) export class CollectionPageRoutingModule { diff --git a/src/app/collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html index 9a5414952f..ec8da3c7e7 100644 --- a/src/app/collection-page/collection-page.component.html +++ b/src/app/collection-page/collection-page.component.html @@ -3,7 +3,6 @@ *ngVar="(collectionRD$ | async) as collectionRD">
-
diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index 7ccf24a761..268c92dc12 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -9,7 +9,7 @@
@@ -27,7 +27,11 @@ @@ -74,15 +74,15 @@
  • {{ 'footer.link.privacy-policy' | translate}} + routerLink="info/privacy" role="link" tabindex="0">{{ 'footer.link.privacy-policy' | translate}}
  • {{ 'footer.link.end-user-agreement' | translate}} + routerLink="info/end-user-agreement" role="link" tabindex="0">{{ 'footer.link.end-user-agreement' | translate}}
  • {{ 'footer.link.feedback' | translate}} + routerLink="info/feedback" role="link" tabindex="0">{{ 'footer.link.feedback' | translate}}
  • diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index e98959d162..9fc398c64e 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -1,7 +1,7 @@
    - + diff --git a/src/app/home-page/home-news/home-news.component.html b/src/app/home-page/home-news/home-news.component.html index 8d3f99b60d..c4dbd191b2 100644 --- a/src/app/home-page/home-news/home-news.component.html +++ b/src/app/home-page/home-news/home-news.component.html @@ -14,7 +14,7 @@
  • issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI
  • Join an international community of leading institutions using DSpace. + target="_blank" role="link" tabindex="0">leading institutions using DSpace.

    diff --git a/src/app/home-page/home-page-routing.module.ts b/src/app/home-page/home-page-routing.module.ts index 196a290552..b95a258613 100644 --- a/src/app/home-page/home-page-routing.module.ts +++ b/src/app/home-page/home-page-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { HomePageResolver } from './home-page.resolver'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { ThemedHomePageComponent } from './themed-home-page.component'; import { MenuItemType } from '../shared/menu/menu-item-type.model'; @@ -28,15 +27,9 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model'; } as LinkMenuItemModel, }], }, - }, - resolve: { - site: HomePageResolver } } ]) - ], - providers: [ - HomePageResolver ] }) export class HomePageRoutingModule { diff --git a/src/app/home-page/home-page.component.html b/src/app/home-page/home-page.component.html index caa86ac290..e217e41a37 100644 --- a/src/app/home-page/home-page.component.html +++ b/src/app/home-page/home-page.component.html @@ -1,8 +1,5 @@
    - - - diff --git a/src/app/home-page/recent-item-list/recent-item-list.component.html b/src/app/home-page/recent-item-list/recent-item-list.component.html index 82262be1b3..c70971c8d8 100644 --- a/src/app/home-page/recent-item-list/recent-item-list.component.html +++ b/src/app/home-page/recent-item-list/recent-item-list.component.html @@ -6,7 +6,7 @@
    - +
    diff --git a/src/app/item-page/alerts/item-alerts.component.html b/src/app/item-page/alerts/item-alerts.component.html index cd71d23a91..b9b30f283b 100644 --- a/src/app/item-page/alerts/item-alerts.component.html +++ b/src/app/item-page/alerts/item-alerts.component.html @@ -6,7 +6,7 @@
    diff --git a/src/app/item-page/edit-item-page/edit-item-page.component.html b/src/app/item-page/edit-item-page/edit-item-page.component.html index 63dadef3b1..f697e0b8f4 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.component.html +++ b/src/app/item-page/edit-item-page/edit-item-page.component.html @@ -3,7 +3,7 @@

    {{'item.edit.head' | translate}}

    + *ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id).bitstreams | async)"> diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts index 5d2afbaf4c..3269a91118 100644 --- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts @@ -147,17 +147,9 @@ describe('ItemAuthorizationsComponent test suite', () => { })); }); - it('should get the item UUID', () => { - - expect(comp.getItemUUID()).toBeObservable(cold('(a|)', { - a: item.id - })); - - }); - it('should get the item\'s bundle', () => { - expect(comp.getItemBundles()).toBeObservable(cold('a', { + expect(comp.bundles$).toBeObservable(cold('a', { a: bundles })); diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index 635cf455b5..cd82f5182f 100644 --- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -4,7 +4,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; -import { catchError, filter, first, map, mergeMap, take } from 'rxjs/operators'; +import { catchError, filter, map, mergeMap, take } from 'rxjs/operators'; import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; import { @@ -17,6 +17,7 @@ import { LinkService } from '../../../core/cache/builders/link.service'; import { Bundle } from '../../../core/shared/bundle.model'; import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { Bitstream } from '../../../core/shared/bitstream.model'; +import { AlertType } from '../../../shared/alert/alert-type'; /** * Interface for a bundle's bitstream map entry @@ -52,7 +53,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * The target editing item * @type {Observable} */ - private item$: Observable; + item$: Observable; /** * Array to track all subscriptions and unsubscribe them onDestroy @@ -91,16 +92,13 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { */ private bitstreamPageSize = 4; - /** - * Initialize instance variables - * - * @param {LinkService} linkService - * @param {ActivatedRoute} route - * @param nameService - */ + itemName$: Observable; + + readonly AlertType = AlertType; + constructor( - private linkService: LinkService, - private route: ActivatedRoute, + protected linkService: LinkService, + protected route: ActivatedRoute, public nameService: DSONameService ) { } @@ -109,37 +107,19 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * Initialize the component, setting up the bundle and bitstream within the item */ ngOnInit(): void { - this.getBundlesPerItem(); + this.getBundlesPerItem(); + this.itemName$ = this.getItemName(); } /** - * Return the item's UUID + * Return the item's name */ - getItemUUID(): Observable { - return this.item$.pipe( - map((item: Item) => item.id), - first((UUID: string) => isNotEmpty(UUID)) - ); - } - - /** - * Return the item's name - */ - getItemName(): Observable { + private getItemName(): Observable { return this.item$.pipe( map((item: Item) => this.nameService.getName(item)) ); } - /** - * Return all item's bundles - * - * @return an observable that emits all item's bundles - */ - getItemBundles(): Observable { - return this.bundles$.asObservable(); - } - /** * Get all bundles per item * and all the bitstreams per bundle diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 581b8b4bcb..6d52ef5b4a 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, HostListener } from '@angular/core'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { map, switchMap, take } from 'rxjs/operators'; -import { Observable, Subscription, combineLatest, BehaviorSubject, tap } from 'rxjs'; +import { Observable, Subscription, combineLatest, BehaviorSubject } from 'rxjs'; import { ItemDataService } from '../../../core/data/item-data.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ActivatedRoute, Router } from '@angular/router'; @@ -187,15 +187,28 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: this.bundlesOptions})).pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), - tap((bundlesPL: PaginatedList) => - this.showLoadMoreLink$.next(bundlesPL.pageInfo.currentPage < bundlesPL.pageInfo.totalPages) - ), - map((bundlePage: PaginatedList) => bundlePage.page), - ).subscribe((bundles: Bundle[]) => { - this.bundlesSubject.next([...this.bundlesSubject.getValue(), ...bundles]); + ).subscribe((bundles: PaginatedList) => { + this.updateBundles(bundles); }); } + /** + * Update the subject containing the bundles with the provided bundles. + * Also updates the showLoadMoreLink observable so it does not show up when it is no longer necessary. + */ + updateBundles(newBundlesPL: PaginatedList) { + const currentBundles = this.bundlesSubject.getValue(); + + // Only add bundles to the bundle subject if they are not present yet + const bundlesToAdd = newBundlesPL.page + .filter(bundleToAdd => !currentBundles.some(currentBundle => currentBundle.id === bundleToAdd.id)); + + const updatedBundles = [...currentBundles, ...bundlesToAdd]; + + this.showLoadMoreLink$.next(updatedBundles.length < newBundlesPL.totalElements); + this.bundlesSubject.next(updatedBundles); + } + /** * Submit the current changes diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index a751a89e1b..6351c86934 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -203,8 +203,8 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { switchMap(() => this.bundleService.getBitstreams( this.bundle.id, paginatedOptions, - followLink('format') - )) + followLink('format'), + )), ); }), getAllSucceededRemoteData(), diff --git a/src/app/item-page/field-components/collections/collections.component.html b/src/app/item-page/field-components/collections/collections.component.html index 27ebb41d61..dfa22237ae 100644 --- a/src/app/item-page/field-components/collections/collections.component.html +++ b/src/app/item-page/field-components/collections/collections.component.html @@ -1,6 +1,6 @@ @@ -15,6 +15,7 @@ class="load-more-btn btn btn-sm btn-outline-secondary" role="button" href="javascript:void(0);" + tabindex="0" > {{'item.page.collections.load-more' | translate}} diff --git a/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.html b/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.html index 7d7174536b..5e575e63b5 100644 --- a/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.html +++ b/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.html @@ -1,5 +1,5 @@ - + {{ linktext || mdValue.value }} diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.html b/src/app/item-page/field-components/metadata-values/metadata-values.component.html index a9576da26a..de6f413994 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.html +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.html @@ -21,7 +21,9 @@ @@ -35,5 +37,5 @@ {{value}} + [queryParams]="getQueryParams(value)" role="link" tabindex="0">{{value}} diff --git a/src/app/item-page/full/full-item-page.component.html b/src/app/item-page/full/full-item-page.component.html index 1d83181395..39f682113a 100644 --- a/src/app/item-page/full/full-item-page.component.html +++ b/src/app/item-page/full/full-item-page.component.html @@ -3,7 +3,6 @@
    -
    diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts index 5fb11f7056..d116eb9297 100644 --- a/src/app/item-page/item-page-routing.module.ts +++ b/src/app/item-page/item-page-routing.module.ts @@ -19,6 +19,7 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths'; import { OrcidPageComponent } from './orcid-page/orcid-page.component'; import { OrcidPageGuard } from './orcid-page/orcid-page.guard'; import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; +import { ViewTrackerResolverService } from '../statistics/angulartics/dspace/view-tracker-resolver.service'; @NgModule({ imports: [ @@ -37,6 +38,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; pathMatch: 'full', resolve: { menu: DSOEditMenuResolver, + tracking: ViewTrackerResolverService, }, }, { @@ -44,6 +46,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; component: ThemedFullItemPageComponent, resolve: { menu: DSOEditMenuResolver, + tracking: ViewTrackerResolverService, }, }, { @@ -103,7 +106,8 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; LinkService, ItemPageAdministratorGuard, VersionResolver, - OrcidPageGuard + OrcidPageGuard, + ViewTrackerResolverService, ] }) diff --git a/src/app/item-page/media-viewer/media-viewer.component.html b/src/app/item-page/media-viewer/media-viewer.component.html index c8a02e039c..eb491c5193 100644 --- a/src/app/item-page/media-viewer/media-viewer.component.html +++ b/src/app/item-page/media-viewer/media-viewer.component.html @@ -16,12 +16,7 @@
    - - - + + diff --git a/src/app/item-page/media-viewer/media-viewer.component.spec.ts b/src/app/item-page/media-viewer/media-viewer.component.spec.ts index 0c170ac8cf..6de0d82595 100644 --- a/src/app/item-page/media-viewer/media-viewer.component.spec.ts +++ b/src/app/item-page/media-viewer/media-viewer.component.spec.ts @@ -139,9 +139,9 @@ describe('MediaViewerComponent', () => { expect(mediaItem.thumbnail).toBe(null); }); - it('should display a default, thumbnail', () => { + it('should display a default thumbnail', () => { const defaultThumbnail = fixture.debugElement.query( - By.css('ds-themed-media-viewer-image') + By.css('ds-themed-thumbnail') ); expect(defaultThumbnail.nativeElement).toBeDefined(); }); diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html index c37b54aab3..0ba2c87011 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html @@ -1,18 +1,18 @@

    {{'person.orcid.registry.auth' | translate}}

    - +
    -
    +
    {{ 'person.page.orcid.granted-authorizations'| translate }}
      -
    • +
    • {{getAuthorizationDescription(auth) | translate}}
    @@ -25,13 +25,13 @@
    {{ 'person.page.orcid.missing-authorizations'| translate }}
    - + {{'person.page.orcid.no-missing-authorizations-message' | translate}} - + {{'person.page.orcid.missing-authorizations-message' | translate}}
      -
    • +
    • {{getAuthorizationDescription(auth) | translate }}
    @@ -41,11 +41,11 @@
    - + {{ 'person.page.orcid.remove-orcid-message' | translate}} -
    +
    - @@ -68,7 +68,7 @@
    orcid-logo
    - {{ getOrcidNotLinkedMessage() | async }} + {{ getOrcidNotLinkedMessage() }}
    diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts index 73b4a7b4e1..59da0d4fe6 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts @@ -12,6 +12,7 @@ import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service'; import { createFailedRemoteDataObject } from '../../../shared/remote-data.utils'; import { HttpErrorResponse } from '@angular/common/http'; +import { AlertType } from '../../../shared/alert/alert-type'; @Component({ selector: 'ds-orcid-auth', @@ -28,43 +29,49 @@ export class OrcidAuthComponent implements OnInit, OnChanges { /** * The list of exposed orcid authorization scopes for the orcid profile */ - profileAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + profileAuthorizationScopes$: BehaviorSubject = new BehaviorSubject([]); + + hasOrcidAuthorizations$: Observable; /** * The list of all orcid authorization scopes missing in the orcid profile */ - missingAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + missingAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + + hasMissingOrcidAuthorizations$: Observable; /** * The list of all orcid authorization scopes available */ - orcidAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + orcidAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); /** * A boolean representing if unlink operation is processing */ - unlinkProcessing: BehaviorSubject = new BehaviorSubject(false); + unlinkProcessing: BehaviorSubject = new BehaviorSubject(false); /** * A boolean representing if orcid profile is linked */ - private isOrcidLinked$: BehaviorSubject = new BehaviorSubject(false); + isOrcidLinked$: BehaviorSubject = new BehaviorSubject(false); /** * A boolean representing if only admin can disconnect orcid profile */ - private onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); + onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); /** * A boolean representing if owner can disconnect orcid profile */ - private ownerCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); + ownerCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); /** * An event emitted when orcid profile is unliked successfully */ @Output() unlink: EventEmitter = new EventEmitter(); + readonly AlertType = AlertType; + constructor( private orcidAuthService: OrcidAuthService, private translateService: TranslateService, @@ -78,6 +85,8 @@ export class OrcidAuthComponent implements OnInit, OnChanges { this.orcidAuthorizationScopes.next(scopes); this.initOrcidAuthSettings(); }); + this.hasOrcidAuthorizations$ = this.hasOrcidAuthorizations(); + this.hasMissingOrcidAuthorizations$ = this.hasMissingOrcidAuthorizations(); } ngOnChanges(changes: SimpleChanges): void { @@ -90,18 +99,11 @@ export class OrcidAuthComponent implements OnInit, OnChanges { * Check if the list of exposed orcid authorization scopes for the orcid profile has values */ hasOrcidAuthorizations(): Observable { - return this.profileAuthorizationScopes.asObservable().pipe( + return this.profileAuthorizationScopes$.pipe( map((scopes: string[]) => scopes.length > 0) ); } - /** - * Return the list of exposed orcid authorization scopes for the orcid profile - */ - getOrcidAuthorizations(): Observable { - return this.profileAuthorizationScopes.asObservable(); - } - /** * Check if the list of exposed orcid authorization scopes for the orcid profile has values */ @@ -111,26 +113,12 @@ export class OrcidAuthComponent implements OnInit, OnChanges { ); } - /** - * Return the list of exposed orcid authorization scopes for the orcid profile - */ - getMissingOrcidAuthorizations(): Observable { - return this.profileAuthorizationScopes.asObservable(); - } - - /** - * Return a boolean representing if orcid profile is linked - */ - isLinkedToOrcid(): Observable { - return this.isOrcidLinked$.asObservable(); - } - - getOrcidNotLinkedMessage(): Observable { + getOrcidNotLinkedMessage(): string { const orcid = this.item.firstMetadataValue('person.identifier.orcid'); if (orcid) { - return this.translateService.get('person.page.orcid.orcid-not-linked-message', { 'orcid': orcid }); + return this.translateService.instant('person.page.orcid.orcid-not-linked-message', { 'orcid': orcid }); } else { - return this.translateService.get('person.page.orcid.no-orcid-message'); + return this.translateService.instant('person.page.orcid.no-orcid-message'); } } @@ -143,13 +131,6 @@ export class OrcidAuthComponent implements OnInit, OnChanges { return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-'); } - /** - * Return a boolean representing if only admin can disconnect orcid profile - */ - onlyAdminCanDisconnectProfileFromOrcid(): Observable { - return this.onlyAdminCanDisconnectProfileFromOrcid$.asObservable(); - } - /** * Return a boolean representing if owner can disconnect orcid profile */ @@ -215,7 +196,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges { } private setOrcidAuthorizationsFromItem(): void { - this.profileAuthorizationScopes.next(this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item)); + this.profileAuthorizationScopes$.next(this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item)); } } diff --git a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html index 6ba318f7fd..9b3e470fc1 100644 --- a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html +++ b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html @@ -3,13 +3,13 @@

    {{ 'person.orcid.registry.queue' | translate }}

    - {{ 'person.page.orcid.sync-queue.empty-message' | translate}} -
    @@ -22,7 +22,7 @@ - + diff --git a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts index 3e88826952..7df84864c6 100644 --- a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts +++ b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts @@ -47,13 +47,12 @@ export class OrcidQueueComponent implements OnInit, OnDestroy { /** * A list of orcid queue records */ - private list$: BehaviorSubject>> = new BehaviorSubject>>({} as any); + list$: BehaviorSubject>> = new BehaviorSubject>>({} as any); /** * The AlertType enumeration - * @type {AlertType} */ - AlertTypeEnum = AlertType; + readonly AlertTypeEnum = AlertType; /** * Array to track all subscriptions and unsubscribe them onDestroy @@ -99,13 +98,6 @@ export class OrcidQueueComponent implements OnInit, OnDestroy { ); } - /** - * Return the list of orcid queue records - */ - getList(): Observable>> { - return this.list$.asObservable(); - } - /** * Return the icon class for the queue object type * diff --git a/src/app/item-page/simple/item-page.component.html b/src/app/item-page/simple/item-page.component.html index cc9983bb35..5e2dc63ee9 100644 --- a/src/app/item-page/simple/item-page.component.html +++ b/src/app/item-page/simple/item-page.component.html @@ -3,7 +3,6 @@
    -
    diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index 3749f63964..c886f5512d 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -85,7 +85,7 @@ diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html index 904b7e039c..e446fffbdb 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -71,7 +71,7 @@ diff --git a/src/app/lookup-by-id/objectnotfound/objectnotfound.component.html b/src/app/lookup-by-id/objectnotfound/objectnotfound.component.html index e1cf58b5b2..8b3e2ebded 100644 --- a/src/app/lookup-by-id/objectnotfound/objectnotfound.component.html +++ b/src/app/lookup-by-id/objectnotfound/objectnotfound.component.html @@ -3,6 +3,6 @@

    {{missingItem}}


    - {{"404.link.home-page" | translate}} + {{"404.link.home-page" | translate}}

    diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html index bda9815481..c4f0a612f3 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -1,5 +1,5 @@
    diff --git a/src/app/pagenotfound/pagenotfound.component.html b/src/app/pagenotfound/pagenotfound.component.html index e85316b0ec..dba2f9700e 100644 --- a/src/app/pagenotfound/pagenotfound.component.html +++ b/src/app/pagenotfound/pagenotfound.component.html @@ -5,6 +5,6 @@

    {{"404.help" | translate}}


    - {{"404.link.home-page" | translate}} + {{"404.link.home-page" | translate}}

    \ No newline at end of file diff --git a/src/app/process-page/overview/process-overview.component.html b/src/app/process-page/overview/process-overview.component.html index b904133b01..aa0fa32bac 100644 --- a/src/app/process-page/overview/process-overview.component.html +++ b/src/app/process-page/overview/process-overview.component.html @@ -71,16 +71,16 @@
    diff --git a/src/app/search-navbar/search-navbar.component.html b/src/app/search-navbar/search-navbar.component.html index e9b5f285fb..84ffb5b38d 100644 --- a/src/app/search-navbar/search-navbar.component.html +++ b/src/app/search-navbar/search-navbar.component.html @@ -7,7 +7,7 @@ [class.display]="searchExpanded ? 'inline-block' : 'none'" [tabIndex]="searchExpanded ? 0 : -1" [attr.data-test]="'header-search-box' | dsBrowserOnly"> - diff --git a/src/app/shared/access-control-form-container/access-control-array-form/access-control-array-form.component.ts b/src/app/shared/access-control-form-container/access-control-array-form/access-control-array-form.component.ts index 227de596ff..f91bae7858 100644 --- a/src/app/shared/access-control-form-container/access-control-array-form/access-control-array-form.component.ts +++ b/src/app/shared/access-control-form-container/access-control-array-form/access-control-array-form.component.ts @@ -119,6 +119,10 @@ export class AccessControlArrayFormComponent implements OnInit { return item.id; } + isValid() { + return this.ngForm.valid; + } + } diff --git a/src/app/shared/access-control-form-container/access-control-form-container.component.ts b/src/app/shared/access-control-form-container/access-control-form-container.component.ts index cddd1b1a29..ad3a3c0052 100644 --- a/src/app/shared/access-control-form-container/access-control-form-container.component.ts +++ b/src/app/shared/access-control-form-container/access-control-form-container.component.ts @@ -156,5 +156,9 @@ export class AccessControlFormContainerComponent impleme this.selectableListService.deselectAll(ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID); } + isValid() { + return this.bitstreamAccessCmp.isValid() || this.itemAccessCmp.isValid(); + } + } diff --git a/src/app/shared/access-control-form-container/item-access-control-select-bitstreams-modal/item-access-control-select-bitstreams-modal.component.html b/src/app/shared/access-control-form-container/item-access-control-select-bitstreams-modal/item-access-control-select-bitstreams-modal.component.html index 8cf0ecea38..88706e2df3 100644 --- a/src/app/shared/access-control-form-container/item-access-control-select-bitstreams-modal/item-access-control-select-bitstreams-modal.component.html +++ b/src/app/shared/access-control-form-container/item-access-control-select-bitstreams-modal/item-access-control-select-bitstreams-modal.component.html @@ -8,20 +8,19 @@
    diff --git a/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts b/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts index 8c5fc88137..f2ee4374a0 100644 --- a/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts +++ b/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts @@ -3,7 +3,7 @@ import { ChangeDetectorRef, Component, Injector, NO_ERRORS_SCHEMA } from '@angul import { of as observableOf } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; -import { cold } from 'jasmine-marbles'; +import { hot } from 'jasmine-marbles'; import uniqueId from 'lodash/uniqueId'; import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; @@ -22,14 +22,13 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../testing/pagination-service.stub'; -describe('EpersonGroupListComponent test suite', () => { +describe('EpersonGroupListComponent', () => { let comp: EpersonGroupListComponent; let compAsAny: any; let fixture: ComponentFixture; - let de; let groupService: any; let epersonService: any; - let paginationService; + let paginationService: PaginationServiceStub; const paginationOptions: PaginationComponentOptions = new PaginationComponentOptions(); paginationOptions.id = uniqueId('eperson-group-list-pagination-test'); @@ -91,7 +90,6 @@ describe('EpersonGroupListComponent test suite', () => { })); describe('', () => { - let testComp: TestComponent; let testFixture: ComponentFixture; // synchronous beforeEach @@ -101,7 +99,6 @@ describe('EpersonGroupListComponent test suite', () => { `; testFixture = createTestComponent(html, TestComponent) as ComponentFixture; - testComp = testFixture.componentInstance; }); afterEach(() => { @@ -129,7 +126,6 @@ describe('EpersonGroupListComponent test suite', () => { afterEach(() => { comp = null; compAsAny = null; - de = null; fixture.destroy(); }); @@ -147,16 +143,15 @@ describe('EpersonGroupListComponent test suite', () => { fixture.detectChanges(); - expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id); + expect(comp.entrySelectedId$.value).toBe(EPersonMock.id); }); it('should init the list of eperson', () => { epersonService.searchByScope.and.returnValue(observableOf(epersonPaginatedListRD)); fixture.detectChanges(); - expect(compAsAny.list$.value).toEqual(epersonPaginatedListRD); - expect(comp.getList()).toBeObservable(cold('a', { - a: epersonPaginatedListRD + expect(comp.list$).toBeObservable(hot('(a|)', { + a: epersonPaginatedList, })); }); @@ -165,23 +160,13 @@ describe('EpersonGroupListComponent test suite', () => { comp.emitSelect(EPersonMock); expect(comp.select.emit).toHaveBeenCalled(); - expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id); + expect(comp.entrySelectedId$.value).toBe(EPersonMock.id); }); - it('should return true when entry is selected', () => { - compAsAny.entrySelectedId.next(EPersonMock.id); + it('should return the entrySelectedId$ value', () => { + comp.entrySelectedId$.next(EPersonMock.id); - expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', { - a: true - })); - }); - - it('should return false when entry is not selected', () => { - compAsAny.entrySelectedId.next(''); - - expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', { - a: false - })); + expect(comp.entrySelectedId$.value).toBe(EPersonMock.id); }); }); @@ -199,7 +184,6 @@ describe('EpersonGroupListComponent test suite', () => { afterEach(() => { comp = null; compAsAny = null; - de = null; fixture.destroy(); }); @@ -217,16 +201,15 @@ describe('EpersonGroupListComponent test suite', () => { fixture.detectChanges(); - expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id); + expect(comp.entrySelectedId$.value).toBe(GroupMock.id); }); it('should init the list of group', () => { groupService.searchGroups.and.returnValue(observableOf(groupPaginatedListRD)); fixture.detectChanges(); - expect(compAsAny.list$.value).toEqual(groupPaginatedListRD); - expect(comp.getList()).toBeObservable(cold('a', { - a: groupPaginatedListRD + expect(comp.list$).toBeObservable(hot('(a|)', { + a: groupPaginatedList, })); }); @@ -235,27 +218,16 @@ describe('EpersonGroupListComponent test suite', () => { comp.emitSelect(GroupMock); expect(comp.select.emit).toHaveBeenCalled(); - expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id); + expect(comp.entrySelectedId$.value).toBe(GroupMock.id); }); - it('should return true when entry is selected', () => { - compAsAny.entrySelectedId.next(EPersonMock.id); + it('should return the entrySelectedId$ value', () => { + comp.entrySelectedId$.next(GroupMock.id); - expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', { - a: true - })); - }); - - it('should return false when entry is not selected', () => { - compAsAny.entrySelectedId.next(''); - - expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', { - a: false - })); + expect(comp.entrySelectedId$.value).toBe(GroupMock.id); }); it('should update list on search triggered', () => { - const options: PaginationComponentOptions = comp.paginationOptions; const event: SearchEvent = { scope: 'metadata', query: 'test' @@ -263,7 +235,7 @@ describe('EpersonGroupListComponent test suite', () => { spyOn(comp, 'updateList'); comp.onSearch(event); - expect(compAsAny.updateList).toHaveBeenCalledWith('metadata', 'test'); + expect(comp.updateList).toHaveBeenCalledWith('metadata', 'test'); }); }); }); diff --git a/src/app/shared/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/eperson-group-list/eperson-group-list.component.ts index 7cad7a9783..154533deda 100644 --- a/src/app/shared/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/eperson-group-list/eperson-group-list.component.ts @@ -1,14 +1,12 @@ import { Component, EventEmitter, Injector, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; import uniqueId from 'lodash/uniqueId'; -import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; -import { hasValue, isNotEmpty } from '../empty.util'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { EPERSON } from '../../core/eperson/models/eperson.resource-type'; import { GROUP } from '../../core/eperson/models/group.resource-type'; @@ -16,10 +14,12 @@ import { ResourceType } from '../../core/shared/resource-type'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../core/eperson/group-data.service'; import { fadeInOut } from '../animations/fade'; -import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { getAllCompletedRemoteData, getRemoteDataPayload } from '../../core/shared/operators'; import { PaginationService } from '../../core/pagination/pagination.service'; import { FindListOptions } from '../../core/data/find-list-options.model'; import { getDataServiceFor } from '../../core/data/base/data-service.decorator'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { Group } from '../../core/eperson/models/group.model'; export interface SearchEvent { scope: string; @@ -79,21 +79,13 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { /** * A list of eperson or group */ - private list$: BehaviorSubject>> = new BehaviorSubject>>({} as any); + list$: Observable>; /** * The eperson or group's id selected * @type {string} */ - private entrySelectedId: BehaviorSubject = new BehaviorSubject(''); - - /** - * Array to track all subscriptions and unsubscribe them onDestroy - * @type {Array} - */ - private subs: Subscription[] = []; - - private pageConfigSub: Subscription; + entrySelectedId$: BehaviorSubject = new BehaviorSubject(''); /** * Initialize instance variables and inject the properly DataService @@ -119,7 +111,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { this.paginationOptions.pageSize = 5; if (this.initSelected) { - this.entrySelectedId.next(this.initSelected); + this.entrySelectedId$.next(this.initSelected); } this.updateList(this.currentSearchScope, this.currentSearchQuery); @@ -133,28 +125,9 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { */ emitSelect(entry: DSpaceObject): void { this.select.emit(entry); - this.entrySelectedId.next(entry.id); + this.entrySelectedId$.next(entry.id); } - /** - * Return the list of eperson or group - */ - getList(): Observable>> { - return this.list$.asObservable(); - } - - /** - * Return a boolean representing if a table row is selected - * - * @return {boolean} - */ - isSelected(entry: DSpaceObject): Observable { - return this.entrySelectedId.asObservable().pipe( - map((selectedId) => isNotEmpty(selectedId) && selectedId === entry.id) - ); - } - - /** * Method called on search */ @@ -169,38 +142,26 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { * Retrieve a paginate list of eperson or group */ updateList(scope: string, query: string): void { - if (hasValue(this.pageConfigSub)) { - this.pageConfigSub.unsubscribe(); - } - this.pageConfigSub = this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) - .subscribe((paginationOptions) => { - const options: FindListOptions = Object.assign({}, new FindListOptions(), { + this.list$ = this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions).pipe( + switchMap((paginationOptions) => { + const options: FindListOptions = Object.assign(new FindListOptions(), { elementsPerPage: paginationOptions.pageSize, - currentPage: paginationOptions.currentPage - }); + currentPage: paginationOptions.currentPage, + }); - const search$: Observable>> = this.isListOfEPerson ? - (this.dataService as EPersonDataService).searchByScope(scope, query, options) : - (this.dataService as GroupDataService).searchGroups(query, options); - - this.subs.push(search$.pipe(getFirstCompletedRemoteData()) - .subscribe((list: RemoteData>) => { - if (hasValue(this.list$)) { - this.list$.next(list); - } - }) + return this.isListOfEPerson ? + (this.dataService as EPersonDataService).searchByScope(scope, query, options) : + (this.dataService as GroupDataService).searchGroups(query, options); + }), + getAllCompletedRemoteData(), + getRemoteDataPayload(), ); - }); } /** * Unsubscribe from all subscriptions */ ngOnDestroy(): void { - this.list$ = null; - this.subs - .filter((subscription) => hasValue(subscription)) - .forEach((subscription) => subscription.unsubscribe()); this.paginationService.clearPagination(this.paginationOptions.id); } diff --git a/src/app/shared/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index 59f255a652..2a01d9dbdc 100644 --- a/src/app/shared/file-download-link/file-download-link.component.html +++ b/src/app/shared/file-download-link/file-download-link.component.html @@ -1,8 +1,10 @@ - + [attr.aria-label]="('file-download-link.download' | translate) + dsoNameService.getName(bitstream)" + role="link" + tabindex="0"> diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html index 6a0e34f671..985d3187a6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html @@ -62,7 +62,7 @@
    @@ -46,6 +46,8 @@ [(ngModel)]="node.isSelected" [checked]="node.isSelected" (change)="onSelect(node.item)" + role="checkbox" + tabindex="0" > {{node.item.display}} @@ -55,7 +57,9 @@ [ngbTooltip]="node.item?.otherInformation?.note" [openDelay]="500" container="body" - (click)="onSelect(node.item)"> + (click)="onSelect(node.item)" + role="button" + tabindex="0"> {{node.item.display}} @@ -64,7 +68,9 @@ @@ -78,6 +84,8 @@ [(ngModel)]="node.isSelected" [checked]="node.isSelected" (change)="onSelect(node.item)" + role="checkbox" + tabindex="0" > {{node.item.display}} @@ -87,21 +95,23 @@ [ngbTooltip]="node.item?.otherInformation?.note" [openDelay]="500" container="body" - (click)="onSelect(node.item)"> + (click)="onSelect(node.item)" + role="button" + tabindex="0"> {{node.item.display}} diff --git a/src/app/shared/host-window.service.ts b/src/app/shared/host-window.service.ts index 1c71bf5075..2a07165a1e 100644 --- a/src/app/shared/host-window.service.ts +++ b/src/app/shared/host-window.service.ts @@ -137,10 +137,10 @@ export class HostWindowService { } isXsOrSm(): Observable { - return observableCombineLatest( + return observableCombineLatest([ this.isXs(), this.isSm() - ).pipe( + ]).pipe( map(([isXs, isSm]) => isXs || isSm), distinctUntilChanged() ); diff --git a/src/app/shared/input-suggestions/input-suggestions.component.ts b/src/app/shared/input-suggestions/input-suggestions.component.ts index 7e05dbcc8c..5b22b4ba20 100644 --- a/src/app/shared/input-suggestions/input-suggestions.component.ts +++ b/src/app/shared/input-suggestions/input-suggestions.component.ts @@ -165,11 +165,11 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange } /** - * When any key is pressed (except for the Enter button) the query input should move to the input field + * When any key is pressed (except for the Enter & Tab button) the query input should move to the input field * @param {KeyboardEvent} event The keyboard event */ onKeydown(event: KeyboardEvent) { - if (event.key !== 'Enter') { + if (event.key !== 'Enter' && event.key !== 'Tab') { this.queryInput.nativeElement.focus(); } } diff --git a/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html index 378bb6ed67..5c780d5c3d 100644 --- a/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html +++ b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html @@ -1,3 +1,3 @@ - diff --git a/src/app/shared/log-in/methods/password/log-in-password.component.html b/src/app/shared/log-in/methods/password/log-in-password.component.html index 865247e39a..28a4400470 100644 --- a/src/app/shared/log-in/methods/password/log-in-password.component.html +++ b/src/app/shared/log-in/methods/password/log-in-password.component.html @@ -24,14 +24,14 @@ @fadeOut>{{ (message | async) | translate }}
    + [dsBtnDisabled]="!form.valid" role="button" tabindex="0"> {{"login.form.submit" | translate}} diff --git a/src/app/shared/menu/menu-item/link-menu-item.component.html b/src/app/shared/menu/menu-item/link-menu-item.component.html index 71eeda2e68..f96084e6e1 100644 --- a/src/app/shared/menu/menu-item/link-menu-item.component.html +++ b/src/app/shared/menu/menu-item/link-menu-item.component.html @@ -8,4 +8,5 @@ (keyup.space)="navigate($event)" (keydown.enter)="navigate($event)" href="javascript:void(0);" + tabindex="0" >{{item.text | translate}} diff --git a/src/app/shared/menu/menu-item/text-menu-item.component.html b/src/app/shared/menu/menu-item/text-menu-item.component.html index ba3cf99a49..e2dd334caf 100644 --- a/src/app/shared/menu/menu-item/text-menu-item.component.html +++ b/src/app/shared/menu/menu-item/text-menu-item.component.html @@ -1 +1 @@ -{{item.text | translate}} +{{item.text | translate}} diff --git a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html index ff7efcc309..539ce77939 100644 --- a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html +++ b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html @@ -1,5 +1,5 @@
    - + {{object.value}} diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html index c50b382495..f8c8a39125 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html @@ -1,5 +1,5 @@
    - + {{ dsoNameService.getName(object) }} diff --git a/src/app/shared/object-list/community-list-element/community-list-element.component.html b/src/app/shared/object-list/community-list-element/community-list-element.component.html index 2101261bdc..e00121c666 100644 --- a/src/app/shared/object-list/community-list-element/community-list-element.component.html +++ b/src/app/shared/object-list/community-list-element/community-list-element.component.html @@ -1,5 +1,5 @@
    - + {{ dsoNameService.getName(object) }} diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html index 7d416e9f3e..e94427a6fd 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html @@ -4,14 +4,16 @@ {{mdRepresentation.getValue()}} + target="_blank" [href]="mdRepresentation.getValue()" role="link" tabindex="0"> {{mdRepresentation.getValue()}} {{mdRepresentation.getValue()}} + [queryParams]="getQueryParams()" + role="link" + tabindex="0"> {{mdRepresentation.getValue()}}
    diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index f7a687048a..f2ec34b4ef 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -1,7 +1,7 @@
    + [routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0"> @@ -18,7 +18,7 @@ + [innerHTML]="dsoTitle" role="link" tabindex="0"> diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index 836c94bf6b..d7fdb62c5a 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -7,13 +7,13 @@
    - +
    -
    +
    diff --git a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html index 91738c6e9a..4b0baed7d6 100644 --- a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html @@ -8,11 +8,11 @@ diff --git a/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html index 7d0ad89914..1f7ef98c83 100644 --- a/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html @@ -8,11 +8,11 @@ diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html index 0ebd7ee6be..65c566c1e1 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html @@ -4,7 +4,7 @@ [queryParams]="addQueryParams" queryParamsHandling="merge" (click)="announceFilter()">
    - @@ -44,7 +44,7 @@
    -
    diff --git a/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html index 91738c6e9a..c1cdc3ea0c 100644 --- a/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html @@ -8,11 +8,11 @@ diff --git a/src/app/shared/search/search-filters/search-filters.component.html b/src/app/shared/search/search-filters/search-filters.component.html index e1e54c683d..bb020f85b1 100644 --- a/src/app/shared/search/search-filters/search-filters.component.html +++ b/src/app/shared/search/search-filters/search-filters.component.html @@ -1,13 +1,13 @@

    {{"search.filters.head" | translate}}

    -
    - -
    +
    + +
    - + {{"search.filters.reset" | translate}} diff --git a/src/app/shared/search/search-filters/search-filters.component.ts b/src/app/shared/search/search-filters/search-filters.component.ts index b491f21177..189062fea3 100644 --- a/src/app/shared/search/search-filters/search-filters.component.ts +++ b/src/app/shared/search/search-filters/search-filters.component.ts @@ -2,7 +2,7 @@ import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; +import { map, filter, take } from 'rxjs/operators'; import { SearchService } from '../../../core/shared/search/search.service'; import { RemoteData } from '../../../core/data/remote-data'; @@ -62,9 +62,16 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { searchLink: string; /** - * Filters for which visibility has been computed + * Keeps track of the filters computed for each configuration during the current rendering cycle + * This array stores objects with configuration identifier and number of computed filters */ - filtersWithComputedVisibility = 0; + private currentFiltersComputed = []; + + /** + * Stores the final count of computed filters for each configuration + * Used to determine when all filters for a configuration have been processed + */ + private finalFiltersComputed = []; subs = []; defaultFilterCount: number; @@ -89,7 +96,6 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { ngOnInit(): void { this.clearParams = this.searchConfigService.getCurrentFrontendFilters().pipe( - tap(() => this.filtersWithComputedVisibility = 0), map((filters) => { Object.keys(filters).forEach((f) => filters[f] = null); return filters; @@ -125,7 +131,122 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { countFiltersWithComputedVisibility(computed: boolean) { if (computed) { - this.filtersWithComputedVisibility += 1; + this.filters.pipe( + // Get filter data and check if we need to increment the counter + map(filtersData => { + if (filtersData && filtersData.hasSucceeded && filtersData.payload) { + const totalFilters = filtersData.payload.length; + const currentComputed = this.getCurrentFiltersComputed(this.currentConfiguration); + + // If we've already computed all filters for this configuration + if (currentComputed >= totalFilters) { + // Register in finalFiltersComputed if not already registered + if (!this.findConfigInFinalFilters(this.currentConfiguration)) { + this.updateFinalFiltersComputed(this.currentConfiguration, totalFilters); + } + return { shouldIncrement: false }; + } + + // We haven't reached the total yet, proceed with increment + return { + shouldIncrement: true, + totalFilters + }; + } + return { shouldIncrement: false }; + }), + // Only continue if we need to increment the counter + filter(result => result.shouldIncrement), + // Increment the counter for the current configuration + map(result => { + const filterConfig = this.findConfigInCurrentFilters(this.currentConfiguration); + + if (filterConfig) { + // Update existing counter + filterConfig.filtersComputed += 1; + } else { + // Create new counter entry + this.currentFiltersComputed.push({ + configuration: this.currentConfiguration, + filtersComputed: 1 + }); + } + + // Pass along the total and updated count + return { + totalFilters: result.totalFilters, + currentComputed: this.getCurrentFiltersComputed(this.currentConfiguration) + }; + }), + // Check if we've reached the total after incrementing + map(result => { + if (result.currentComputed === result.totalFilters) { + // If we've reached the total, update final filters count + this.updateFinalFiltersComputed(this.currentConfiguration, result.currentComputed); + } + return result; + }) + ).pipe(take(1)).subscribe(); // Execute the pipeline and immediately unsubscribe } } + + /** + * Finds a configuration entry in the currentFiltersComputed array + * @param configuration The configuration identifier to search for + * @returns The filter configuration object if found, otherwise undefined + */ + private findConfigInCurrentFilters(configuration: string) { + return this.currentFiltersComputed.find( + (configFilter) => configFilter.configuration === configuration + ); + } + + /** + * Finds a configuration entry in the finalFiltersComputed array + * @param configuration The configuration identifier to search for + * @returns The filter configuration object if found, otherwise undefined + */ + private findConfigInFinalFilters(configuration: string) { + return this.finalFiltersComputed.find( + (configFilter) => configFilter.configuration === configuration + ); + } + + /** + * Updates or adds a new entry in the finalFiltersComputed array + * @param configuration The configuration identifier to update + * @param count The number of computed filters to set for this configuration + */ + private updateFinalFiltersComputed(configuration: string, count: number) { + const filterConfig = this.findConfigInFinalFilters(configuration); + + if (filterConfig) { + filterConfig.filtersComputed = count; + } else { + this.finalFiltersComputed.push({ + configuration, + filtersComputed: count + }); + } + } + + /** + * Gets the current number of computed filters for a specific configuration + * @param configuration The configuration identifier to get the count for + * @returns The number of computed filters, or 0 if none found + */ + private getCurrentFiltersComputed(configuration: string) { + const configFilter = this.findConfigInCurrentFilters(configuration); + return configFilter?.filtersComputed || 0; + } + + /** + * Gets the final number of computed filters for a specific configuration + * @param configuration The configuration identifier to get the count for + * @returns The number of computed filters in the final state, or 0 if none found + */ + getFinalFiltersComputed(configuration: string): number { + const configFilter = this.findConfigInFinalFilters(configuration); + return configFilter?.filtersComputed || 0; + } } diff --git a/src/app/shared/search/search-results/search-results.component.html b/src/app/shared/search/search-results/search-results.component.html index a2bc91195c..c396fce5fb 100644 --- a/src/app/shared/search/search-results/search-results.component.html +++ b/src/app/shared/search/search-results/search-results.component.html @@ -32,7 +32,7 @@ {{ 'search.results.no-results' | translate }} + queryParamsHandling="merge" role="link" tabindex="0"> {{"search.results.no-results-link" | translate}}
    diff --git a/src/app/shared/starts-with/date/starts-with-date.component.html b/src/app/shared/starts-with/date/starts-with-date.component.html index b350d9896c..c3fbb884ab 100644 --- a/src/app/shared/starts-with/date/starts-with-date.component.html +++ b/src/app/shared/starts-with/date/starts-with-date.component.html @@ -27,7 +27,7 @@
    - +
    diff --git a/src/app/shared/starts-with/text/starts-with-text.component.html b/src/app/shared/starts-with/text/starts-with-text.component.html index edbd1f1960..3952f86fda 100644 --- a/src/app/shared/starts-with/text/starts-with-text.component.html +++ b/src/app/shared/starts-with/text/starts-with-text.component.html @@ -4,7 +4,7 @@
    - +
    diff --git a/src/app/shared/testing/eperson.mock.ts b/src/app/shared/testing/eperson.mock.ts index 370c2ff1b9..dd2918782f 100644 --- a/src/app/shared/testing/eperson.mock.ts +++ b/src/app/shared/testing/eperson.mock.ts @@ -91,3 +91,43 @@ export const EPersonMock2: EPerson = Object.assign(new EPerson(), { ] } }); + +export const EPersonMockWithNoName: EPerson = Object.assign(new EPerson(), { + handle: null, + groups: [], + netid: 'test@test.com', + lastActive: '2018-05-14T12:25:42.411+0000', + canLogIn: true, + email: 'test@test.com', + requireCertificate: false, + selfRegistered: false, + _links: { + self: { + href: 'https://rest.api/dspace-spring-rest/api/eperson/epersons/testid', + }, + groups: { href: 'https://rest.api/dspace-spring-rest/api/eperson/epersons/testid/groups' }, + }, + id: 'testid', + uuid: 'testid', + type: 'eperson', + metadata: { + 'dc.title': [ + { + language: null, + value: 'User Test', + }, + ], + 'eperson.lastname': [ + { + language: null, + value: 'Test', + }, + ], + 'eperson.language': [ + { + language: null, + value: 'en', + }, + ], + }, +}); diff --git a/src/app/shared/theme-support/themed.component.ts b/src/app/shared/theme-support/themed.component.ts index 0d2833b33f..2e2585f777 100644 --- a/src/app/shared/theme-support/themed.component.ts +++ b/src/app/shared/theme-support/themed.component.ts @@ -82,6 +82,9 @@ export abstract class ThemedComponent implements AfterViewInit, OnDestroy, On } initComponentInstance(changes?: SimpleChanges) { + if (hasValue(this.themeSub)) { + this.themeSub.unsubscribe(); + } this.themeSub = this.themeService?.getThemeName$().subscribe(() => { this.renderComponentInstance(changes); }); diff --git a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html index 55ebe2d957..1ac89e5329 100644 --- a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html +++ b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html @@ -10,6 +10,7 @@ (keyup.Space)="toggle()" role="button" [attr.aria-expanded]="isExpanded" + tabindex="0" > {{ 'item.truncatable-part.show-' + (isExpanded ? 'less' : 'more') | translate }} diff --git a/src/app/shared/upload/uploader/uploader-options.model.ts b/src/app/shared/upload/uploader/uploader-options.model.ts index 559fb0485b..e21628d06e 100644 --- a/src/app/shared/upload/uploader/uploader-options.model.ts +++ b/src/app/shared/upload/uploader/uploader-options.model.ts @@ -22,6 +22,11 @@ export class UploaderOptions { */ maxFileNumber: number; + /** + * Impersonating user uuid + */ + impersonatingID: string; + /** * The request method to use for the file upload request */ diff --git a/src/app/shared/upload/uploader/uploader.component.ts b/src/app/shared/upload/uploader/uploader.component.ts index 2072cb4c83..5033746f6a 100644 --- a/src/app/shared/upload/uploader/uploader.component.ts +++ b/src/app/shared/upload/uploader/uploader.component.ts @@ -23,6 +23,11 @@ import { DragService } from '../../../core/drag.service'; export class UploaderComponent { + /** + * Header key to impersonate a user + */ + private readonly ON_BEHALF_HEADER = 'X-On-Behalf-Of'; + /** * The message to show when drag files on the drop zone */ @@ -139,7 +144,13 @@ export class UploaderComponent { item.url = this.uploader.options.url; } // Ensure the current XSRF token is included in every upload request (token may change between items uploaded) - this.uploader.options.headers = [{ name: XSRF_REQUEST_HEADER, value: this.tokenExtractor.getToken() }]; + // Ensure the behalf header is set if impersonating + this.uploader.options.headers = [ + { name: XSRF_REQUEST_HEADER, value: this.tokenExtractor.getToken() }, + ]; + if (hasValue(this.uploadFilesOptions.impersonatingID)) { + this.uploader.options.headers.push({ name: this.ON_BEHALF_HEADER, value: this.uploadFilesOptions.impersonatingID }); + } this.onBeforeUpload(); this.isOverDocumentDropZone = observableOf(false); }; diff --git a/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.ts b/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.ts new file mode 100644 index 0000000000..fc23c09c51 --- /dev/null +++ b/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.ts @@ -0,0 +1,56 @@ +import { Injectable, } from '@angular/core'; +import { Angulartics2 } from 'angulartics2'; +import { switchMap } from 'rxjs'; +import { filter, take } from 'rxjs/operators'; + +import { ReferrerService } from '../../../core/services/referrer.service'; +import { ActivatedRouteSnapshot, ResolveEnd, Router, RouterStateSnapshot } from '@angular/router'; + +/** + * This component triggers a page view statistic + */ +@Injectable({ + providedIn: 'root' +}) +export class ViewTrackerResolverService { + + constructor( + public angulartics2: Angulartics2, + public referrerService: ReferrerService, + public router: Router, + ) { + } + + resolve(routeSnapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + const dsoPath = routeSnapshot.data.dsoPath || 'dso.payload'; // Fetch the resolvers passed via the route data + this.router.events.pipe( + filter(event => event instanceof ResolveEnd), + take(1), + switchMap(() => + this.referrerService.getReferrer().pipe(take(1)))) + .subscribe((referrer: string) => { + this.angulartics2.eventTrack.next({ + action: 'page_view', + properties: { + object: this.getNestedProperty(routeSnapshot.data, dsoPath), + referrer, + }, + }); + }); + return true; + } + + private getNestedProperty(obj: any, path: string) { + const keys = path.split('.'); + let result = obj; + + for (const key of keys) { + if (result && result.hasOwnProperty(key)) { + result = result[key]; + } else { + return undefined; + } + } + return result; + } +} diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.html b/src/app/statistics/angulartics/dspace/view-tracker.component.html deleted file mode 100644 index c0c0ffe181..0000000000 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.html +++ /dev/null @@ -1 +0,0 @@ -  diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.scss b/src/app/statistics/angulartics/dspace/view-tracker.component.scss deleted file mode 100644 index c76cafbe44..0000000000 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: none -} diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.ts b/src/app/statistics/angulartics/dspace/view-tracker.component.ts deleted file mode 100644 index 805d311cfd..0000000000 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Component, Input, OnInit, OnDestroy } from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { Subscription } from 'rxjs/internal/Subscription'; -import { take } from 'rxjs/operators'; -import { hasValue } from '../../../shared/empty.util'; -import { ReferrerService } from '../../../core/services/referrer.service'; - -/** - * This component triggers a page view statistic - */ -@Component({ - selector: 'ds-view-tracker', - styleUrls: ['./view-tracker.component.scss'], - templateUrl: './view-tracker.component.html', -}) -export class ViewTrackerComponent implements OnInit, OnDestroy { - /** - * The DSpaceObject to track a view event about - */ - @Input() object: DSpaceObject; - - /** - * The subscription on this.referrerService.getReferrer() - * @protected - */ - protected sub: Subscription; - - constructor( - public angulartics2: Angulartics2, - public referrerService: ReferrerService - ) { - } - - ngOnInit(): void { - this.sub = this.referrerService.getReferrer() - .pipe(take(1)) - .subscribe((referrer: string) => { - this.angulartics2.eventTrack.next({ - action: 'page_view', - properties: { - object: this.object, - referrer - }, - }); - }); - } - - ngOnDestroy(): void { - // unsubscribe in the case that this component is destroyed before - // this.referrerService.getReferrer() has emitted - if (hasValue(this.sub)) { - this.sub.unsubscribe(); - } - } -} diff --git a/src/app/statistics/statistics.module.ts b/src/app/statistics/statistics.module.ts index 4870e4fbf0..d93d774394 100644 --- a/src/app/statistics/statistics.module.ts +++ b/src/app/statistics/statistics.module.ts @@ -2,7 +2,6 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CoreModule } from '../core/core.module'; import { SharedModule } from '../shared/shared.module'; -import { ViewTrackerComponent } from './angulartics/dspace/view-tracker.component'; import { StatisticsEndpoint } from './statistics-endpoint.model'; /** @@ -19,10 +18,8 @@ export const models = [ SharedModule, ], declarations: [ - ViewTrackerComponent, ], exports: [ - ViewTrackerComponent, ] }) /** diff --git a/src/app/submission/form/section-add/submission-form-section-add.component.html b/src/app/submission/form/section-add/submission-form-section-add.component.html index 3740de6345..4f9a20187a 100644 --- a/src/app/submission/form/section-add/submission-form-section-add.component.html +++ b/src/app/submission/form/section-add/submission-form-section-add.component.html @@ -2,11 +2,11 @@ #sectionAdd="ngbDropdown" placement="bottom-right" class="d-inline-block" - [ngClass]="{'w-100': windowService.isXs()}"> + [ngClass]="{'w-100': isXs$}"> @@ -14,7 +14,7 @@
    + [ngClass]="{'w-100': (isXs$ | async)}"> diff --git a/src/app/submission/form/section-add/submission-form-section-add.component.ts b/src/app/submission/form/section-add/submission-form-section-add.component.ts index 1a758961d6..f4b399948b 100644 --- a/src/app/submission/form/section-add/submission-form-section-add.component.ts +++ b/src/app/submission/form/section-add/submission-form-section-add.component.ts @@ -42,6 +42,11 @@ export class SubmissionFormSectionAddComponent implements OnInit { */ public hasSections$: Observable; + /** + * A boolean representing whether it's a small screen + */ + isXs$: Observable; + /** * Initialize instance variables * @@ -62,6 +67,7 @@ export class SubmissionFormSectionAddComponent implements OnInit { this.hasSections$ = this.sectionList$.pipe( map((list: SectionDataObject[]) => list.length > 0) ); + this.isXs$ = this.windowService.isXs(); } /** diff --git a/src/app/submission/form/submission-form.component.html b/src/app/submission/form/submission-form.component.html index 4a916cfe23..c9ad7c542e 100644 --- a/src/app/submission/form/submission-form.component.html +++ b/src/app/submission/form/submission-form.component.html @@ -1,5 +1,5 @@
    -
    +
    - +
    - diff --git a/src/app/submission/form/submission-form.component.spec.ts b/src/app/submission/form/submission-form.component.spec.ts index cc77c44afb..9614e2418c 100644 --- a/src/app/submission/form/submission-form.component.spec.ts +++ b/src/app/submission/form/submission-form.component.spec.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core'; +import { ChangeDetectorRef, Component, SimpleChange } from '@angular/core'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; @@ -26,8 +26,9 @@ import { Item } from '../../core/shared/item.model'; import { TestScheduler } from 'rxjs/testing'; import { SectionsService } from '../sections/sections.service'; import { VisibilityType } from '../sections/visibility-type'; +import { TranslateModule } from '@ngx-translate/core'; -describe('SubmissionFormComponent Component', () => { +describe('SubmissionFormComponent', () => { let comp: SubmissionFormComponent; let compAsAny: any; @@ -47,7 +48,9 @@ describe('SubmissionFormComponent Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [], + imports: [ + TranslateModule.forRoot(), + ], declarations: [ SubmissionFormComponent, TestComponent @@ -60,7 +63,6 @@ describe('SubmissionFormComponent Component', () => { ChangeDetectorRef, SubmissionFormComponent ], - schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -117,7 +119,7 @@ describe('SubmissionFormComponent Component', () => { expect(compAsAny.submissionSections).toBeUndefined(); expect(compAsAny.subs).toEqual([]); expect(submissionServiceStub.startAutoSave).not.toHaveBeenCalled(); - expect(comp.loading).toBeObservable(cold('(a|)', { a: true })); + expect(comp.isLoading$).toBeObservable(cold('(a|)', { a: true })); done(); }); @@ -197,7 +199,6 @@ describe('SubmissionFormComponent Component', () => { }); scheduler.flush(); - expect(comp.collectionId).toEqual(submissionObjectNew.collection.id); expect(comp.submissionDefinition).toEqual(submissionObjectNew.submissionDefinition); expect(comp.definitionId).toEqual(submissionObjectNew.submissionDefinition.name); expect(comp.sections).toEqual(submissionObjectNew.sections); @@ -235,7 +236,6 @@ describe('SubmissionFormComponent Component', () => { }); scheduler.flush(); - expect(comp.collectionId).toEqual('45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb'); expect(submissionServiceStub.resetSubmissionObject).not.toHaveBeenCalled(); done(); }); diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index 216aefcfc3..d0b2a32590 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -18,10 +18,10 @@ import { Item } from '../../core/shared/item.model'; import { SectionsType } from '../sections/sections-type'; import { SectionsService } from '../sections/sections.service'; import { SubmissionError } from '../objects/submission-error.model'; -import { SubmissionSectionVisibility } from './../../core/config/models/config-submission-section.model'; import { SubmissionSectionModel } from './../../core/config/models/config-submission-section.model'; import { VisibilityType } from '../sections/visibility-type'; import isEqual from 'lodash/isEqual'; +import { SectionVisibility } from '../objects/section-visibility.model'; /** * This component represents the submission form. @@ -88,7 +88,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { * A boolean representing if a submission form is pending * @type {Observable} */ - public loading: Observable = observableOf(true); + public isLoading$: Observable = observableOf(true); /** * Emits true when the submission config has bitstream uploading enabled in submission @@ -160,7 +160,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { this.uploadEnabled$ = this.sectionsService.isSectionTypeAvailable(this.submissionId, SectionsType.Upload); // check if is submission loading - this.loading = this.submissionService.getSubmissionObject(this.submissionId).pipe( + this.isLoading$ = this.submissionService.getSubmissionObject(this.submissionId).pipe( filter(() => this.isActive), map((submission: SubmissionObjectEntry) => submission.isLoading), map((isLoading: boolean) => isLoading), @@ -173,6 +173,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { distinctUntilChanged()) .subscribe((endpointURL) => { this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); + this.uploadFilesOptions.impersonatingID = this.authService.getImpersonateID(); this.uploadFilesOptions.url = endpointURL.concat(`/${this.submissionId}`); this.definitionId = this.submissionDefinition.name; this.submissionService.dispatchInit( @@ -195,7 +196,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { /** * Returns the visibility object of the collection section */ - private getCollectionVisibility(): SubmissionSectionVisibility { + private getCollectionVisibility(): SectionVisibility { const submissionSectionModel: SubmissionSectionModel = this.submissionDefinition.sections.page.find( (section) => isEqual(section.sectionType, SectionsType.Collection) @@ -249,13 +250,12 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { * new submission object */ onCollectionChange(submissionObject: SubmissionObject) { - this.collectionId = (submissionObject.collection as Collection).id; if (this.definitionId !== (submissionObject.submissionDefinition as SubmissionDefinitionsModel).name) { this.sections = submissionObject.sections; this.submissionDefinition = (submissionObject.submissionDefinition as SubmissionDefinitionsModel); this.definitionId = this.submissionDefinition.name; this.submissionService.resetSubmissionObject( - this.collectionId, + (submissionObject.collection as Collection).id, this.submissionId, submissionObject._links.self.href, this.submissionDefinition, @@ -266,13 +266,6 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { } } - /** - * Check if submission form is loading - */ - isLoading(): Observable { - return this.loading; - } - /** * Check if submission form is loading */ diff --git a/src/app/submission/objects/section-visibility.model.ts b/src/app/submission/objects/section-visibility.model.ts index c41735178c..16cf16b2ab 100644 --- a/src/app/submission/objects/section-visibility.model.ts +++ b/src/app/submission/objects/section-visibility.model.ts @@ -5,3 +5,9 @@ export interface SectionVisibility { main: any; other: any; } + + +export enum SectionScope { + Submission = 'SUBMISSION', + Workflow = 'WORKFLOW', +} diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index 9182611e47..b23e812341 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -11,7 +11,7 @@ import { SubmissionObject } from '../../core/submission/models/submission-object import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; import { SectionsType } from '../sections/sections-type'; import { Item } from '../../core/shared/item.model'; -import { SectionVisibility } from './section-visibility.model'; +import { SectionScope, SectionVisibility } from './section-visibility.model'; import { SubmissionError } from './submission-error.model'; import { SubmissionSectionError } from './submission-section-error.model'; @@ -116,6 +116,7 @@ export class InitSectionAction implements Action { header: string; config: string; mandatory: boolean; + scope: SectionScope; sectionType: SectionsType; visibility: SectionVisibility; enabled: boolean; @@ -136,6 +137,8 @@ export class InitSectionAction implements Action { * the section's config * @param mandatory * the section's mandatory + * @param scope + * the section's scope * @param sectionType * the section's type * @param visibility @@ -152,12 +155,13 @@ export class InitSectionAction implements Action { header: string, config: string, mandatory: boolean, + scope: SectionScope, sectionType: SectionsType, visibility: SectionVisibility, enabled: boolean, data: WorkspaceitemSectionDataType, errors: SubmissionSectionError[]) { - this.payload = { submissionId, sectionId, header, config, mandatory, sectionType, visibility, enabled, data, errors }; + this.payload = { submissionId, sectionId, header, config, mandatory, scope, sectionType, visibility, enabled, data, errors }; } } diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index 11eb375cc6..ed76c41fd0 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -151,6 +151,7 @@ describe('SubmissionObjectEffects test suite', () => { sectionDefinition.header, config, sectionDefinition.mandatory, + sectionDefinition.scope, sectionDefinition.sectionType, sectionDefinition.visibility, enabled, diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 4623d808e2..864b78487f 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -87,6 +87,7 @@ export class SubmissionObjectEffects { sectionDefinition.header, config, sectionDefinition.mandatory, + sectionDefinition.scope, sectionDefinition.sectionType, sectionDefinition.visibility, enabled, diff --git a/src/app/submission/objects/submission-objects.reducer.spec.ts b/src/app/submission/objects/submission-objects.reducer.spec.ts index 2a24afae19..1d2c233dfc 100644 --- a/src/app/submission/objects/submission-objects.reducer.spec.ts +++ b/src/app/submission/objects/submission-objects.reducer.spec.ts @@ -233,6 +233,7 @@ describe('submissionReducer test suite', () => { header: 'submit.progressbar.describe.stepone', config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone', mandatory: true, + scope: null, sectionType: 'submission-form', visibility: undefined, collapsed: false, @@ -253,6 +254,7 @@ describe('submissionReducer test suite', () => { 'submit.progressbar.describe.stepone', 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone', true, + null, SectionsType.SubmissionForm, undefined, true, diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index a05bf05f52..4ded1781a4 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -548,6 +548,7 @@ function initSection(state: SubmissionObjectState, action: InitSectionAction): S header: action.payload.header, config: action.payload.config, mandatory: action.payload.mandatory, + scope: action.payload.scope, sectionType: action.payload.sectionType, visibility: action.payload.visibility, collapsed: false, diff --git a/src/app/submission/objects/submission-section-object.model.ts b/src/app/submission/objects/submission-section-object.model.ts index 16e437da80..8ecdc2607a 100644 --- a/src/app/submission/objects/submission-section-object.model.ts +++ b/src/app/submission/objects/submission-section-object.model.ts @@ -1,5 +1,5 @@ import { SectionsType } from '../sections/sections-type'; -import { SectionVisibility } from './section-visibility.model'; +import { SectionScope, SectionVisibility } from './section-visibility.model'; import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; import { SubmissionSectionError } from './submission-section-error.model'; @@ -22,6 +22,11 @@ export interface SubmissionSectionObject { */ mandatory: boolean; + /** + * The submission scope for this section + */ + scope: SectionScope; + /** * The section type */ diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html index e3e11c258a..0f79fa6a1d 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html @@ -120,28 +120,26 @@ - - -
    - + +
    + +
    +
    +
    + {{ 'submission.sections.ccLicense.link' | translate }}
    -
    -
    - {{ 'submission.sections.ccLicense.link' | translate }} -
    - - {{ licenseLink }} - -
    -
    - - {{ 'submission.sections.ccLicense.confirmation' | translate }} -
    + + {{ licenseLink }} + +
    +
    + + {{ 'submission.sections.ccLicense.confirmation' | translate }}
    - +
    diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts index fff5428c2f..ddb3be855d 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, OnChanges, SimpleChanges, OnInit } from '@angular/core'; import { Observable, of as observableOf, Subscription, tap } from 'rxjs'; import { Field, Option, SubmissionCcLicence } from '../../../core/submission/models/submission-cc-license.model'; import { @@ -15,7 +15,7 @@ import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; import { WorkspaceitemSectionCcLicenseObject } from '../../../core/submission/models/workspaceitem-section-cc-license.model'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { isNotEmpty } from '../../../shared/empty.util'; +import { isNotEmpty, hasValue, hasNoValue } from '../../../shared/empty.util'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { SubmissionCcLicenseUrlDataService } from '../../../core/submission/submission-cc-license-url-data.service'; import {ConfigurationDataService} from '../../../core/data/configuration-data.service'; @@ -30,7 +30,7 @@ import { FindListOptions } from '../../../core/data/find-list-options.model'; styleUrls: ['./submission-section-cc-licenses.component.scss'] }) @renderSectionFor(SectionsType.CcLicense) -export class SubmissionSectionCcLicensesComponent extends SectionModelComponent { +export class SubmissionSectionCcLicensesComponent extends SectionModelComponent implements OnChanges, OnInit { /** * The form id @@ -106,6 +106,8 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent return this.data.accepted; } + ccLicenseLink$: Observable; + constructor( protected modalService: NgbModal, protected sectionService: SectionsService, @@ -125,6 +127,19 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent ); } + ngOnInit(): void { + super.ngOnInit(); + if (hasNoValue(this.ccLicenseLink$)) { + this.ccLicenseLink$ = this.getCcLicenseLink$(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + if (hasValue(changes.sectionData) || hasValue(changes.submissionCcLicenses)) { + this.ccLicenseLink$ = this.getCcLicenseLink$(); + } + } + /** * The data of this section. */ @@ -149,6 +164,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent }, uri: undefined, }); + this.ccLicenseLink$ = this.getCcLicenseLink$(); } /** @@ -180,6 +196,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent }, accepted: false, }); + this.ccLicenseLink$ = this.getCcLicenseLink$(); } /** diff --git a/src/app/submission/sections/form/section-form-operations.service.ts b/src/app/submission/sections/form/section-form-operations.service.ts index 778063dd31..2ae5903d6b 100644 --- a/src/app/submission/sections/form/section-form-operations.service.ts +++ b/src/app/submission/sections/form/section-form-operations.service.ts @@ -403,7 +403,7 @@ export class SectionFormOperationsService { ); } } - } else if (!value.hasValue()) { + } else if (isNotEmpty(value) && !value.hasValue()) { // New value is empty, so dispatch a remove operation if (this.getArrayIndexFromEvent(event) === 0) { this.operationsBuilder.remove(pathCombiner.getPath(segmentedPath)); diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.html b/src/app/submission/sections/identifiers/section-identifiers.component.html index dd0b5d2930..caf249e5b6 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.html +++ b/src/app/submission/sections/identifiers/section-identifiers.component.html @@ -3,18 +3,17 @@ Template for the identifiers submission section component @author Kim Shepherd --> - - -
    - {{'submission.sections.identifiers.info' | translate}} -
      - - -
    • {{'submission.sections.identifiers.' + identifier.identifierType + '_label' | translate}} - {{identifier.value}}
    • + +
      + {{ 'submission.sections.identifiers.info' | translate }} +
        + + +
      • {{ 'submission.sections.identifiers.' + identifier.identifierType + '_label' | translate }} + {{ identifier.value }} +
      • +
        - -
      -
      -
      +
    +
    diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.ts b/src/app/submission/sections/identifiers/section-identifiers.component.ts index ac4af63adb..b5afad721d 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.ts +++ b/src/app/submission/sections/identifiers/section-identifiers.component.ts @@ -1,13 +1,12 @@ import {ChangeDetectionStrategy, Component, Inject } from '@angular/core'; -import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; import { SectionsType } from '../sections-type'; import { SectionModelComponent } from '../models/section.model'; import { renderSectionFor } from '../sections-decorator'; import { SectionDataObject } from '../models/section-data.model'; import { SubmissionService } from '../../submission.service'; -import { AlertType } from '../../../shared/alert/alert-type'; import { SectionsService } from '../sections.service'; import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/models/workspaceitem-section-identifiers.model'; @@ -26,11 +25,6 @@ import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/ @renderSectionFor(SectionsType.Identifiers) export class SubmissionSectionIdentifiersComponent extends SectionModelComponent { - /** - * The Alert categories. - * @type {AlertType} - */ - public AlertTypeEnum = AlertType; /** * Variable to track if the section is loading. @@ -42,14 +36,7 @@ export class SubmissionSectionIdentifiersComponent extends SectionModelComponent * Observable identifierData subject * @type {Observable} */ - public identifierData$: Observable = new Observable(); - - /** - * Array to track all subscriptions and unsubscribe them onDestroy - * @type {Array} - */ - protected subs: Subscription[] = []; - public subbedIdentifierData: WorkspaceitemSectionIdentifiersObject; + public identifierData$: Observable; /** * Initialize instance variables. @@ -71,10 +58,6 @@ export class SubmissionSectionIdentifiersComponent extends SectionModelComponent super(injectedCollectionId, injectedSectionData, injectedSubmissionId); } - ngOnInit() { - super.ngOnInit(); - } - /** * Initialize all instance variables and retrieve configuration. */ @@ -83,13 +66,6 @@ export class SubmissionSectionIdentifiersComponent extends SectionModelComponent this.identifierData$ = this.getIdentifierData(); } - /** - * Check if identifier section has read-only visibility - */ - isReadOnly(): boolean { - return true; - } - /** * Unsubscribe from all subscriptions, if needed. */ diff --git a/src/app/submission/sections/sections.service.spec.ts b/src/app/submission/sections/sections.service.spec.ts index 5aa47d1447..b1540cb55f 100644 --- a/src/app/submission/sections/sections.service.spec.ts +++ b/src/app/submission/sections/sections.service.spec.ts @@ -37,6 +37,7 @@ import { SectionsType } from './sections-type'; import { FormService } from '../../shared/form/form.service'; import { getMockFormService } from '../../shared/mocks/form-service.mock'; import { SubmissionSectionError } from '../objects/submission-section-error.model'; +import { SectionScope } from '../objects/section-visibility.model'; describe('SectionsService test suite', () => { let notificationsServiceStub: NotificationsServiceStub; @@ -253,46 +254,282 @@ describe('SectionsService test suite', () => { }); describe('isSectionReadOnly', () => { - it('should return an observable of true when it\'s a readonly section and scope is not workspace', () => { - store.select.and.returnValue(observableOf({ - visibility: { - main: null, + describe('when submission scope is workspace', () => { + describe('and section scope is workspace', () => { + it('should return an observable of true when visibility main is READONLY and visibility other is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: 'READONLY', + other: null, + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of true when both visibility main and other are READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: 'READONLY', + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility main is null and visibility other is READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: null, + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + + }); + + describe('and section scope is workflow', () => { + it('should return an observable of false when visibility main is READONLY and visibility other is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: 'READONLY', + other: null, + }, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of true when both visibility main and other are READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: 'READONLY', + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of true when visibility main is null and visibility other is READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: null, other: 'READONLY' } - })); + })); - const expected = cold('(b|)', { + const expected = cold('(b|)', { b: true + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + }); - expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + describe('and section scope is null', () => { + it('should return an observable of false', () => { + store.select.and.returnValue(observableOf({ + scope: null, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + }); }); - it('should return an observable of false when it\'s a readonly section and scope is workspace', () => { - store.select.and.returnValue(observableOf({ - visibility: { - main: null, + describe('when submission scope is workflow', () => { + describe('and section scope is workspace', () => { + it('should return an observable of false when visibility main is READONLY and visibility other is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: 'READONLY', + other: null, + }, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of true when both visibility main and other are READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: 'READONLY', + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of true when visibility main is null and visibility other is READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: null, other: 'READONLY' } - })); + })); - const expected = cold('(b|)', { + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: null, + })); + + const expected = cold('(b|)', { b: false + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + }); - expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); - }); + describe('and section scope is workflow', () => { + it('should return an observable of true when visibility main is READONLY and visibility other is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: 'READONLY', + other: null, + }, + })); - it('should return an observable of false when it\'s not a readonly section', () => { - store.select.and.returnValue(observableOf({ - visibility: null - })); + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of true when both visibility main and other is READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: 'READONLY', + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility main is null and visibility other is READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: null, + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); - const expected = cold('(b|)', { - b: false }); - expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + describe('and section scope is null', () => { + it('should return an observable of false', () => { + store.select.and.returnValue(observableOf({ + scope: null, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + }); }); }); diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index 0ea6232237..1454ab9753 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -45,6 +45,7 @@ import { JsonPatchOperationPathCombiner } from '../../core/json-patch/builder/js import { FormError } from '../../shared/form/form.reducer'; import { SubmissionSectionObject } from '../objects/submission-section-object.model'; import { SubmissionSectionError } from '../objects/submission-section-error.model'; +import { SectionScope } from '../objects/section-visibility.model'; /** * A service that provides methods used in submission process. @@ -333,10 +334,14 @@ export class SectionsService { return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( filter((sectionObj) => hasValue(sectionObj)), map((sectionObj: SubmissionSectionObject) => { - return isNotEmpty(sectionObj.visibility) - && ((sectionObj.visibility.other === 'READONLY' && submissionScope !== SubmissionScopeType.WorkspaceItem) - || (sectionObj.visibility.main === 'READONLY' && submissionScope === SubmissionScopeType.WorkspaceItem) - ); + if (isEmpty(submissionScope) || isEmpty(sectionObj.visibility) || isEmpty(sectionObj.scope)) { + return false; + } + const convertedSubmissionScope: SectionScope = submissionScope.valueOf() === SubmissionScopeType.WorkspaceItem.valueOf() ? + SectionScope.Submission : SectionScope.Workflow; + const visibility = convertedSubmissionScope.valueOf() === sectionObj.scope.valueOf() ? + sectionObj.visibility.main : sectionObj.visibility.other; + return visibility === 'READONLY'; }), distinctUntilChanged()); } diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 1e2be5b612..d2cb07ced4 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -42,6 +42,7 @@ import { storeModuleConfig } from '../app.reducer'; import { environment } from '../../environments/environment'; import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service'; import { SubmissionJsonPatchOperationsServiceStub } from '../shared/testing/submission-json-patch-operations-service.stub'; +import { SectionScope } from './objects/section-visibility.model'; describe('SubmissionService test suite', () => { const collectionId = '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f'; @@ -58,6 +59,7 @@ describe('SubmissionService test suite', () => { extraction: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'utils', visibility: { main: 'HIDDEN', @@ -74,6 +76,7 @@ describe('SubmissionService test suite', () => { collection: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'collection', visibility: { main: 'HIDDEN', @@ -213,6 +216,7 @@ describe('SubmissionService test suite', () => { extraction: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'utils', visibility: { main: 'HIDDEN', @@ -229,6 +233,7 @@ describe('SubmissionService test suite', () => { collection: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'collection', visibility: { main: 'HIDDEN', @@ -566,6 +571,7 @@ describe('SubmissionService test suite', () => { describe('getSubmissionSections', () => { it('should return submission form sections', () => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkspaceItem); spyOn((service as any).store, 'select').and.returnValue(hot('a|', { a: subState.objects[826] })); @@ -735,6 +741,7 @@ describe('SubmissionService test suite', () => { describe('getSubmissionStatus', () => { it('should return properly submission status', () => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkspaceItem); spyOn((service as any).store, 'select').and.returnValue(hot('-a-b', { a: subState, b: validSubState @@ -794,41 +801,207 @@ describe('SubmissionService test suite', () => { }); describe('isSectionHidden', () => { - it('should return true/false when section is hidden/visible', () => { - let section: any = { - config: '', - header: '', - mandatory: true, - sectionType: 'collection' as any, - visibility: { - main: 'HIDDEN', - other: 'HIDDEN' - }, - collapsed: false, - enabled: true, - data: {}, - errorsToShow: [], - serverValidationErrors: [], - isLoading: false, - isValid: false - }; - expect(service.isSectionHidden(section)).toBeTruthy(); + describe('when submission scope is workspace', () => { + beforeEach(() => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkspaceItem); + }); + + describe('and section scope is workspace', () => { + it('should return true when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN' + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is workflow', () => { + it('should return false when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is null', () => { + it('should return false', () => { + let section: any = { + scope: null, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); - section = { - header: 'submit.progressbar.describe.keyinformation', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/keyinformation', - mandatory: true, - sectionType: 'submission-form', - collapsed: false, - enabled: true, - data: {}, - errorsToShow: [], - serverValidationErrors: [], - isLoading: false, - isValid: false - }; - expect(service.isSectionHidden(section)).toBeFalsy(); }); + + describe('when submission scope is workflow', () => { + beforeEach(() => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkflowItem); + }); + + describe('and section scope is workspace', () => { + it('should return false when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is workflow', () => { + it('should return true when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is null', () => { + it('should return false', () => { + let section: any = { + scope: null, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + }); + + }); describe('isSubmissionLoading', () => { diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 610570e8c0..87c561878d 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -46,6 +46,7 @@ import { environment } from '../../environments/environment'; import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service'; import { SubmissionSectionObject } from './objects/submission-section-object.model'; import { SubmissionError } from './objects/submission-error.model'; +import { SectionScope } from './objects/section-visibility.model'; function getSubmissionSelector(submissionId: string): MemoizedSelector { return createSelector( @@ -475,9 +476,15 @@ export class SubmissionService { * true if section is hidden, false otherwise */ isSectionHidden(sectionData: SubmissionSectionObject): boolean { - return (isNotUndefined(sectionData.visibility) - && sectionData.visibility.main === 'HIDDEN' - && sectionData.visibility.other === 'HIDDEN'); + const submissionScope: SubmissionScopeType = this.getSubmissionScope(); + if (isEmpty(submissionScope) || isEmpty(sectionData.visibility) || isEmpty(sectionData.scope)) { + return false; + } + const convertedSubmissionScope: SectionScope = submissionScope.valueOf() === SubmissionScopeType.WorkspaceItem.valueOf() ? + SectionScope.Submission : SectionScope.Workflow; + const visibility = convertedSubmissionScope.valueOf() === sectionData.scope.valueOf() ? + sectionData.visibility.main : sectionData.visibility.other; + return visibility === 'HIDDEN'; } /** diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index 049a47e1b0..bd161ea748 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,15 +1,15 @@
    -
    +
    - - -
    + + +
    {{ placeholder | translate }} diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts index 1fe1215bfe..56250bf6e8 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -73,31 +73,31 @@ describe('ThumbnailComponent', () => { describe('loading', () => { it('should start out with isLoading$ true', () => { - expect(comp.isLoading).toBeTrue(); + expect(comp.isLoading$.getValue()).toBeTrue(); }); it('should set isLoading$ to false once an image is successfully loaded', () => { comp.setSrc('http://bit.stream'); fixture.debugElement.query(By.css('img.thumbnail-content')).triggerEventHandler('load', new Event('load')); - expect(comp.isLoading).toBeFalse(); + expect(comp.isLoading$.getValue()).toBeFalse(); }); it('should set isLoading$ to false once the src is set to null', () => { comp.setSrc(null); - expect(comp.isLoading).toBeFalse(); + expect(comp.isLoading$.getValue()).toBeFalse(); }); it('should show a loading animation while isLoading$ is true', () => { expect(de.query(By.css('ds-themed-loading'))).toBeTruthy(); - comp.isLoading = false; + comp.isLoading$.next(false); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('ds-themed-loading'))).toBeFalsy(); }); describe('with a thumbnail image', () => { beforeEach(() => { - comp.src = 'https://bit.stream'; + comp.src$.next('https://bit.stream'); fixture.detectChanges(); }); @@ -106,7 +106,7 @@ describe('ThumbnailComponent', () => { expect(img).toBeTruthy(); expect(img.classes['d-none']).toBeTrue(); - comp.isLoading = false; + comp.isLoading$.next(false); fixture.detectChanges(); img = fixture.debugElement.query(By.css('img.thumbnail-content')); expect(img).toBeTruthy(); @@ -117,14 +117,14 @@ describe('ThumbnailComponent', () => { describe('without a thumbnail image', () => { beforeEach(() => { - comp.src = null; + comp.src$.next(null); fixture.detectChanges(); }); it('should only show the HTML placeholder once done loading', () => { expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeFalsy(); - comp.isLoading = false; + comp.isLoading$.next(false); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeTruthy(); }); @@ -220,14 +220,14 @@ describe('ThumbnailComponent', () => { describe('fallback', () => { describe('if there is a default image', () => { it('should display the default image', () => { - comp.src = 'http://bit.stream'; + comp.src$.next('http://bit.stream'); comp.defaultImage = 'http://default.img'; comp.errorHandler(); - expect(comp.src).toBe(comp.defaultImage); + expect(comp.src$.getValue()).toBe(comp.defaultImage); }); it('should include the alt text', () => { - comp.src = 'http://bit.stream'; + comp.src$.next('http://bit.stream'); comp.defaultImage = 'http://default.img'; comp.errorHandler(); @@ -239,10 +239,10 @@ describe('ThumbnailComponent', () => { describe('if there is no default image', () => { it('should display the HTML placeholder', () => { - comp.src = 'http://default.img'; + comp.src$.next('http://default.img'); comp.defaultImage = null; comp.errorHandler(); - expect(comp.src).toBe(null); + expect(comp.src$.getValue()).toBe(null); fixture.detectChanges(); const placeholder = fixture.debugElement.query(By.css('div.thumbnail-placeholder')).nativeElement; @@ -334,7 +334,7 @@ describe('ThumbnailComponent', () => { it('should show the default image', () => { comp.defaultImage = 'default/image.jpg'; comp.ngOnChanges({}); - expect(comp.src).toBe('default/image.jpg'); + expect(comp.src$.getValue()).toBe('default/image.jpg'); }); }); }); @@ -382,7 +382,7 @@ describe('ThumbnailComponent', () => { }); it('should start out with isLoading$ true', () => { - expect(comp.isLoading).toBeTrue(); + expect(comp.isLoading$.getValue()).toBeTrue(); expect(de.query(By.css('ds-themed-loading'))).toBeTruthy(); }); diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index 6bfe35bee3..5f8927f3a2 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -2,7 +2,7 @@ import { Component, Inject, Input, OnChanges, PLATFORM_ID, SimpleChanges } from import { Bitstream } from '../core/shared/bitstream.model'; import { hasNoValue, hasValue } from '../shared/empty.util'; import { RemoteData } from '../core/data/remote-data'; -import { of as observableOf } from 'rxjs'; +import { of as observableOf, BehaviorSubject } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; @@ -35,7 +35,7 @@ export class ThumbnailComponent implements OnChanges { /** * The src attribute used in the template to render the image. */ - src: string = undefined; + src$: BehaviorSubject = new BehaviorSubject(undefined); retriedWithToken = false; @@ -58,7 +58,7 @@ export class ThumbnailComponent implements OnChanges { * Whether the thumbnail is currently loading * Start out as true to avoid flashing the alt text while a thumbnail is being loaded. */ - isLoading = true; + isLoading$: BehaviorSubject = new BehaviorSubject(true); constructor( @Inject(PLATFORM_ID) private platformID: any, @@ -114,7 +114,7 @@ export class ThumbnailComponent implements OnChanges { * Otherwise, fall back to the default image or a HTML placeholder */ errorHandler() { - const src = this.src; + const src = this.src$.getValue(); const thumbnail = this.bitstream; const thumbnailSrc = thumbnail?._links?.content?.href; @@ -166,9 +166,22 @@ export class ThumbnailComponent implements OnChanges { * @param src */ setSrc(src: string): void { - this.src = src; - if (src === null) { - this.isLoading = false; + // only update the src if it has changed (the parent component may fire the same one multiple times + if (this.src$.getValue() !== src) { + // every time the src changes we need to start the loading animation again, as it's possible + // that it is first set to null when the parent component initializes and then set to + // the actual value + // + // isLoading$ will be set to false by the error or success handler afterwards, except in the + // case where src is null, then we have to set it manually here (because those handlers won't + // trigger) + if (src !== null && this.isLoading$.getValue() === false) { + this.isLoading$.next(true); + } + this.src$.next(src); + if (src === null && this.isLoading$.getValue() === true) { + this.isLoading$.next(false); + } } } @@ -176,6 +189,6 @@ export class ThumbnailComponent implements OnChanges { * Stop the loading animation once the thumbnail is successfully loaded */ successHandler() { - this.isLoading = false; + this.isLoading$.next(false); } } diff --git a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts index 4bb3eac513..c5d6ee9520 100644 --- a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts +++ b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts @@ -27,6 +27,7 @@ export class WorkflowItemPageResolver implements Resolve> { +export class WorkspaceItemPageResolver implements Resolve> { constructor(private workspaceItemService: WorkspaceitemDataService) { } @@ -22,11 +22,12 @@ export class WorkspaceItemPageResolver implements Resolve> Emits the found workflow item based on the parameters in the current route, * or an error if something went wrong */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { return this.workspaceItemService.findById(route.params.id, true, false, followLink('item'), + followLink('collection'), ).pipe( getFirstCompletedRemoteData(), ); diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index 5e48b3d7e8..8f624cbd39 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -847,7 +847,7 @@ "bitstream.edit.bitstream": "Bitstream: ", // "bitstream.edit.form.description.hint": "Optionally, provide a brief description of the file, for example \"Main article\" or \"Experiment data readings\".", - "bitstream.edit.form.description.hint": "Hier können Sie eine kurze Beschreibung der Datei angeben, zum Beispiel \"Artikel\" oder \"Tabellenhanhang\".", + "bitstream.edit.form.description.hint": "Hier können Sie eine kurze Beschreibung der Datei angeben, zum Beispiel \"Artikel\" oder \"Tabellenanhang\".", // "bitstream.edit.form.description.label": "Description", "bitstream.edit.form.description.label": "Beschreibung", @@ -1639,6 +1639,12 @@ // "cookies.consent.decline": "Decline", "cookies.consent.decline": "Ablehnen", + // "cookies.consent.ok": "That's ok", + "cookies.consent.ok": "Zustimmen", + + // "cookies.consent.save": "Save", + "cookies.consent.save": "Speichern", + // "cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: Authentication, Preferences, Acknowledgement and Statistics.
    To learn more, please read our {privacyPolicy}.", "cookies.consent.content-notice.description": "Wir sammeln und verarbeiten Ihre personenbezogenen Daten für die folgenden Zwecke: Authentifikation, Einstellungen, Zustimmungen und Statistiken.
    Um mehr zu erfahren, lesen Sie bitte unsere {privacyPolicy}.", @@ -1655,9 +1661,13 @@ "cookies.consent.content-modal.privacy-policy.text": "Um mehr zu erfahren, lesen Sie bitte unsere {privacyPolicy}.", // "cookies.consent.content-modal.title": "Information that we collect", - "cookies.consent.content-modal.title": "Information, die wir sammeln", + "cookies.consent.content-modal.title": "Informationen, die wir sammeln", + // "cookies.consent.content-modal.services": "services", + "cookies.consent.content-modal.services": "Dienste", + // "cookies.consent.content-modal.service": "service", + "cookies.consent.content-modal.service": "Dienst", // "cookies.consent.app.title.authentication": "Authentication", "cookies.consent.app.title.authentication": "Authentifizierung", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 7a2f5a55ef..ed6e732e24 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1,5 +1,4 @@ { - "401.help": "You're not authorized to access this page. You can use the button below to get back to the home page.", "401.link.home-page": "Take me to the home page", @@ -2604,6 +2603,14 @@ "item.preview.oaire.citation.volume": "Volume", + "item.preview.dc.relation.hasversion": "Has version", + + "item.preview.dc.relation.ispartofseries": "Is part of series", + + "item.preview.dc.rights": "Rights", + + "item.preview.dc.identifier.other": "Other Identifier", + "item.preview.dc.relation.issn": "ISSN", "item.preview.dc.identifier.isbn": "ISBN", @@ -3854,6 +3861,10 @@ "search.filters.filter.creativeDatePublished.label": "Search date published", + "search.filters.filter.creativeDatePublished.min.label": "Start", + + "search.filters.filter.creativeDatePublished.max.label": "End", + "search.filters.filter.creativeWorkEditor.head": "Editor", "search.filters.filter.creativeWorkEditor.placeholder": "Editor", @@ -4374,6 +4385,8 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalIssue": "Local Journal Issues ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfIssue": "Local Journal Volumes ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfPublication": "Local Journal Volumes ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalVolume": "Local Journal Volumes ({{ count }})", @@ -4420,6 +4433,8 @@ "submission.sections.describe.relationship-lookup.title.JournalIssue": "Journal Issues", + "submission.sections.describe.relationship-lookup.title.isJournalVolumeOfIssue": "Journal Volumes", + "submission.sections.describe.relationship-lookup.title.isJournalVolumeOfPublication": "Journal Volumes", "submission.sections.describe.relationship-lookup.title.JournalVolume": "Journal Volumes", @@ -4460,6 +4475,8 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalOfPublication": "Selected Journals", + "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalVolumeOfIssue": "Selected Journal Volume", + "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalVolumeOfPublication": "Selected Journal Volume", "submission.sections.describe.relationship-lookup.selection-tab.title.Project": "Selected Projects", @@ -4870,6 +4887,8 @@ "subscriptions.tooltip": "Subscribe", + "subscriptions.unsubscribe": "Unsubscribe", + "subscriptions.modal.title": "Subscriptions", "subscriptions.modal.type-frequency": "Type and frequency", diff --git a/src/config/universal-config.interface.ts b/src/config/universal-config.interface.ts index 84678b6dcf..531f7d54d5 100644 --- a/src/config/universal-config.interface.ts +++ b/src/config/universal-config.interface.ts @@ -1,5 +1,10 @@ import { Config } from './config.interface'; +export interface SsrExcludePatterns { + pattern: string | RegExp; + flag?: string; +} + export interface UniversalConfig extends Config { preboot: boolean; async: boolean; @@ -32,9 +37,9 @@ export interface UniversalConfig extends Config { replaceRestUrl: boolean; /** - * Paths to enable SSR for. Defaults to the home page and paths in the sitemap. + * Patterns to be used as regexes to match url's path and check if SSR is disabled for it. */ - paths: Array; + excludePathPatterns: SsrExcludePatterns[]; /** * Whether to enable rendering of search component on SSR diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts index bdb953d181..e5066ab48f 100644 --- a/src/environments/environment.production.ts +++ b/src/environments/environment.production.ts @@ -11,7 +11,25 @@ export const environment: Partial = { inlineCriticalCss: false, transferState: true, replaceRestUrl: true, - paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ], + excludePathPatterns: [ + { + pattern: '^/communities/[a-f0-9-]{36}/browse(/.*)?$', + flag: 'i', + }, + { + pattern: '^/collections/[a-f0-9-]{36}/browse(/.*)?$', + flag: 'i', + }, + { pattern: '^/browse/' }, + { pattern: '^/search' }, + { pattern: '^/community-list$' }, + { pattern: '^/statistics/?' }, + { pattern: '^/admin/' }, + { pattern: '^/processes/?' }, + { pattern: '^/notifications/' }, + { pattern: '^/access-control/' }, + { pattern: '^/health$' }, + ], enableSearchComponent: false, enableBrowseComponent: false, }, diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index a46211fe0e..34386cecc6 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -14,7 +14,25 @@ export const environment: BuildConfig = { inlineCriticalCss: false, transferState: true, replaceRestUrl: false, - paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ], + excludePathPatterns: [ + { + pattern: '^/communities/[a-f0-9-]{36}/browse(/.*)?$', + flag: 'i', + }, + { + pattern: '^/collections/[a-f0-9-]{36}/browse(/.*)?$', + flag: 'i', + }, + { pattern: '^/browse/' }, + { pattern: '^/search' }, + { pattern: '^/community-list$' }, + { pattern: '^/statistics/?' }, + { pattern: '^/admin/' }, + { pattern: '^/processes/?' }, + { pattern: '^/notifications/' }, + { pattern: '^/access-control/' }, + { pattern: '^/health$' }, + ], enableSearchComponent: false, enableBrowseComponent: false, }, diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 502bae140a..11525e4790 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -16,7 +16,25 @@ export const environment: Partial = { inlineCriticalCss: false, transferState: true, replaceRestUrl: false, - paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ], + excludePathPatterns: [ + { + pattern: '^/communities/[a-f0-9-]{36}/browse(/.*)?$', + flag: 'i', + }, + { + pattern: '^/collections/[a-f0-9-]{36}/browse(/.*)?$', + flag: 'i', + }, + { pattern: '^/browse/' }, + { pattern: '^/search' }, + { pattern: '^/community-list$' }, + { pattern: '^/statistics/?' }, + { pattern: '^/admin/' }, + { pattern: '^/processes/?' }, + { pattern: '^/notifications/' }, + { pattern: '^/access-control/' }, + { pattern: '^/health$' }, + ], enableSearchComponent: false, enableBrowseComponent: false, }, diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss index 07c1cf538e..50dff1d739 100644 --- a/src/styles/_global-styles.scss +++ b/src/styles/_global-styles.scss @@ -230,15 +230,31 @@ ds-dynamic-form-control-container.d-none { } .thumb-font-2 { - .thumbnail-placeholder { - font-size: 0.9rem; - padding: 0.125rem; + .thumbnail-placeholder { + @media screen and (max-width: map-get($grid-breakpoints, sm)) { + font-size: 0.4rem; + padding: 0.1rem; } + @media screen and (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, lg)) { + font-size: 0.7rem; + padding: 0.1rem; + } + font-size: 1rem; + padding: 0.5rem; + } } .thumb-font-3 { .thumbnail-placeholder { - font-size: 1.25rem; + @media screen and (max-width: map-get($grid-breakpoints, sm)) { + font-size: 0.4rem; + padding: 0.1rem; + } + @media screen and (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, lg)) { + font-size: 0.7rem; + padding: 0.1rem; + } + font-size: 1rem; padding: 0.5rem; } } diff --git a/src/styles/_truncatable-part.component.scss b/src/styles/_truncatable-part.component.scss index d9158a6751..127dda0171 100644 --- a/src/styles/_truncatable-part.component.scss +++ b/src/styles/_truncatable-part.component.scss @@ -4,24 +4,11 @@ height: $lines * $height; } .content { - max-height: $lines * $height; - position: relative; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: $lines; + -webkit-box-orient: vertical; overflow: hidden; - line-height: $line-height; - overflow-wrap: break-word; - &:after { - content: ""; - position: absolute; - padding-right: 15px; - top: ($lines - 1) * $height; - right: 0; - width: 30%; - min-width: 75px; - max-width: 150px; - height: $height; - background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba($bg, 1) 70%); - pointer-events: none; - } } } diff --git a/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.html b/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.scss b/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.ts b/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.ts new file mode 100644 index 0000000000..358f11f0d1 --- /dev/null +++ b/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { AdminSearchPageComponent as BaseComponent } from '../../../../../app/admin/admin-search-page/admin-search-page.component'; + +@Component({ + selector: 'ds-admin-search-page', + // styleUrls: ['./admin-search-page.component.scss'], + styleUrls: ['../../../../../app/admin/admin-search-page/admin-search-page.component.scss'], + // templateUrl: './admin-search-page.component.html', + templateUrl: '../../../../../app/admin/admin-search-page/admin-search-page.component.html', +}) +export class AdminSearchPageComponent extends BaseComponent { +} diff --git a/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.html b/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.scss b/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.ts b/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.ts new file mode 100644 index 0000000000..27c7e57fed --- /dev/null +++ b/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { AdminWorkflowPageComponent as BaseComponent } from '../../../../../app/admin/admin-workflow-page/admin-workflow-page.component'; + +@Component({ + selector: 'ds-admin-workflow-page', + // styleUrls: ['./admin-workflow-page.component.scss'], + styleUrls: ['../../../../../app/admin/admin-workflow-page/admin-workflow-page.component.scss'], + // templateUrl: './admin-workflow-page.component.html', + templateUrl: '../../../../../app/admin/admin-workflow-page/admin-workflow-page.component.html', +}) +export class AdminWorkflowPageComponent extends BaseComponent { +} diff --git a/src/themes/custom/lazy-theme.module.ts b/src/themes/custom/lazy-theme.module.ts index 546d2dccbf..011c2ea4ae 100644 --- a/src/themes/custom/lazy-theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -161,6 +161,8 @@ import { BrowseByComponent } from './app/shared/browse-by/browse-by.component'; import { RegisterEmailFormComponent } from './app/register-email-form/register-email-form.component'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { AdminSearchPageComponent } from './app/admin/admin-search-page/admin-search-page.component'; +import { AdminWorkflowPageComponent } from './app/admin/admin-workflow-page/admin-workflow-page.component'; const DECLARATIONS = [ FileSectionComponent, @@ -247,6 +249,8 @@ const DECLARATIONS = [ UserMenuComponent, BrowseByComponent, RegisterEmailFormComponent, + AdminSearchPageComponent, + AdminWorkflowPageComponent, ]; @NgModule({ diff --git a/src/themes/dspace/app/header/header.component.html b/src/themes/dspace/app/header/header.component.html index 02226ac0a1..eaf107dff8 100644 --- a/src/themes/dspace/app/header/header.component.html +++ b/src/themes/dspace/app/header/header.component.html @@ -5,7 +5,7 @@ [attr.role]="(isMobile$ | async) ? 'navigation' : 'presentation'" [attr.aria-label]="(isMobile$ | async) ? ('nav.main.description' | translate) : null" class="h-100 flex-fill d-flex flex-row flex-nowrap justify-content-start align-items-center gapx-3"> - +
    diff --git a/yarn.lock b/yarn.lock index 7983caea23..dd2ae7fee7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1352,12 +1352,10 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/runtime@7.26.7", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.26.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.7.tgz#f4e7fe527cd710f8dc0618610b61b4b060c3c341" - integrity sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ== - dependencies: - regenerator-runtime "^0.14.0" +"@babel/runtime@7.27.6", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.27.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6" + integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q== "@babel/template@7.20.7": version "7.20.7" @@ -1712,12 +1710,12 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz#200a0965cf654ac28b971358ecdca9cc5b44c335" integrity sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg== -"@eslint-community/eslint-utils@^4.2.0": - version "4.4.0" - resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.5.1": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz#b0fc7e06d0c94f801537fd4237edc2706d3b8e4c" + integrity sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w== dependencies: - eslint-visitor-keys "^3.3.0" + eslint-visitor-keys "^3.4.3" "@eslint-community/regexpp@^4.4.0": version "4.5.0" @@ -2243,6 +2241,11 @@ "@parcel/watcher-win32-ia32" "2.4.1" "@parcel/watcher-win32-x64" "2.4.1" +"@pkgr/core@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.4.tgz#d897170a2b0ba51f78a099edccd968f7b103387c" + integrity sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw== + "@react-dnd/asap@^4.0.0": version "4.0.1" resolved "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz" @@ -2539,10 +2542,10 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.17.15": - version "4.17.15" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.15.tgz#12d4af0ed17cc7600ce1f9980cec48fc17ad1e89" - integrity sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw== +"@types/lodash@^4.17.17": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.17.tgz#fb85a04f47e9e4da888384feead0de05f7070355" + integrity sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ== "@types/mime@*": version "3.0.1" @@ -2605,10 +2608,10 @@ resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/sanitize-html@^2.13.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.13.0.tgz#ac3620e867b7c68deab79c72bd117e2049cdd98e" - integrity sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ== +"@types/sanitize-html@^2.16.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.16.0.tgz#860d72c1ba8a5d044946f37559cc359c0a13b24e" + integrity sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw== dependencies: htmlparser2 "^8.0.0" @@ -3037,6 +3040,11 @@ acorn@^8.1.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8 resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== +acorn@^8.14.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + adjust-sourcemap-loader@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz" @@ -3404,10 +3412,10 @@ aws4@^1.8.0: resolved "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz" integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== -axe-core@^4.10.2: - version "4.10.2" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df" - integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w== +axe-core@^4.10.3: + version "4.10.3" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.3.tgz#04145965ac7894faddbac30861e5d8f11bfd14fc" + integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== axios@0.21.4: version "0.21.4" @@ -3416,10 +3424,10 @@ axios@0.21.4: dependencies: follow-redirects "^1.14.0" -axios@^1.7.9: - version "1.7.9" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" - integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== +axios@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54" + integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -4129,10 +4137,10 @@ compression-webpack-plugin@^9.2.0: schema-utils "^4.0.0" serialize-javascript "^6.0.0" -compression@^1.7.4, compression@^1.7.5: - version "1.7.5" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.5.tgz#fdd256c0a642e39e314c478f6c2cd654edd74c93" - integrity sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q== +compression@^1.7.4, compression@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.0.tgz#09420efc96e11a0f44f3a558de59e321364180f7" + integrity sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA== dependencies: bytes "3.1.2" compressible "~2.0.18" @@ -4272,10 +4280,10 @@ core-js-compat@^3.25.1: dependencies: browserslist "^4.21.5" -core-js@^3.40.0: - version "3.40.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.40.0.tgz#2773f6b06877d8eda102fc42f828176437062476" - integrity sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ== +core-js@^3.42.0: + version "3.42.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.42.0.tgz#edbe91f78ac8cfb6df8d997e74d368a68082fe37" + integrity sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g== core-util-is@1.0.2: version "1.0.2" @@ -5299,10 +5307,10 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-compat-utils@^0.6.0: - version "0.6.4" - resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.6.4.tgz#173d305132da755ac3612cccab03e1b2e14235ed" - integrity sha512-/u+GQt8NMfXO8w17QendT4gvO5acfxQsAKirAt0LVxDnr2N8YLCVbregaNc/Yhp7NM128DwCaRvr8PLDfeNkQw== +eslint-compat-utils@^0.6.4: + version "0.6.5" + resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.6.5.tgz#6b06350a1c947c4514cfa64a170a6bfdbadc7ec2" + integrity sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ== dependencies: semver "^7.5.4" @@ -5377,19 +5385,19 @@ eslint-plugin-jsdoc@^45.0.0: semver "^7.5.1" spdx-expression-parse "^3.0.1" -eslint-plugin-jsonc@^2.19.1: - version "2.19.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.19.1.tgz#aeedd7131d115b8c46f439a8855139837a0e2752" - integrity sha512-MmlAOaZK1+Lg7YoCZPGRjb88ZjT+ct/KTsvcsbZdBm+w8WMzGx+XEmexk0m40P1WV9G2rFV7X3klyRGRpFXEjA== +eslint-plugin-jsonc@^2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.20.1.tgz#138b41e857a2add02b5408b13f3bc6f14d51d702" + integrity sha512-gUzIwQHXx7ZPypUoadcyRi4WbHW2TPixDr0kqQ4miuJBU0emJmyGTlnaT3Og9X2a8R1CDayN9BFSq5weGWbTng== dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - eslint-compat-utils "^0.6.0" + "@eslint-community/eslint-utils" "^4.5.1" + eslint-compat-utils "^0.6.4" eslint-json-compat-utils "^0.2.1" - espree "^9.6.1" + espree "^9.6.1 || ^10.3.0" graphemer "^1.4.0" - jsonc-eslint-parser "^2.0.4" + jsonc-eslint-parser "^2.4.0" natural-compare "^1.4.0" - synckit "^0.6.0" + synckit "^0.6.2 || ^0.7.3 || ^0.11.5" eslint-plugin-lodash@^7.4.0: version "7.4.0" @@ -5451,11 +5459,16 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4 resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz" integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== -eslint-visitor-keys@^3.4.1: +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + eslint@^8.39.0: version "8.39.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1" @@ -5507,7 +5520,7 @@ esm@^3.2.25: resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== -espree@^9.0.0, espree@^9.5.1, espree@^9.6.1: +espree@^9.0.0, espree@^9.5.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -5516,6 +5529,15 @@ espree@^9.0.0, espree@^9.5.1, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" +"espree@^9.6.1 || ^10.3.0": + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.0" + esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" @@ -6491,10 +6513,10 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" -http-proxy-middleware@^2.0.3, http-proxy-middleware@^2.0.6, http-proxy-middleware@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" - integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== +http-proxy-middleware@^2.0.3, http-proxy-middleware@^2.0.6, http-proxy-middleware@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" + integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" @@ -7080,10 +7102,10 @@ isbinaryfile@^4.0.8: resolved "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz" integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== -isbot@^5.1.22: - version "5.1.22" - resolved "https://registry.yarnpkg.com/isbot/-/isbot-5.1.22.tgz#18a4a58bbfff6974ff7868dafea59907deb7b68d" - integrity sha512-RqCFY3cJy3c2y1I+rMn81cfzAR4XJwfPBC+M8kffUjbPzxApzyyv7Tbm1C/gXXq2dSCuD238pKFEWlQMTWsTFw== +isbot@^5.1.28: + version "5.1.28" + resolved "https://registry.yarnpkg.com/isbot/-/isbot-5.1.28.tgz#a9a32e70c890cf19b76090971b1fccf6021a519b" + integrity sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw== isexe@^2.0.0: version "2.0.0" @@ -7350,10 +7372,10 @@ json5@^2.1.2, json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonc-eslint-parser@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.1.0.tgz#4c126b530aa583d85308d0b3041ff81ce402bbb2" - integrity sha512-qCRJWlbP2v6HbmKW7R3lFbeiVWHo+oMJ0j+MizwvauqnCV/EvtAeEeuCgoc/ErtsuoKgYB8U4Ih8AxJbXoE6/g== +jsonc-eslint-parser@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz#74ded53f9d716e8d0671bd167bf5391f452d5461" + integrity sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg== dependencies: acorn "^8.5.0" eslint-visitor-keys "^3.0.0" @@ -8336,10 +8358,10 @@ mute-stream@0.0.8: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nanoid@^3.3.6, nanoid@^3.3.8: - version "3.3.8" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" - integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== +nanoid@^3.3.11, nanoid@^3.3.6: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== natural-compare-lite@^1.4.0: version "1.4.0" @@ -8375,10 +8397,10 @@ neo-async@^2.6.2: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -ng-mocks@^14.13.2: - version "14.13.2" - resolved "https://registry.yarnpkg.com/ng-mocks/-/ng-mocks-14.13.2.tgz#ddd675d675eb16dfa85834e28dd42343853a6622" - integrity sha512-ItAB72Pc0uznL1j4TPsFp1wehhitVp7DARkc67aafeIk1FDgwnAZvzJwntMnIp/IWMSbzrEQ6kl3cc5euX1NRA== +ng-mocks@^14.13.5: + version "14.13.5" + resolved "https://registry.yarnpkg.com/ng-mocks/-/ng-mocks-14.13.5.tgz#0f73b0cf1d26595668fd451d092ec1a029c42f69" + integrity sha512-/0eMqYgKoy7ySvjXcD+2eOv8h9v84+JFOLIrhwX1sZLXHgwV8wVbZct5djxnbHJzwmOgQ3tn3osDZSSs3lIFuw== ng2-file-upload@1.4.0: version "1.4.0" @@ -9454,11 +9476,11 @@ postcss@8.4.31: source-map-js "^1.0.2" postcss@^8.2.14, postcss@^8.3.11, postcss@^8.3.7, postcss@^8.4.19, postcss@^8.5: - version "8.5.1" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.1.tgz#e2272a1f8a807fafa413218245630b5db10a3214" - integrity sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ== + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== dependencies: - nanoid "^3.3.8" + nanoid "^3.3.11" picocolors "^1.1.1" source-map-js "^1.2.1" @@ -9956,11 +9978,6 @@ regenerator-runtime@^0.13.11: resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - regenerator-transform@^0.15.1: version "0.15.1" resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz" @@ -10177,13 +10194,20 @@ rxjs@6.6.7, rxjs@^6.5.5, rxjs@~6.6.0: dependencies: tslib "^1.9.0" -rxjs@7.8.1, rxjs@^7.5.1, rxjs@^7.5.5, rxjs@^7.8.0: +rxjs@7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" +rxjs@^7.5.1, rxjs@^7.5.5, rxjs@^7.8.2: + version "7.8.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" + integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== + dependencies: + tslib "^2.1.0" + safe-array-concat@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" @@ -10223,10 +10247,10 @@ safe-stable-stringify@^2.4.3: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sanitize-html@^2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.14.0.tgz#bd2a7b97ee1d86a7f0e0babf3a4468f639c3a429" - integrity sha512-CafX+IUPxZshXqqRaG9ZClSlfPVjSxI0td7n07hk8QO2oO+9JDnlcL8iM8TWeOXOIBFgIOx6zioTzM53AOMn3g== +sanitize-html@^2.17.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.0.tgz#a8f66420a6be981d8fe412e3397cc753782598e4" + integrity sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" @@ -10270,10 +10294,10 @@ sass@1.58.1: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -sass@^1.25.0, sass@~1.84.0: - version "1.84.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.84.0.tgz#da9154cbccb2d2eac7a9486091b6d9ba93ef5bad" - integrity sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg== +sass@^1.25.0, sass@~1.89.1: + version "1.89.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.1.tgz#9281c52c85b4be54264d310fef63a811dfcfb9d9" + integrity sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q== dependencies: chokidar "^4.0.0" immutable "^5.0.2" @@ -10997,12 +11021,12 @@ symbol-tree@^3.2.4: resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -synckit@^0.6.0: - version "0.6.2" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.6.2.tgz#e1540b97825f2855f7170b98276e8463167f33eb" - integrity sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA== +"synckit@^0.6.2 || ^0.7.3 || ^0.11.5": + version "0.11.6" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.6.tgz#e742a0c27bbc1fbc96f2010770521015cca7ed5c" + integrity sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw== dependencies: - tslib "^2.3.1" + "@pkgr/core" "^0.2.4" tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" @@ -11250,7 +11274,7 @@ tslib@2.3.1: resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tslib@2.5.0, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1: +tslib@2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== @@ -11260,6 +11284,11 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"